-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathResize-DevDrive.ps1
More file actions
801 lines (686 loc) · 33.9 KB
/
Copy pathResize-DevDrive.ps1
File metadata and controls
801 lines (686 loc) · 33.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
<#
.SYNOPSIS
Resizes a Windows Dev Drive to a target size by recreating the VHDX (data-preserving).
.DESCRIPTION
INTENT:
=======
The Dev Drive D: (mounted as a VHDX at C:\DevDrive.vhdx) is full at 50 GB.
This script creates a new 100 GB Dev Drive with the same configuration:
- Same VHDX path: C:\DevDrive.vhdx
- Same drive letter: D:
- Same file system: ReFS (Dev Drive)
- Same junction: C:\Development -> D:\
PREREQUISITES:
==============
- Windows 11 with Dev Drive support (Build 26100+ / 23H2+)
- The Format-Volume -DevDrive parameter must exist (verified 2026-08-07)
- Administrator rights (elevated PowerShell)
- VS Code and all programs accessing D:\ or C:\Development\ MUST be closed
- Enough space on C: (~50 GB for backup + 100 GB for the new VHDX = ~150 GB, available: ~385 GB)
STARTING STATE (determined 2026-08-07):
=======================================
- C:\DevDrive.vhdx: 49.41 GB (dynamic, File Backed Virtual Disk)
- Volume D: 50 GB ReFS, label "Development", 0 bytes free
- C:\Development is a junction -> Volume{3bffa0c3-fb9b-4d3e-9475-8518b412a905}\
- Disk 1, GPT, BusType "File Backed Virtual"
- Content: D:\git\ (~24.7 GB), D:\bap\ (~0 GB), Recycle Bin (~4.3 GB)
- ReFS metadata: 1.6 GB
- C: has 385 GB free
FLOW (7 phases):
================
Phase 1: Validation - Checks prerequisites (admin, space, no locks)
Phase 2: Backup - Copies all of D:\ to C:\DevDriveBackup\ (robocopy)
Phase 3: Dismount - Removes junction C:\Development, dismounts the VHDX
Phase 4: Destroy - Deletes the old 50 GB VHDX
Phase 5: Create - Creates a new 100 GB VHDX, partitions and formats it as a ReFS Dev Drive
Phase 6: Restore - Recreates the junction, copies the data back
Phase 7: Cleanup - Removes the backup folder after confirmation
RECOVERY ON FAILURE:
====================
The script writes a state file after each phase: C:\DevDriveResize-State.json
On a subsequent start it reads the state and resumes where it was interrupted.
If the script fails and another agent has to fix it:
1. Read the state file: C:\DevDriveResize-State.json
2. Check the backup: C:\DevDriveBackup\ (contains ALL data from the old Dev Drive)
3. The old VHDX may still exist: C:\DevDrive.vhdx
4. Junction target: C:\Development must point to volume D:\ (after rebuild)
5. This script INTENTIONALLY lives at C:\tools\resize-devdrive\ (NOT on the Dev Drive!)
CONFIGURATION (adjustable):
===========================
Settings come from parameters OR a nested JSON config file (reusable).
Precedence: command-line parameters > config file > auto-detected/defaults.
REQUIRED (no meaningful default):
$DriveLetter - Which drive to resize (config: drive.letter)
$NewSizeGB - Target size in GB (config: drive.targetSizeGB)
OPTIONAL (auto-detected from the drive, or defaulted; config: advanced.*):
$VhdxPath - Backing VHDX (auto-detected from the drive if empty)
$VolumeLabel - ReFS label (existing label preserved if empty)
$JunctionPath - Junction path (empty = no junction; steps skipped)
$BackupPath - Backup folder (default: %TEMP%\DevDriveBackup-<letter>)
$StatePath - Recovery state (default: state\ next to this script)
$LogDir - Log folder (default: logs\ next to this script)
$BackupLogPath - robocopy backup log (default: under $LogDir)
$RestoreLogPath - robocopy restore log (default: under $LogDir)
$NonInteractive - Auto-confirm all prompts (default off)
$ConfigPath - Explicit config file. If omitted, a resize-devdrive.config.json
next to this script is used when present.
See resize-devdrive.config.example.json.
.NOTES
Created: 2026-08-07
Context: logikal-main workspace on the Dev Drive, git fetch failed because D: was full
Tested: end-to-end via Test-ResizeDevDrive.ps1 and a real run on 2026-08-07
Windows Build: 10.0.26100.0 (Format-Volume -DevDrive verified)
.EXAMPLE
# Run as Administrator in a separate PowerShell window (NOT from VS Code!):
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
C:\tools\resize-devdrive\Resize-DevDrive.ps1
.EXAMPLE
# Reuse for a different Dev Drive via a config file:
Copy-Item resize-devdrive.config.example.json resize-devdrive.config.json
# edit resize-devdrive.config.json, then just run:
C:\tools\resize-devdrive\Resize-DevDrive.ps1
# or point at a specific config explicitly:
C:\tools\resize-devdrive\Resize-DevDrive.ps1 -ConfigPath D:\configs\my-devdrive.json
#>
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
# === REQUIRED =========================================================
# Which drive to resize and how big to make it. These have no meaningful
# defaults and must come from a parameter or the config file.
[char]$DriveLetter = [char]0,
[int]$NewSizeGB = 0,
# === OPTIONAL (auto-detected from the drive or defaulted) ==============
# Leave empty to derive automatically:
# VhdxPath -> read from the mounted drive (its backing VHDX)
# VolumeLabel -> the current label of the drive (preserved)
# JunctionPath-> empty means "no junction" (junction steps are skipped)
# BackupPath -> a temp folder
# StatePath -> a state\ folder next to this script
# LogDir -> a logs\ folder next to this script
[string]$VhdxPath = "",
[string]$JunctionPath = "",
[string]$VolumeLabel = "",
[string]$BackupPath = "",
[string]$StatePath = "",
[string]$LogDir = "",
[string]$BackupLogPath = "",
[string]$RestoreLogPath = "",
[switch]$NonInteractive,
# === CONFIG FILE =======================================================
[string]$ConfigPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
#region === CONFIG FILE ===
# Makes the script reusable: put your settings in a (nested) JSON file instead of
# editing the script or passing many parameters. Precedence (highest first):
# 1. Explicit command-line parameters
# 2. Values from the config file
# 3. Auto-detected / default values
# If -ConfigPath is not given, a "resize-devdrive.config.json" next to this
# script is used automatically when present. See resize-devdrive.config.example.json.
function Get-ConfigValue {
# Reads a dotted path (e.g. "drive.letter") from a config object. Returns
# $null if any segment is missing.
param($Root, [string]$DottedPath)
$node = $Root
foreach ($part in $DottedPath.Split('.')) {
if ($null -eq $node) { return $null }
$prop = $node.PSObject.Properties[$part]
if (-not $prop) { return $null }
$node = $prop.Value
}
return $node
}
if (-not $ConfigPath) {
$defaultConfig = Join-Path $PSScriptRoot "resize-devdrive.config.json"
if (Test-Path $defaultConfig) { $ConfigPath = $defaultConfig }
}
if ($ConfigPath) {
if (-not (Test-Path $ConfigPath)) {
throw "ERROR: Config file not found: $ConfigPath"
}
try {
$config = Get-Content -Raw -Path $ConfigPath | ConvertFrom-Json
} catch {
throw "ERROR: Could not parse config file '$ConfigPath': $($_.Exception.Message)"
}
# Map each parameter to its nested config path (with a flat fallback by name).
$map = [ordered]@{
'DriveLetter' = 'drive.letter'
'NewSizeGB' = 'drive.targetSizeGB'
'VhdxPath' = 'advanced.vhdxPath'
'JunctionPath' = 'advanced.junctionPath'
'VolumeLabel' = 'advanced.volumeLabel'
'BackupPath' = 'advanced.backupPath'
'StatePath' = 'advanced.statePath'
'LogDir' = 'advanced.logDir'
'BackupLogPath' = 'advanced.backupLogPath'
'RestoreLogPath' = 'advanced.restoreLogPath'
'NonInteractive' = 'advanced.nonInteractive'
}
foreach ($key in $map.Keys) {
if ($PSBoundParameters.ContainsKey($key)) { continue } # CLI wins
$value = Get-ConfigValue $config $map[$key]
if ($null -eq $value) { $value = Get-ConfigValue $config $key } # flat fallback
if ($null -eq $value) { continue }
switch ($key) {
'DriveLetter' { if ("$value") { Set-Variable -Name $key -Value ([char]([string]$value)[0]) } }
'NewSizeGB' { Set-Variable -Name $key -Value ([int]$value) }
'NonInteractive' { Set-Variable -Name $key -Value ([switch][bool]$value) }
default { Set-Variable -Name $key -Value ([string]$value) }
}
}
Write-Host " [Config] Loaded: $ConfigPath" -ForegroundColor DarkGray
}
#endregion
#region === RESOLVE / VALIDATE SETTINGS ===
# --- Required: drive letter + target size ---
if ($DriveLetter -eq [char]0) {
throw "ERROR: DriveLetter is required. Pass -DriveLetter <X> or set 'drive.letter' in the config file."
}
if ($NewSizeGB -le 0) {
throw "ERROR: NewSizeGB (target size in GB) is required. Pass -NewSizeGB <n> or set 'drive.targetSizeGB' in the config file."
}
if ($NewSizeGB -lt 50) {
throw "ERROR: NewSizeGB must be at least 50 GB for a Dev Drive."
}
# Resolve the mounted drive and enforce the assumptions required by the
# destructive recreate operation before any state or backup is created.
$volume = Get-Volume -DriveLetter $DriveLetter -ErrorAction SilentlyContinue
$part = Get-Partition -DriveLetter $DriveLetter -ErrorAction SilentlyContinue
$disk = if ($part) { $part | Get-Disk -ErrorAction SilentlyContinue } else { $null }
if (-not $volume) {
throw "ERROR: Drive ${DriveLetter}: is not mounted."
}
if ($volume.FileSystem -ne 'ReFS') {
throw "ERROR: Drive ${DriveLetter}: uses $($volume.FileSystem), not ReFS. Only Dev Drives are supported."
}
if (-not $disk -or $disk.BusType -ne 'File Backed Virtual' -or -not $disk.Location) {
throw "ERROR: Drive ${DriveLetter}: is not backed by a detectable VHDX (BusType: $($disk.BusType))."
}
$currentDiskSizeGB = [math]::Round($disk.Size / 1GB, 2)
$targetSizeBytes = [int64]$NewSizeGB * 1GB
if ($targetSizeBytes -eq [int64]$disk.Size) {
throw "ERROR: Target size $NewSizeGB GB equals the current disk size $currentDiskSizeGB GB. Nothing to resize."
}
if ($targetSizeBytes -lt [int64]$disk.Size) {
$usedBytes = [int64]($volume.Size - $volume.SizeRemaining)
$requiredTargetBytes = $usedBytes + [int64](5GB)
if ($targetSizeBytes -le $requiredTargetBytes) {
$usedGB = [math]::Round($usedBytes / 1GB, 2)
throw "ERROR: Cannot shrink to $NewSizeGB GB: $usedGB GB is used and at least 5 GB free space is required."
}
}
# --- Auto-detect the backing VHDX from the drive if not provided ---
if (-not $VhdxPath) {
if ($disk -and $disk.Location -and (Test-Path $disk.Location)) {
$VhdxPath = $disk.Location
Write-Host " [Auto] VHDX detected from ${DriveLetter}: -> $VhdxPath" -ForegroundColor DarkGray
} else {
throw "ERROR: Could not auto-detect the VHDX for drive ${DriveLetter}:. Is the drive mounted? Otherwise set 'advanced.vhdxPath' in the config."
}
}
if (-not (Test-Path -LiteralPath $VhdxPath)) {
throw "ERROR: VHDX not found: $VhdxPath"
}
$detectedVhdxPath = (Get-Item -LiteralPath $disk.Location).FullName
$configuredVhdxPath = (Get-Item -LiteralPath $VhdxPath).FullName
if (-not [string]::Equals($detectedVhdxPath, $configuredVhdxPath, [StringComparison]::OrdinalIgnoreCase)) {
throw "ERROR: Configured VHDX '$configuredVhdxPath' does not back drive ${DriveLetter}: ('$detectedVhdxPath')."
}
$VhdxPath = $configuredVhdxPath
# --- Preserve the existing volume label if not provided ---
if (-not $VolumeLabel) {
$VolumeLabel = if ($volume.FileSystemLabel) { $volume.FileSystemLabel } else { "DevDrive" }
}
# --- Auto-detect a junction pointing to the drive if not provided ---
# (empty means "no junction found" -> junction steps are simply skipped)
if (-not $JunctionPath) {
$target = "${DriveLetter}:\"
$found = Get-ChildItem C:\ -Directory -Force -ErrorAction SilentlyContinue |
Where-Object { $_.LinkType -eq 'Junction' -and ($_.Target -contains $target) } |
Select-Object -First 1
if ($found) {
$JunctionPath = $found.FullName
Write-Host " [Auto] Junction detected: $JunctionPath -> $target" -ForegroundColor DarkGray
}
}
# --- Sensible defaults for backup/state/logs (independent of the machine) ---
if (-not $BackupPath) { $BackupPath = Join-Path $env:TEMP "DevDriveBackup-$DriveLetter" }
if (-not $StatePath) { $StatePath = Join-Path $PSScriptRoot ("state\DevDriveResize-$DriveLetter.json") }
if (-not $LogDir) { $LogDir = Join-Path $PSScriptRoot "logs" }
if (-not $BackupLogPath) { $BackupLogPath = Join-Path $LogDir "Resize-$DriveLetter-Backup.log" }
if (-not $RestoreLogPath) { $RestoreLogPath = Join-Path $LogDir "Resize-$DriveLetter-Restore.log" }
# Ensure the state and log directories exist.
foreach ($d in @((Split-Path -Parent $StatePath), $LogDir)) {
if ($d -and -not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null }
}
#endregion
#region === HELPER FUNCTIONS ===
function Write-Phase {
param([string]$Phase, [string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host ""
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " [$timestamp] Phase: $Phase" -ForegroundColor Cyan
Write-Host " $Message" -ForegroundColor White
Write-Host "================================================================" -ForegroundColor Cyan
}
function Save-State {
param([string]$CompletedPhase, [hashtable]$ExtraData = @{})
$state = @{
CompletedPhase = $CompletedPhase
Timestamp = (Get-Date -Format "o")
Config = @{
VhdxPath = $VhdxPath
NewSizeGB = $NewSizeGB
DriveLetter = [string]$DriveLetter
JunctionPath = $JunctionPath
BackupPath = $BackupPath
}
Data = $ExtraData
}
$state | ConvertTo-Json -Depth 5 | Set-Content -Path $StatePath -Encoding UTF8
Write-Host " [State] Saved: phase '$CompletedPhase' completed" -ForegroundColor DarkGray
}
function Get-SavedState {
if (Test-Path $StatePath) {
return (Get-Content $StatePath -Raw | ConvertFrom-Json)
}
return $null
}
function Test-PhaseCompleted {
param([string]$Phase)
$state = Get-SavedState
if (-not $state) { return $false }
$phaseOrder = @("Validation", "Backup", "Dismount", "Destroy", "Create", "Restore", "Cleanup")
$savedIndex = $phaseOrder.IndexOf($state.CompletedPhase)
$checkIndex = $phaseOrder.IndexOf($Phase)
return ($savedIndex -ge $checkIndex)
}
function Confirm-Continue {
param([string]$Message)
if ($script:NonInteractive) {
Write-Host " [AUTO] $Message -> automatically confirmed (NonInteractive)" -ForegroundColor DarkGray
return
}
Write-Host ""
Write-Host " CONFIRMATION REQUIRED:" -ForegroundColor Yellow
Write-Host " $Message" -ForegroundColor Yellow
$response = (Read-Host " Continue? (y/n)").Trim()
if ($response -notin @('y', 'yes')) {
Write-Host " Aborted by user." -ForegroundColor Red
exit 1
}
}
function Confirm-YesNo {
# Soft yes/no prompt: returns $true/$false, does NOT abort.
param([string]$Message)
if ($script:NonInteractive) {
Write-Host " [AUTO] $Message -> Yes (NonInteractive)" -ForegroundColor DarkGray
return $true
}
Write-Host ""
Write-Host " $Message" -ForegroundColor Yellow
$response = (Read-Host " (y/n)").Trim()
return ($response -in @('y', 'yes'))
}
function Remove-DirectoryRobust {
# Deletes a directory tree in a long-path-safe way (>260 chars).
# Remove-Item -Recurse fails on long paths; robocopy does not.
param([string]$Path)
if (-not (Test-Path $Path)) { return }
$empty = Join-Path $env:TEMP ("empty_" + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $empty -Force | Out-Null
try {
# /MIR mirrors the empty folder -> empties the target tree (long-path capable)
& robocopy $empty $Path /MIR /NFL /NDL /NJH /NJS /NP /R:1 /W:1 | Out-Null
Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop
} finally {
Remove-Item -LiteralPath $empty -Recurse -Force -ErrorAction SilentlyContinue
}
}
#endregion
#region === MAIN EXECUTION ===
Write-Host ""
Write-Host "================================================================" -ForegroundColor Magenta
Write-Host " DEV DRIVE RESIZE -> ${NewSizeGB} GB" -ForegroundColor Magenta
Write-Host " VHDX: $VhdxPath" -ForegroundColor Magenta
Write-Host "================================================================" -ForegroundColor Magenta
# Check for resume
$savedState = Get-SavedState
if ($savedState) {
Write-Host ""
Write-Host " PREVIOUS RUN FOUND:" -ForegroundColor Yellow
Write-Host " Last completed phase: $($savedState.CompletedPhase)" -ForegroundColor Yellow
Write-Host " Timestamp: $($savedState.Timestamp)" -ForegroundColor Yellow
Confirm-Continue "Resume from the last state? (n = start from scratch)"
}
#endregion
#region === PHASE 1: VALIDATION ===
if (-not (Test-PhaseCompleted "Validation")) {
Write-Phase "1/7 - Validation" "Checking prerequisites..."
# 1a. Admin check (redundant due to #Requires, but explicit)
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
throw "ERROR: Script must be run as Administrator!"
}
Write-Host " [OK] Administrator rights" -ForegroundColor Green
# 1b. Format-Volume -DevDrive parameter exists
if (-not (Get-Command Format-Volume).Parameters.ContainsKey('DevDrive')) {
throw "ERROR: Format-Volume has no -DevDrive parameter. Windows build too old (needs 23H2+)!"
}
Write-Host " [OK] Format-Volume -DevDrive available" -ForegroundColor Green
# 1c. VHDX exists
if (-not (Test-Path $VhdxPath)) {
throw "ERROR: VHDX not found: $VhdxPath"
}
$vhdxSize = [math]::Round((Get-Item $VhdxPath).Length / 1GB, 2)
Write-Host " [OK] VHDX found: $VhdxPath ($vhdxSize GB)" -ForegroundColor Green
# 1d. Drive is mounted
$volume = Get-Volume -DriveLetter $DriveLetter -ErrorAction SilentlyContinue
if (-not $volume) {
throw "ERROR: Drive ${DriveLetter}: not found!"
}
Write-Host " [OK] Drive ${DriveLetter}: mounted (label: $($volume.FileSystemLabel), FS: $($volume.FileSystem))" -ForegroundColor Green
# 1e. Junction (optional)
if ($JunctionPath) {
$junctionItem = Get-Item $JunctionPath -ErrorAction SilentlyContinue
if ($junctionItem -and $junctionItem.LinkType -eq "Junction") {
Write-Host " [OK] Junction: $JunctionPath -> $($junctionItem.Target)" -ForegroundColor Green
} elseif ($junctionItem) {
Write-Host " [WARN] $JunctionPath exists but is not a junction (LinkType: $($junctionItem.LinkType)). Handling it anyway." -ForegroundColor Yellow
} else {
Write-Host " [INFO] Junction $JunctionPath does not exist yet; it will be created in the restore phase." -ForegroundColor DarkGray
}
} else {
Write-Host " [INFO] No junction configured; junction steps are skipped." -ForegroundColor DarkGray
}
# 1f. Check free space on C:
$cVolume = Get-Volume -DriveLetter C
$cFreeGB = [math]::Round($cVolume.SizeRemaining / 1GB, 2)
$requiredGB = $vhdxSize + 10 # backup (max old size) + some buffer
if ($cFreeGB -lt $requiredGB) {
throw "ERROR: Not enough space on C: (free: $cFreeGB GB, required: ~$requiredGB GB for backup)"
}
Write-Host " [OK] C: has $cFreeGB GB free (requires ~$requiredGB GB)" -ForegroundColor Green
# 1g. Check whether processes are using D: (warning)
try {
$processes = Get-Process | Where-Object {
try { $_.Modules | Where-Object { $_.FileName -like "${DriveLetter}:\*" } } catch {}
}
if ($processes) {
Write-Host " [WARN] The following processes are using ${DriveLetter}:\:" -ForegroundColor Yellow
$processes | Select-Object -Unique Name, Id | ForEach-Object {
Write-Host " - $($_.Name) (PID: $($_.Id))" -ForegroundColor Yellow
}
}
} catch {
Write-Host " [INFO] Process check skipped (not critical)" -ForegroundColor DarkGray
}
Confirm-Continue "All programs accessing ${DriveLetter}:\ or $JunctionPath MUST be closed. Close VS Code!"
Save-State -CompletedPhase "Validation"
}
#endregion
#region === PHASE 2: BACKUP ===
if (-not (Test-PhaseCompleted "Backup")) {
Write-Phase "2/7 - Backup" "Copying data from ${DriveLetter}:\ to $BackupPath ..."
# Decide what to do if a backup already exists at this path.
# NOTE: wrap the whole 'if' in @(...) - an empty array returned from an
# if-branch unrolls to zero output, making the expression yield $null, and
# $null.Count throws under Set-StrictMode -Version Latest.
$existingFiles = @(
if (Test-Path $BackupPath) {
Get-ChildItem $BackupPath -Recurse -File -ErrorAction SilentlyContinue
}
)
$reuse = $false
if ($existingFiles.Count -gt 0) {
$bkSize = [math]::Round((($existingFiles | Measure-Object -Property Length -Sum).Sum) / 1GB, 2)
$bkDate = (Get-Item $BackupPath).LastWriteTime
Write-Host " [WARN] A backup already exists here:" -ForegroundColor Yellow
Write-Host " $BackupPath" -ForegroundColor Yellow
Write-Host " $($existingFiles.Count) files, $bkSize GB, last modified $bkDate" -ForegroundColor Yellow
if (Confirm-YesNo "Delete it and create a FRESH backup? (recommended - an old backup may be stale)") {
Write-Host " Deleting old backup..." -ForegroundColor White
Remove-DirectoryRobust -Path $BackupPath
} else {
Confirm-Continue "Reuse the EXISTING backup as-is? It MUST match the current ${DriveLetter}:\ contents, otherwise you risk data loss."
$reuse = $true
}
} elseif (Test-Path $BackupPath) {
# Empty leftover folder - just remove it and start clean.
Remove-Item $BackupPath -Recurse -Force -ErrorAction SilentlyContinue
}
if (-not $reuse) {
New-Item -ItemType Directory -Path $BackupPath -Force | Out-Null
# Robocopy: /MIR = mirror, /SL = symlinks as links, /XD = exclude system dirs
Write-Host " Starting robocopy..." -ForegroundColor White
$robocopyArgs = @(
"${DriveLetter}:\"
$BackupPath
"/MIR"
"/COPY:DATSOU"
"/DCOPY:DAT"
"/SL"
"/R:3"
"/W:5"
"/XD"
"`$RECYCLE.BIN"
"System Volume Information"
"/NP"
"/LOG:$BackupLogPath"
"/TEE"
)
Write-Host " robocopy $($robocopyArgs -join ' ')" -ForegroundColor DarkGray
& robocopy @robocopyArgs
# Robocopy exit codes: 0-7 = success/info, 8+ = error
$robocopyExit = $LASTEXITCODE
if ($robocopyExit -ge 8) {
throw "ERROR: Robocopy failed with exit code $robocopyExit. See log: $BackupLogPath"
}
Write-Host " [OK] Backup completed (robocopy exit: $robocopyExit)" -ForegroundColor Green
Write-Host " [OK] Log: $BackupLogPath" -ForegroundColor Green
}
# Verification
$sourceCount = @(Get-ChildItem "${DriveLetter}:\" -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notlike "*`$RECYCLE.BIN*" -and $_.FullName -notlike "*System Volume Information*" }).Count
$backupCount = @(Get-ChildItem $BackupPath -Recurse -File -ErrorAction SilentlyContinue).Count
Write-Host " [INFO] Files on source: $sourceCount, in backup: $backupCount" -ForegroundColor White
if ($backupCount -lt ($sourceCount * 0.95)) {
Write-Host " [WARN] Backup has significantly fewer files than the source!" -ForegroundColor Yellow
Write-Host " Source: $sourceCount file(s), backup: $backupCount file(s)" -ForegroundColor Yellow
if ($script:NonInteractive) {
# Do NOT auto-continue past an incomplete backup: the next phases dismount
# and DELETE the original VHDX, so accepting a short backup would risk
# permanent data loss. Abort here, before anything destructive happens.
throw "ERROR: Backup appears incomplete ($backupCount of $sourceCount files) and -NonInteractive is set. Aborting BEFORE the destructive phase to protect your data. Check the log: $BackupLogPath"
}
Confirm-Continue "Continue anyway? (NOT recommended - the original VHDX will be deleted next)"
}
Save-State -CompletedPhase "Backup" -ExtraData @{ BackupFileCount = $backupCount; SourceFileCount = $sourceCount }
}
#endregion
#region === PHASE 3: DISMOUNT ===
if (-not (Test-PhaseCompleted "Dismount")) {
Write-Phase "3/7 - Dismount" "Removing junction and dismounting VHDX..."
# 3a. Remove junction (optional)
if ($JunctionPath -and (Test-Path $JunctionPath)) {
$item = Get-Item $JunctionPath
if ($item.LinkType -eq "Junction") {
Write-Host " Removing junction: $JunctionPath" -ForegroundColor White
cmd /c rmdir "$JunctionPath" 2>&1 | Out-Null
if (Test-Path $JunctionPath) {
throw "ERROR: Could not remove junction: $JunctionPath"
}
Write-Host " [OK] Junction removed" -ForegroundColor Green
} else {
Write-Host " [WARN] $JunctionPath is not a junction, skipping" -ForegroundColor Yellow
}
} elseif ($JunctionPath) {
Write-Host " [INFO] Junction no longer exists, skipping" -ForegroundColor DarkGray
} else {
Write-Host " [INFO] No junction configured, skipping" -ForegroundColor DarkGray
}
# 3b. Dismount VHD
Write-Host " Dismounting VHDX: $VhdxPath" -ForegroundColor White
Dismount-DiskImage -ImagePath $VhdxPath -ErrorAction Stop
Write-Host " [OK] VHDX dismounted" -ForegroundColor Green
# Verification
if (Get-Volume -DriveLetter $DriveLetter -ErrorAction SilentlyContinue) {
throw "ERROR: Drive ${DriveLetter}: is still mounted after dismount!"
}
Write-Host " [OK] Drive ${DriveLetter}: is no longer visible" -ForegroundColor Green
Save-State -CompletedPhase "Dismount"
}
#endregion
#region === PHASE 4: DESTROY ===
if (-not (Test-PhaseCompleted "Destroy")) {
Write-Phase "4/7 - Destroy" "Deleting old VHDX..."
if (Test-Path $VhdxPath) {
$oldSize = [math]::Round((Get-Item $VhdxPath).Length / 1GB, 2)
Write-Host " Deleting: $VhdxPath ($oldSize GB)" -ForegroundColor White
Confirm-Continue "WARNING: The old VHDX will be deleted! Backup is in $BackupPath. Sure?"
Remove-Item -Path $VhdxPath -Force
Write-Host " [OK] Old VHDX deleted" -ForegroundColor Green
} else {
Write-Host " [INFO] VHDX no longer exists, skipping" -ForegroundColor DarkGray
}
Save-State -CompletedPhase "Destroy"
}
#endregion
#region === PHASE 5: CREATE ===
if (-not (Test-PhaseCompleted "Create")) {
Write-Phase "5/7 - Create" "Creating new ${NewSizeGB} GB VHDX as a Dev Drive..."
# 5a. Create new VHDX (dynamically expanding) via diskpart (no Hyper-V needed)
Write-Host " Creating VHDX: $VhdxPath (${NewSizeGB} GB, dynamic)" -ForegroundColor White
$maxMB = [int]$NewSizeGB * 1024
$dpScript = "create vdisk file=`"$VhdxPath`" maximum=$maxMB type=expandable"
$dpFile = [System.IO.Path]::GetTempFileName()
Set-Content -Path $dpFile -Value $dpScript -Encoding ASCII
$dpOut = & diskpart /s $dpFile 2>&1
Remove-Item $dpFile -Force -ErrorAction SilentlyContinue
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $VhdxPath)) {
throw "ERROR: diskpart could not create the VHDX. Output: $dpOut"
}
Write-Host " [OK] VHDX created" -ForegroundColor Green
# 5b. Mount
Write-Host " Mounting VHDX..." -ForegroundColor White
$mountResult = Mount-DiskImage -ImagePath $VhdxPath -PassThru -ErrorAction Stop
$diskNumber = ($mountResult | Get-DiskImage | Get-Disk).Number
Write-Host " [OK] Mounted as disk $diskNumber" -ForegroundColor Green
# 5c. Initialize (GPT)
Write-Host " Initializing disk (GPT)..." -ForegroundColor White
Initialize-Disk -Number $diskNumber -PartitionStyle GPT -ErrorAction Stop
Write-Host " [OK] Disk initialized" -ForegroundColor Green
# 5d. Create partition
Write-Host " Creating partition..." -ForegroundColor White
$partition = New-Partition -DiskNumber $diskNumber -UseMaximumSize -DriveLetter $DriveLetter -ErrorAction Stop
Write-Host " [OK] Partition created (${DriveLetter}:)" -ForegroundColor Green
# 5e. Format as ReFS with the Dev Drive flag
Write-Host " Formatting as ReFS (Dev Drive)..." -ForegroundColor White
Format-Volume -DriveLetter $DriveLetter -FileSystem ReFS -NewFileSystemLabel $VolumeLabel -DevDrive -Confirm:$false -ErrorAction Stop | Out-Null
Write-Host " [OK] Formatted as ReFS Dev Drive" -ForegroundColor Green
# Verification
$newVolume = Get-Volume -DriveLetter $DriveLetter
$newSizeActual = [math]::Round($newVolume.Size / 1GB, 2)
Write-Host " [OK] New volume: ${DriveLetter}: $newSizeActual GB ReFS ($($newVolume.FileSystemLabel))" -ForegroundColor Green
Save-State -CompletedPhase "Create" -ExtraData @{ DiskNumber = $diskNumber; ActualSizeGB = $newSizeActual }
}
#endregion
#region === PHASE 6: RESTORE ===
if (-not (Test-PhaseCompleted "Restore")) {
Write-Phase "6/7 - Restore" "Restoring junction and data..."
# 6a. Recreate junction (optional)
if ($JunctionPath) {
Write-Host " Creating junction: $JunctionPath -> ${DriveLetter}:\" -ForegroundColor White
if (Test-Path $JunctionPath) {
$item = Get-Item $JunctionPath
if ($item.LinkType -ne "Junction") {
throw "ERROR: $JunctionPath exists but is not a junction! Check manually."
}
Write-Host " [INFO] Junction already exists" -ForegroundColor DarkGray
} else {
cmd /c mklink /J "$JunctionPath" "${DriveLetter}:\" 2>&1 | Out-Null
if (-not (Test-Path $JunctionPath)) {
throw "ERROR: Could not create junction: $JunctionPath"
}
}
Write-Host " [OK] Junction created" -ForegroundColor Green
} else {
Write-Host " [INFO] No junction configured, skipping" -ForegroundColor DarkGray
}
# 6b. Copy data back
Write-Host " Copying data back from $BackupPath to ${DriveLetter}:\..." -ForegroundColor White
$robocopyArgs = @(
$BackupPath
"${DriveLetter}:\"
"/MIR"
"/COPY:DATSOU"
"/DCOPY:DAT"
"/SL"
"/R:3"
"/W:5"
"/NP"
"/LOG:$RestoreLogPath"
"/TEE"
)
Write-Host " robocopy $($robocopyArgs -join ' ')" -ForegroundColor DarkGray
& robocopy @robocopyArgs
$robocopyExit = $LASTEXITCODE
if ($robocopyExit -ge 8) {
throw "ERROR: Robocopy restore failed with exit code $robocopyExit. See log: $RestoreLogPath. Backup is still in $BackupPath!"
}
Write-Host " [OK] Data restored (robocopy exit: $robocopyExit)" -ForegroundColor Green
# Verification
$restoredCount = @(Get-ChildItem "${DriveLetter}:\" -Recurse -File -ErrorAction SilentlyContinue).Count
Write-Host " [INFO] Files on the new drive: $restoredCount" -ForegroundColor White
$vol = Get-Volume -DriveLetter $DriveLetter
$freeGB = [math]::Round($vol.SizeRemaining / 1GB, 2)
Write-Host " [OK] Free space on the new drive: $freeGB GB" -ForegroundColor Green
Save-State -CompletedPhase "Restore" -ExtraData @{ RestoredFileCount = $restoredCount; FreeGB = $freeGB }
}
#endregion
#region === PHASE 7: CLEANUP ===
if (-not (Test-PhaseCompleted "Cleanup")) {
Write-Phase "7/7 - Cleanup" "Cleaning up..."
Write-Host ""
Write-Host " RESULT:" -ForegroundColor Green
$vol = Get-Volume -DriveLetter $DriveLetter
Write-Host " - Drive: ${DriveLetter}: ($($vol.FileSystemLabel), $($vol.FileSystem))" -ForegroundColor White
Write-Host " - Size: $([math]::Round($vol.Size/1GB,2)) GB" -ForegroundColor White
Write-Host " - Free: $([math]::Round($vol.SizeRemaining/1GB,2)) GB" -ForegroundColor White
if ($JunctionPath) {
Write-Host " - Junction: $JunctionPath -> ${DriveLetter}:\" -ForegroundColor White
}
Write-Host ""
if (Test-Path $BackupPath) {
$backupSize = [math]::Round((Get-ChildItem $BackupPath -Recurse -File -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum).Sum / 1GB, 2)
if (Confirm-YesNo "Delete the backup folder? ($BackupPath, $backupSize GB). Verify everything works first!") {
Write-Host " Deleting backup..." -ForegroundColor White
Remove-DirectoryRobust -Path $BackupPath
Write-Host " [OK] Backup deleted" -ForegroundColor Green
} else {
Write-Host " [INFO] Keeping backup: $BackupPath" -ForegroundColor Yellow
Write-Host " Delete manually later: Remove-Item '$BackupPath' -Recurse -Force" -ForegroundColor DarkGray
}
}
Write-Host " [INFO] Logs are kept:" -ForegroundColor DarkGray
Write-Host " - $BackupLogPath" -ForegroundColor DarkGray
Write-Host " - $RestoreLogPath" -ForegroundColor DarkGray
Save-State -CompletedPhase "Cleanup"
# Remove the state file after successful completion
Remove-Item -Path $StatePath -Force -ErrorAction SilentlyContinue
}
#endregion
#region === DONE ===
Write-Host ""
Write-Host "================================================================" -ForegroundColor Green
Write-Host " DEV DRIVE RESIZE COMPLETED!" -ForegroundColor Green
Write-Host " New Dev Drive: ${NewSizeGB} GB ReFS" -ForegroundColor Green
Write-Host " You can reopen VS Code now." -ForegroundColor Green
Write-Host "================================================================" -ForegroundColor Green
#endregion