aid-installer 1.1.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/VERSION +1 -1
- package/bin/aid +158 -20
- package/bin/aid.ps1 +189 -41
- package/dashboard/home.html +41 -4
- package/dashboard/reader/derivation.py +228 -5
- package/dashboard/reader/models.py +22 -0
- package/dashboard/reader/parsers.py +127 -0
- package/dashboard/reader/reader.py +6 -1
- package/dashboard/server/reader.mjs +313 -0
- package/dashboard/server/server.py +12 -0
- package/lib/AidInstallCore.psm1 +439 -63
- package/lib/aid-install-core.sh +384 -36
- package/package.json +9 -6
- package/scripts/vendor.js +0 -98
package/lib/AidInstallCore.psm1
CHANGED
|
@@ -47,6 +47,10 @@
|
|
|
47
47
|
|
|
48
48
|
Set-StrictMode -Version Latest
|
|
49
49
|
|
|
50
|
+
# Enable TLS 1.2 for HTTPS. Windows PowerShell 5.1 (.NET Framework) can default to
|
|
51
|
+
# SSL3/TLS1.0, which GitHub/npm/pypi reject -> downloads fail. Harmless on PS7/.NET Core.
|
|
52
|
+
try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {}
|
|
53
|
+
|
|
50
54
|
# Guard against being imported more than once.
|
|
51
55
|
# Use Get-Variable with -ErrorAction SilentlyContinue to avoid strict-mode failure on first load.
|
|
52
56
|
$_aidLoadedVar = Get-Variable -Name '_AID_INSTALL_CORE_LOADED' -Scope Global -ErrorAction SilentlyContinue
|
|
@@ -58,10 +62,18 @@ $script:_CopyCountCopied = 0
|
|
|
58
62
|
$script:_CopyCountUpToDate = 0
|
|
59
63
|
$script:_CopyCountUpdated = 0
|
|
60
64
|
$script:_CopyCountSkipped = 0
|
|
65
|
+
$script:_CopyCountFailed = 0
|
|
61
66
|
|
|
62
67
|
# Module-level prune counter. Reset by Invoke-PruneToolDirs before each prune pass.
|
|
63
68
|
$script:_PruneRemoved = 0
|
|
64
69
|
|
|
70
|
+
# Module-level project-provisioning state (work-007). Reset by Install-AidTool.
|
|
71
|
+
$script:_SeededSettings = $false
|
|
72
|
+
$script:_GitignoreAction = 'unchanged'
|
|
73
|
+
# Settings format stamp. MUST equal bin/aid AID_SUPPORTED_FORMAT / bin/aid.ps1
|
|
74
|
+
# AidSupportedFormat. Used to stamp a seeded settings.yml so the format gate is quiet.
|
|
75
|
+
$script:_AidSupportedFormat = 1
|
|
76
|
+
|
|
65
77
|
# ---------------------------------------------------------------------------
|
|
66
78
|
# Constants
|
|
67
79
|
# ---------------------------------------------------------------------------
|
|
@@ -81,6 +93,21 @@ function script:Get-RootAgentFile {
|
|
|
81
93
|
}
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
# Get-RootDir <tool> - return the tool's install-tree root dir name (relative to
|
|
97
|
+
# the project root). The AID-own subtree lives at <root>/aid/ (e.g. .claude/aid/).
|
|
98
|
+
# Mirrors the per-tool dispatch in Install-AidTool and the bash _root_dir helper.
|
|
99
|
+
function script:Get-RootDir {
|
|
100
|
+
param([string]$Tool)
|
|
101
|
+
switch ($Tool) {
|
|
102
|
+
'claude-code' { return '.claude' }
|
|
103
|
+
'codex' { return '.codex' }
|
|
104
|
+
'cursor' { return '.cursor' }
|
|
105
|
+
'copilot-cli' { return '.github' }
|
|
106
|
+
'antigravity' { return '.agent' }
|
|
107
|
+
default { return '' }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
# ---------------------------------------------------------------------------
|
|
85
112
|
# Utility
|
|
86
113
|
# ---------------------------------------------------------------------------
|
|
@@ -357,7 +384,13 @@ function Copy-AidFile {
|
|
|
357
384
|
}
|
|
358
385
|
|
|
359
386
|
if (-not (Test-Path $Dst -PathType Leaf)) {
|
|
360
|
-
|
|
387
|
+
try {
|
|
388
|
+
Copy-Item -LiteralPath $Src -Destination $Dst -Force -ErrorAction Stop
|
|
389
|
+
} catch {
|
|
390
|
+
[Console]::Error.WriteLine("ERROR: AidInstallCore: copy failed: $Dst -- $_")
|
|
391
|
+
$script:_CopyCountFailed++
|
|
392
|
+
return
|
|
393
|
+
}
|
|
361
394
|
$script:_CopyCountCopied++
|
|
362
395
|
if ($AidVerbose) { Write-Host "Copied: $Dst" }
|
|
363
396
|
return
|
|
@@ -373,15 +406,22 @@ function Copy-AidFile {
|
|
|
373
406
|
return
|
|
374
407
|
}
|
|
375
408
|
|
|
376
|
-
# File exists and differs.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
409
|
+
# File exists and differs -> always overwrite (work-007).
|
|
410
|
+
# AID-owned files track the bundle, which is the source of truth, so an
|
|
411
|
+
# add/update must bring them current. $Force is retained in the signature for
|
|
412
|
+
# back-compat and bash<->PS parity but no longer gates this overwrite:
|
|
413
|
+
# skip-on-diff silently left stale files behind on in-place upgrades. User-owned
|
|
414
|
+
# root agent files (CLAUDE.md/AGENTS.md) never reach Copy-AidFile -- they are
|
|
415
|
+
# handled by Copy-RootAgentFile, which keeps its own force gating.
|
|
416
|
+
try {
|
|
417
|
+
Copy-Item -LiteralPath $Src -Destination $Dst -Force -ErrorAction Stop
|
|
418
|
+
} catch {
|
|
419
|
+
[Console]::Error.WriteLine("ERROR: AidInstallCore: copy failed: $Dst -- $_")
|
|
420
|
+
$script:_CopyCountFailed++
|
|
421
|
+
return
|
|
384
422
|
}
|
|
423
|
+
$script:_CopyCountUpdated++
|
|
424
|
+
if ($AidVerbose) { Write-Host "Updated: $Dst" }
|
|
385
425
|
}
|
|
386
426
|
|
|
387
427
|
# Copy-AidDir <srcDir> <dstDir> [force] [aidVerbose]
|
|
@@ -434,8 +474,13 @@ function script:Test-AidHeadingStem {
|
|
|
434
474
|
$stem = $Line.Substring(3) # strip "## "
|
|
435
475
|
# Strip trailing parenthetical: " (anything)"
|
|
436
476
|
$stem = $stem -replace ' \([^)]*\)$', ''
|
|
477
|
+
# Stems must cover EVERY "## " heading in the shipped AID:BEGIN/END region
|
|
478
|
+
# (Tracking discipline, Knowledge Base, Workflow, Review output format,
|
|
479
|
+
# Permissions). A missing stem duplicates that section on C2 migration
|
|
480
|
+
# (work-007: Workflow was omitted). Parity with bash is_aid_heading.
|
|
437
481
|
switch ($stem) {
|
|
438
482
|
'Knowledge Base' { return $true }
|
|
483
|
+
'Workflow' { return $true }
|
|
439
484
|
'Review output format' { return $true }
|
|
440
485
|
'Permissions' { return $true }
|
|
441
486
|
'Tracking discipline' { return $true }
|
|
@@ -678,7 +723,7 @@ function Read-ManifestToolPaths {
|
|
|
678
723
|
try {
|
|
679
724
|
$data = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
|
|
680
725
|
$toolData = if ($data.tools -and ($data.tools.PSObject.Properties.Name -contains $Tool)) { $data.tools.$Tool } else { $null }
|
|
681
|
-
if ($toolData -and $toolData.paths) {
|
|
726
|
+
if ($toolData -and $toolData.PSObject.Properties['paths'] -and $toolData.paths) {
|
|
682
727
|
return @($toolData.paths)
|
|
683
728
|
}
|
|
684
729
|
} catch {}
|
|
@@ -693,7 +738,7 @@ function Read-ManifestToolVersion {
|
|
|
693
738
|
try {
|
|
694
739
|
$data = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
|
|
695
740
|
$toolData = if ($data.tools -and ($data.tools.PSObject.Properties.Name -contains $Tool)) { $data.tools.$Tool } else { $null }
|
|
696
|
-
if ($toolData -and $toolData.version) { return $toolData.version }
|
|
741
|
+
if ($toolData -and $toolData.PSObject.Properties['version'] -and $toolData.version) { return [string]$toolData.version }
|
|
697
742
|
} catch {}
|
|
698
743
|
return ''
|
|
699
744
|
}
|
|
@@ -706,9 +751,15 @@ function Read-ManifestRootAgent {
|
|
|
706
751
|
try {
|
|
707
752
|
$data = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
|
|
708
753
|
$toolData = if ($data.tools -and ($data.tools.PSObject.Properties.Name -contains $Tool)) { $data.tools.$Tool } else { $null }
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
754
|
+
# Guard root_agent_files: absent on old-format manifests; PSObject.Properties['key'] is
|
|
755
|
+
# safe under Set-StrictMode -Version Latest unlike direct property access.
|
|
756
|
+
$raf = if ($toolData -and $toolData.PSObject.Properties['root_agent_files']) { $toolData.root_agent_files } else { $null }
|
|
757
|
+
if ($raf) {
|
|
758
|
+
foreach ($entry in $raf) {
|
|
759
|
+
if ($entry.PSObject.Properties['path'] -and $entry.path -eq $FileName) {
|
|
760
|
+
$sha = if ($entry.PSObject.Properties['sha256']) { $entry.sha256 } else { '' }
|
|
761
|
+
return $sha
|
|
762
|
+
}
|
|
712
763
|
}
|
|
713
764
|
}
|
|
714
765
|
} catch {}
|
|
@@ -723,9 +774,10 @@ function Read-ManifestRootAgentStatus {
|
|
|
723
774
|
try {
|
|
724
775
|
$data = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
|
|
725
776
|
$toolData = if ($data.tools -and ($data.tools.PSObject.Properties.Name -contains $Tool)) { $data.tools.$Tool } else { $null }
|
|
726
|
-
if ($toolData -and $toolData.root_agent_files) {
|
|
727
|
-
|
|
728
|
-
|
|
777
|
+
$raf = if ($toolData -and $toolData.PSObject.Properties['root_agent_files']) { $toolData.root_agent_files } else { $null }
|
|
778
|
+
if ($raf) {
|
|
779
|
+
foreach ($entry in $raf) {
|
|
780
|
+
if ($entry.PSObject.Properties['path'] -and $entry.path -eq $FileName) {
|
|
729
781
|
if ($entry.PSObject.Properties['status']) { return $entry.status }
|
|
730
782
|
return 'owned'
|
|
731
783
|
}
|
|
@@ -880,8 +932,11 @@ function Write-AidManifest {
|
|
|
880
932
|
}
|
|
881
933
|
|
|
882
934
|
# Top-level installed_at: preserve existing.
|
|
935
|
+
# Guard via PSObject.Properties['key'] -- Set-StrictMode -Version Latest (active in this module)
|
|
936
|
+
# throws PropertyNotFoundException on direct access of absent properties. Old-format manifests
|
|
937
|
+
# (schema:1) have no root-level installed_at; new-format ones (manifest_version:1) do.
|
|
883
938
|
$topInstalledAt = $now
|
|
884
|
-
if ($existingData -and $existingData.installed_at) {
|
|
939
|
+
if ($existingData -and $existingData.PSObject.Properties['installed_at'] -and $existingData.installed_at) {
|
|
885
940
|
$topInstalledAt = $existingData.installed_at
|
|
886
941
|
}
|
|
887
942
|
|
|
@@ -895,17 +950,22 @@ function Write-AidManifest {
|
|
|
895
950
|
$tid = $_.Name
|
|
896
951
|
if ($tid -ne $Tool) {
|
|
897
952
|
$t = $_.Value
|
|
898
|
-
|
|
953
|
+
# Guard per-tool properties: old manifests lack root_agent_files; use
|
|
954
|
+
# PSObject.Properties['key'] check before direct access (StrictMode-safe).
|
|
955
|
+
$tP = if ($t.PSObject.Properties['paths'] -and $t.paths) { [System.Collections.Generic.List[string]]($t.paths) } else { [System.Collections.Generic.List[string]]::new() }
|
|
899
956
|
$tR = [System.Collections.Generic.List[hashtable]]::new()
|
|
900
|
-
if ($t.root_agent_files) {
|
|
901
|
-
|
|
957
|
+
$tRaf = if ($t.PSObject.Properties['root_agent_files']) { $t.root_agent_files } else { $null }
|
|
958
|
+
if ($tRaf) {
|
|
959
|
+
foreach ($e in $tRaf) {
|
|
960
|
+
$ePath = if ($e.PSObject.Properties['path']) { $e.path } else { '' }
|
|
961
|
+
$eSha256 = if ($e.PSObject.Properties['sha256']) { $e.sha256 } else { '' }
|
|
902
962
|
$st = if ($e.PSObject.Properties['status']) { $e.status } else { 'owned' }
|
|
903
|
-
$tR.Add(@{ path = $
|
|
963
|
+
if ($ePath) { $tR.Add(@{ path = $ePath; sha256 = $eSha256; status = $st }) }
|
|
904
964
|
}
|
|
905
965
|
}
|
|
906
966
|
$toolsMap[$tid] = @{
|
|
907
|
-
Version = if ($t.version) { $t.version } else { '' }
|
|
908
|
-
InstalledAt = if ($t.installed_at) { $t.installed_at } else { $now }
|
|
967
|
+
Version = if ($t.PSObject.Properties['version'] -and $t.version) { [string]$t.version } else { '' }
|
|
968
|
+
InstalledAt = if ($t.PSObject.Properties['installed_at'] -and $t.installed_at) { $t.installed_at } else { $now }
|
|
909
969
|
Paths = $tP
|
|
910
970
|
RootAgentFiles = $tR
|
|
911
971
|
}
|
|
@@ -921,14 +981,14 @@ function Write-AidManifest {
|
|
|
921
981
|
|
|
922
982
|
# tool installed_at: preserve existing.
|
|
923
983
|
$toolInstalledAt = $now
|
|
924
|
-
if ($existingTool -and $existingTool.installed_at) {
|
|
984
|
+
if ($existingTool -and $existingTool.PSObject.Properties['installed_at'] -and $existingTool.installed_at) {
|
|
925
985
|
$toolInstalledAt = $existingTool.installed_at
|
|
926
986
|
}
|
|
927
987
|
|
|
928
988
|
# De-duplicate paths (union, preserving order: existing first, then new).
|
|
929
989
|
$seenPaths = [System.Collections.Generic.HashSet[string]]::new()
|
|
930
990
|
$mergedPaths = [System.Collections.Generic.List[string]]::new()
|
|
931
|
-
if ($existingTool -and $existingTool.paths) {
|
|
991
|
+
if ($existingTool -and $existingTool.PSObject.Properties['paths'] -and $existingTool.paths) {
|
|
932
992
|
foreach ($p in $existingTool.paths) {
|
|
933
993
|
if ($seenPaths.Add($p)) { $mergedPaths.Add($p) }
|
|
934
994
|
}
|
|
@@ -939,10 +999,13 @@ function Write-AidManifest {
|
|
|
939
999
|
|
|
940
1000
|
# Merge root_agent_files: update or add per path.
|
|
941
1001
|
$rafMap = [System.Collections.Specialized.OrderedDictionary]::new()
|
|
942
|
-
if ($existingTool -and $existingTool.root_agent_files) {
|
|
943
|
-
|
|
1002
|
+
$existingRaf = if ($existingTool -and $existingTool.PSObject.Properties['root_agent_files']) { $existingTool.root_agent_files } else { $null }
|
|
1003
|
+
if ($existingRaf) {
|
|
1004
|
+
foreach ($e in $existingRaf) {
|
|
1005
|
+
$ePath = if ($e.PSObject.Properties['path']) { $e.path } else { '' }
|
|
1006
|
+
$eSha256 = if ($e.PSObject.Properties['sha256']) { $e.sha256 } else { '' }
|
|
944
1007
|
$st = if ($e.PSObject.Properties['status']) { $e.status } else { 'owned' }
|
|
945
|
-
$rafMap[$
|
|
1008
|
+
if ($ePath) { $rafMap[$ePath] = @{ path = $ePath; sha256 = $eSha256; status = $st } }
|
|
946
1009
|
}
|
|
947
1010
|
}
|
|
948
1011
|
foreach ($entry in $RootEntries) {
|
|
@@ -995,23 +1058,27 @@ function Remove-ManifestTool {
|
|
|
995
1058
|
}
|
|
996
1059
|
|
|
997
1060
|
# Build tools map without the target tool.
|
|
1061
|
+
# Guard all per-tool property accesses via PSObject.Properties['key'] (StrictMode-safe).
|
|
998
1062
|
$toolsMap = [System.Collections.Specialized.OrderedDictionary]::new()
|
|
999
1063
|
if ($data.tools) {
|
|
1000
1064
|
$data.tools.PSObject.Properties | ForEach-Object {
|
|
1001
1065
|
$tid = $_.Name
|
|
1002
1066
|
if ($tid -ne $Tool) {
|
|
1003
1067
|
$t = $_.Value
|
|
1004
|
-
$tP = if ($t.paths) { [System.Collections.Generic.List[string]]($t.paths) } else { [System.Collections.Generic.List[string]]::new() }
|
|
1068
|
+
$tP = if ($t.PSObject.Properties['paths'] -and $t.paths) { [System.Collections.Generic.List[string]]($t.paths) } else { [System.Collections.Generic.List[string]]::new() }
|
|
1005
1069
|
$tR = [System.Collections.Generic.List[hashtable]]::new()
|
|
1006
|
-
if ($t.root_agent_files) {
|
|
1007
|
-
|
|
1070
|
+
$tRaf = if ($t.PSObject.Properties['root_agent_files']) { $t.root_agent_files } else { $null }
|
|
1071
|
+
if ($tRaf) {
|
|
1072
|
+
foreach ($e in $tRaf) {
|
|
1073
|
+
$ePath = if ($e.PSObject.Properties['path']) { $e.path } else { '' }
|
|
1074
|
+
$eSha256 = if ($e.PSObject.Properties['sha256']) { $e.sha256 } else { '' }
|
|
1008
1075
|
$st = if ($e.PSObject.Properties['status']) { $e.status } else { 'owned' }
|
|
1009
|
-
$tR.Add(@{ path = $
|
|
1076
|
+
if ($ePath) { $tR.Add(@{ path = $ePath; sha256 = $eSha256; status = $st }) }
|
|
1010
1077
|
}
|
|
1011
1078
|
}
|
|
1012
1079
|
$toolsMap[$tid] = @{
|
|
1013
|
-
Version = if ($t.version) { $t.version } else { '' }
|
|
1014
|
-
InstalledAt = if ($t.installed_at) { $t.installed_at } else { '' }
|
|
1080
|
+
Version = if ($t.PSObject.Properties['version'] -and $t.version) { [string]$t.version } else { '' }
|
|
1081
|
+
InstalledAt = if ($t.PSObject.Properties['installed_at'] -and $t.installed_at) { $t.installed_at } else { '' }
|
|
1015
1082
|
Paths = $tP
|
|
1016
1083
|
RootAgentFiles = $tR
|
|
1017
1084
|
}
|
|
@@ -1024,8 +1091,8 @@ function Remove-ManifestTool {
|
|
|
1024
1091
|
return
|
|
1025
1092
|
}
|
|
1026
1093
|
|
|
1027
|
-
$topIat = if ($data.installed_at) { $data.installed_at } else { ([System.DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')) }
|
|
1028
|
-
$topVer = if ($data.aid_version) { $data.aid_version } else { '0.0.0' }
|
|
1094
|
+
$topIat = if ($data.PSObject.Properties['installed_at'] -and $data.installed_at) { $data.installed_at } else { ([System.DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')) }
|
|
1095
|
+
$topVer = if ($data.PSObject.Properties['aid_version'] -and $data.aid_version) { $data.aid_version } else { '0.0.0' }
|
|
1029
1096
|
|
|
1030
1097
|
$json = script:Build-ManifestJson -TopInstalledAt $topIat -TopVersion $topVer -ToolsMap $toolsMap
|
|
1031
1098
|
|
|
@@ -1099,7 +1166,7 @@ function Invoke-AidProvisionSharedStateHome {
|
|
|
1099
1166
|
"schema: 1",
|
|
1100
1167
|
"projects:"
|
|
1101
1168
|
)
|
|
1102
|
-
|
|
1169
|
+
[System.IO.File]::WriteAllText($tmp, (($seedLines) -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
|
|
1103
1170
|
Move-Item -LiteralPath $tmp -Destination $reg -Force -ErrorAction Stop
|
|
1104
1171
|
} catch {
|
|
1105
1172
|
Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
|
|
@@ -1211,15 +1278,20 @@ function Invoke-PruneToolDirs {
|
|
|
1211
1278
|
# Rule (d): prune now-empty subdirs (deepest first, skip the root itself).
|
|
1212
1279
|
$subdirs = @(Get-ChildItem -LiteralPath $ADir -Recurse -Directory -ErrorAction SilentlyContinue)
|
|
1213
1280
|
# Sort deepest first (longest path first by ordinal).
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1281
|
+
# Guard: pipeline over an empty @() returns $null on PowerShell; [System.Array]::Sort($null)
|
|
1282
|
+
# throws "Value cannot be null" (reproduces on pwsh when the dir has no subdirectories,
|
|
1283
|
+
# e.g. cursor/antigravity idempotent re-run after retired roots are already gone).
|
|
1284
|
+
if ($subdirs.Count -gt 0) {
|
|
1285
|
+
$subdirPaths = [string[]]($subdirs | ForEach-Object { $_.FullName })
|
|
1286
|
+
[System.Array]::Sort($subdirPaths, [System.StringComparer]::Ordinal)
|
|
1287
|
+
[System.Array]::Reverse($subdirPaths)
|
|
1288
|
+
foreach ($dp in $subdirPaths) {
|
|
1289
|
+
if (Test-Path $dp -PathType Container) {
|
|
1290
|
+
$rem = @(Get-ChildItem -LiteralPath $dp -ErrorAction SilentlyContinue) | Select-Object -First 1
|
|
1291
|
+
if (-not $rem) {
|
|
1292
|
+
Remove-Item -LiteralPath $dp -Force
|
|
1293
|
+
if ($AidVerbose) { Write-Host "Pruned dir: $dp" }
|
|
1294
|
+
}
|
|
1223
1295
|
}
|
|
1224
1296
|
}
|
|
1225
1297
|
}
|
|
@@ -1231,6 +1303,8 @@ function Invoke-PruneToolDirs {
|
|
|
1231
1303
|
# that map points copilot-cli at the .github ROOT (forbidden by R1).
|
|
1232
1304
|
# Scope is the R1-compliant set: .github/{agents,skills,aid} only.
|
|
1233
1305
|
# -----------------------------------------------------------------------
|
|
1306
|
+
# Per-tool scoping: new layout (work-005/delivery-001).
|
|
1307
|
+
# Codex unified under .codex/; cursor and antigravity no longer ship rules/.
|
|
1234
1308
|
switch ($Tool) {
|
|
1235
1309
|
'claude-code' {
|
|
1236
1310
|
& $pruneNativeDir (Join-Path $Target '.claude\agents')
|
|
@@ -1238,15 +1312,15 @@ function Invoke-PruneToolDirs {
|
|
|
1238
1312
|
& $pruneAidSubtree (Join-Path $Target '.claude\aid')
|
|
1239
1313
|
}
|
|
1240
1314
|
'codex' {
|
|
1241
|
-
#
|
|
1315
|
+
# New unified layout: everything under .codex/ (agents, skills, aid).
|
|
1242
1316
|
& $pruneNativeDir (Join-Path $Target '.codex\agents')
|
|
1243
|
-
& $pruneNativeDir (Join-Path $Target '.
|
|
1244
|
-
& $pruneAidSubtree (Join-Path $Target '.
|
|
1317
|
+
& $pruneNativeDir (Join-Path $Target '.codex\skills')
|
|
1318
|
+
& $pruneAidSubtree (Join-Path $Target '.codex\aid')
|
|
1245
1319
|
}
|
|
1246
1320
|
'cursor' {
|
|
1321
|
+
# rules/ dir removed from new layout; agents/skills/aid remain.
|
|
1247
1322
|
& $pruneNativeDir (Join-Path $Target '.cursor\agents')
|
|
1248
1323
|
& $pruneNativeDir (Join-Path $Target '.cursor\skills')
|
|
1249
|
-
& $pruneNativeDir (Join-Path $Target '.cursor\rules')
|
|
1250
1324
|
& $pruneAidSubtree (Join-Path $Target '.cursor\aid')
|
|
1251
1325
|
}
|
|
1252
1326
|
'copilot-cli' {
|
|
@@ -1256,7 +1330,8 @@ function Invoke-PruneToolDirs {
|
|
|
1256
1330
|
& $pruneAidSubtree (Join-Path $Target '.github\aid')
|
|
1257
1331
|
}
|
|
1258
1332
|
'antigravity' {
|
|
1259
|
-
|
|
1333
|
+
# rules/ dir removed from new layout; agents/skills/aid remain.
|
|
1334
|
+
& $pruneNativeDir (Join-Path $Target '.agent\agents')
|
|
1260
1335
|
& $pruneNativeDir (Join-Path $Target '.agent\skills')
|
|
1261
1336
|
& $pruneAidSubtree (Join-Path $Target '.agent\aid')
|
|
1262
1337
|
}
|
|
@@ -1267,6 +1342,140 @@ function Invoke-PruneToolDirs {
|
|
|
1267
1342
|
}
|
|
1268
1343
|
}
|
|
1269
1344
|
|
|
1345
|
+
# ---------------------------------------------------------------------------
|
|
1346
|
+
# Retired-root migration sweep (FR7/FR7a)
|
|
1347
|
+
# ---------------------------------------------------------------------------
|
|
1348
|
+
|
|
1349
|
+
# Invoke-MigrateRetiredLayout <target> <tool> [aidVerbose]
|
|
1350
|
+
#
|
|
1351
|
+
# Complete-replacement migration: removes AID-owned content from the static
|
|
1352
|
+
# list of retired AID roots that no longer exist in the new bundle layout.
|
|
1353
|
+
# Called from Install-AidTool BEFORE Invoke-PruneToolDirs (same aid update pass).
|
|
1354
|
+
#
|
|
1355
|
+
# Retired roots swept per tool:
|
|
1356
|
+
# codex: .agents\ (split layout retired)
|
|
1357
|
+
# cursor: .cursor\rules\ (rules dir retired)
|
|
1358
|
+
# antigravity: .agent\rules\ (rules dir retired)
|
|
1359
|
+
#
|
|
1360
|
+
# Ownership markers applied (content-isolation.md rules 1+2):
|
|
1361
|
+
# Marker 1: filename starts with "aid-" (tool-native dir files)
|
|
1362
|
+
# Marker 2: lives inside an "aid\" subtree
|
|
1363
|
+
#
|
|
1364
|
+
# Marker 3 (AID:BEGIN/END region in root files) is NOT touched here;
|
|
1365
|
+
# that is handled by Copy-RootAgentFile exclusively.
|
|
1366
|
+
#
|
|
1367
|
+
# User content (no marker) is NEVER removed.
|
|
1368
|
+
# Idempotent: a no-op when the retired path is already absent.
|
|
1369
|
+
# Sets $script:_MigrateRetiredCount with the count of items removed.
|
|
1370
|
+
function Invoke-MigrateRetiredLayout {
|
|
1371
|
+
param(
|
|
1372
|
+
[string]$Target,
|
|
1373
|
+
[string]$Tool,
|
|
1374
|
+
[bool]$AidVerbose = $false,
|
|
1375
|
+
[bool]$ListOnly = $false # $true = dry-run enumeration only (no removals)
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
$script:_MigrateRetiredCount = 0
|
|
1379
|
+
|
|
1380
|
+
# Determine whether a file is AID-owned (markers 1 or 2).
|
|
1381
|
+
$isAidOwned = {
|
|
1382
|
+
param([System.IO.FileInfo]$File)
|
|
1383
|
+
# Marker 1: filename starts with "aid-".
|
|
1384
|
+
if ($File.Name -like 'aid-*') { return $true }
|
|
1385
|
+
# Marker 2: lives inside an "aid" folder (any ancestor named "aid").
|
|
1386
|
+
$dir = $File.Directory
|
|
1387
|
+
while ($dir -ne $null -and $dir.FullName -ne $Target -and $dir.FullName -ne $dir.Root.FullName) {
|
|
1388
|
+
if ($dir.Name -eq 'aid') { return $true }
|
|
1389
|
+
$dir = $dir.Parent
|
|
1390
|
+
}
|
|
1391
|
+
return $false
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
# Sweep one retired root directory: move AID-owned files to trash, prune empty
|
|
1395
|
+
# dirs, then remove the retired root itself if now empty.
|
|
1396
|
+
# In ListOnly mode: enumerate would-be-moved files, make no changes.
|
|
1397
|
+
$sweepRetiredRoot = {
|
|
1398
|
+
param([string]$RDir)
|
|
1399
|
+
if (-not (Test-Path $RDir -PathType Container)) { return }
|
|
1400
|
+
|
|
1401
|
+
$trashBase = Join-Path $Target (Join-Path '.aid' '.trash')
|
|
1402
|
+
|
|
1403
|
+
# Walk all files; move (or collect) AID-owned ones.
|
|
1404
|
+
$files = @(Get-ChildItem -LiteralPath $RDir -Recurse -File -ErrorAction SilentlyContinue |
|
|
1405
|
+
Sort-Object FullName)
|
|
1406
|
+
foreach ($f in $files) {
|
|
1407
|
+
if (& $isAidOwned $f) {
|
|
1408
|
+
if ($ListOnly) {
|
|
1409
|
+
# Emit path via Write-Output so the caller can capture with $(...).
|
|
1410
|
+
Write-Output $f.FullName
|
|
1411
|
+
$script:_MigrateRetiredCount++
|
|
1412
|
+
} else {
|
|
1413
|
+
# Compute relative path from $Target and move to trash.
|
|
1414
|
+
$rel = $f.FullName.Substring($Target.Length).TrimStart([char]'\', [char]'/')
|
|
1415
|
+
$dest = Join-Path $trashBase $rel
|
|
1416
|
+
$destDir = Split-Path $dest -Parent
|
|
1417
|
+
New-Item -ItemType Directory -Path $destDir -Force -ErrorAction SilentlyContinue | Out-Null
|
|
1418
|
+
Move-Item -LiteralPath $f.FullName -Destination $dest -Force -ErrorAction SilentlyContinue
|
|
1419
|
+
$script:_MigrateRetiredCount++
|
|
1420
|
+
if ($AidVerbose) { Write-Host "Trashed: $($f.FullName) -> $dest" }
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
if ($ListOnly) { return }
|
|
1426
|
+
|
|
1427
|
+
# Prune now-empty subdirs (deepest first).
|
|
1428
|
+
$subdirs = @(Get-ChildItem -LiteralPath $RDir -Recurse -Directory -ErrorAction SilentlyContinue)
|
|
1429
|
+
# Guard: pipeline over an empty @() returns $null on PowerShell; [System.Array]::Sort($null)
|
|
1430
|
+
# throws "Value cannot be null" when the retired root has no subdirectories (e.g. cursor
|
|
1431
|
+
# .cursor/rules/ contains only files, no nested dirs; or on idempotent re-run when the dir
|
|
1432
|
+
# is already absent). Mirrors bash rm -rf / rmdir which silently no-op on absent paths.
|
|
1433
|
+
if ($subdirs.Count -gt 0) {
|
|
1434
|
+
$subPaths = [string[]]($subdirs | ForEach-Object { $_.FullName })
|
|
1435
|
+
[System.Array]::Sort($subPaths, [System.StringComparer]::Ordinal)
|
|
1436
|
+
[System.Array]::Reverse($subPaths)
|
|
1437
|
+
foreach ($dp in $subPaths) {
|
|
1438
|
+
if (Test-Path $dp -PathType Container) {
|
|
1439
|
+
$rem = @(Get-ChildItem -LiteralPath $dp -ErrorAction SilentlyContinue) | Select-Object -First 1
|
|
1440
|
+
if (-not $rem) {
|
|
1441
|
+
Remove-Item -LiteralPath $dp -Force -ErrorAction SilentlyContinue
|
|
1442
|
+
if ($AidVerbose) { Write-Host "Retired dir: $dp" }
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
# Remove the retired root itself if now empty.
|
|
1449
|
+
if (Test-Path $RDir -PathType Container) {
|
|
1450
|
+
$rem = @(Get-ChildItem -LiteralPath $RDir -ErrorAction SilentlyContinue) | Select-Object -First 1
|
|
1451
|
+
if (-not $rem) {
|
|
1452
|
+
Remove-Item -LiteralPath $RDir -Force -ErrorAction SilentlyContinue
|
|
1453
|
+
if ($AidVerbose) { Write-Host "Retired root dir: $RDir" }
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
switch ($Tool) {
|
|
1459
|
+
'codex' {
|
|
1460
|
+
# Retired root: .agents\ (old split layout -- skills\ + aid\ lived here).
|
|
1461
|
+
& $sweepRetiredRoot (Join-Path $Target '.agents')
|
|
1462
|
+
}
|
|
1463
|
+
'cursor' {
|
|
1464
|
+
# Retired root: .cursor\rules\ (rules dir no longer in new layout).
|
|
1465
|
+
& $sweepRetiredRoot (Join-Path $Target '.cursor\rules')
|
|
1466
|
+
}
|
|
1467
|
+
'antigravity' {
|
|
1468
|
+
# Retired root: .agent\rules\ (rules dir no longer in new layout).
|
|
1469
|
+
& $sweepRetiredRoot (Join-Path $Target '.agent\rules')
|
|
1470
|
+
}
|
|
1471
|
+
# claude-code and copilot-cli have no retired roots in this migration.
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (-not $ListOnly -and $script:_MigrateRetiredCount -gt 0) {
|
|
1475
|
+
Write-Host " $($script:_MigrateRetiredCount) retired AID file(s) moved to .aid/.trash/"
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1270
1479
|
# ---------------------------------------------------------------------------
|
|
1271
1480
|
# Version marker
|
|
1272
1481
|
# ---------------------------------------------------------------------------
|
|
@@ -1284,6 +1493,118 @@ function Write-VersionMarker {
|
|
|
1284
1493
|
[System.IO.File]::WriteAllBytes($markerPath, $bytes)
|
|
1285
1494
|
}
|
|
1286
1495
|
|
|
1496
|
+
# ---------------------------------------------------------------------------
|
|
1497
|
+
# Project-level provisioning (work-007): required runtime file + VCS hygiene.
|
|
1498
|
+
# Parity with bash seed_settings_yml / update_gitignore. Both idempotent.
|
|
1499
|
+
# ---------------------------------------------------------------------------
|
|
1500
|
+
|
|
1501
|
+
# AID-managed .gitignore region markers (must match the bash _AID_GI_* strings).
|
|
1502
|
+
$script:_AidGiBegin = "# >>> AID managed -- do not edit (aid add/update maintains this block) >>>"
|
|
1503
|
+
$script:_AidGiEnd = "# <<< AID managed <<<"
|
|
1504
|
+
|
|
1505
|
+
# Get-AidGitignoreBlock - return the AID-managed block as a single LF-joined
|
|
1506
|
+
# string (markers inclusive), NO trailing newline (matches bash contract).
|
|
1507
|
+
function script:Get-AidGitignoreBlock {
|
|
1508
|
+
$lines = @(
|
|
1509
|
+
$script:_AidGiBegin,
|
|
1510
|
+
".aid/.temp/",
|
|
1511
|
+
".aid/.trash/",
|
|
1512
|
+
".aid/.heartbeat/",
|
|
1513
|
+
".aid/generated/",
|
|
1514
|
+
".aid/knowledge/.cache/",
|
|
1515
|
+
".aid/knowledge/.manual-checklist.json",
|
|
1516
|
+
".aid/knowledge/.spot-check-facts.txt",
|
|
1517
|
+
$script:_AidGiEnd
|
|
1518
|
+
)
|
|
1519
|
+
return ($lines -join "`n")
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
# Initialize-AidSettingsFile <target> <tool>
|
|
1523
|
+
# Seed <target>\.aid\settings.yml from the tool's just-installed template when it
|
|
1524
|
+
# does not already exist. NEVER overwrites existing settings.yml (user config).
|
|
1525
|
+
# Not manifest-tracked. Sets $script:_SeededSettings.
|
|
1526
|
+
function script:Initialize-AidSettingsFile {
|
|
1527
|
+
param([string]$Target, [string]$Tool)
|
|
1528
|
+
$script:_SeededSettings = $false
|
|
1529
|
+
$root = script:Get-RootDir -Tool $Tool
|
|
1530
|
+
if (-not $root) { return }
|
|
1531
|
+
$aidDir = Join-Path $Target '.aid'
|
|
1532
|
+
$dst = Join-Path $aidDir 'settings.yml'
|
|
1533
|
+
$tmpl = Join-Path (Join-Path (Join-Path (Join-Path $Target $root) 'aid') 'templates') 'settings.yml'
|
|
1534
|
+
if (Test-Path $dst -PathType Leaf) { return } # never clobber user config
|
|
1535
|
+
# Template absent -> surface it (work-007 C5): a silent skip re-triggers the
|
|
1536
|
+
# "no settings.yml" class and the format gate would then warn forever.
|
|
1537
|
+
if (-not (Test-Path $tmpl -PathType Leaf)) {
|
|
1538
|
+
[Console]::Error.WriteLine("WARN: AidInstallCore: settings template missing ($tmpl); .aid/settings.yml not seeded for '$Tool'.")
|
|
1539
|
+
return
|
|
1540
|
+
}
|
|
1541
|
+
if (-not (Test-Path $aidDir -PathType Container)) {
|
|
1542
|
+
New-Item -ItemType Directory -Path $aidDir -Force | Out-Null
|
|
1543
|
+
}
|
|
1544
|
+
# Seed from template, STAMPING format_version as the first line if absent, so
|
|
1545
|
+
# the settings-format gate stays quiet. Byte-faithful (raw-byte prepend, no
|
|
1546
|
+
# text round-trip) so the install tree is byte-identical to the bash seed
|
|
1547
|
+
# (test-install-parity.sh diff -r covers .aid/settings.yml).
|
|
1548
|
+
$tmplBytes = [System.IO.File]::ReadAllBytes($tmpl)
|
|
1549
|
+
$tmplText = [System.Text.Encoding]::UTF8.GetString($tmplBytes)
|
|
1550
|
+
if ($tmplText -match '(?m)^format_version:') {
|
|
1551
|
+
[System.IO.File]::WriteAllBytes($dst, $tmplBytes)
|
|
1552
|
+
} else {
|
|
1553
|
+
$fv = [System.Text.Encoding]::UTF8.GetBytes("format_version: $($script:_AidSupportedFormat)`n")
|
|
1554
|
+
[System.IO.File]::WriteAllBytes($dst, [byte[]]($fv + $tmplBytes))
|
|
1555
|
+
}
|
|
1556
|
+
$script:_SeededSettings = $true
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
# Update-AidGitignore <target>
|
|
1560
|
+
# Ensure <target>\.gitignore carries the AID-managed region with the current
|
|
1561
|
+
# transient .aid/ exclusions. Creates if absent; else strips any existing AID
|
|
1562
|
+
# region, preserves user content, appends a fresh region with a single blank-line
|
|
1563
|
+
# separator. Idempotent. Sets $script:_GitignoreAction to created|updated|unchanged.
|
|
1564
|
+
function script:Update-AidGitignore {
|
|
1565
|
+
param([string]$Target)
|
|
1566
|
+
$script:_GitignoreAction = 'unchanged'
|
|
1567
|
+
$gi = Join-Path $Target '.gitignore'
|
|
1568
|
+
$block = script:Get-AidGitignoreBlock
|
|
1569
|
+
|
|
1570
|
+
if (-not (Test-Path $gi -PathType Leaf)) {
|
|
1571
|
+
[System.IO.File]::WriteAllText($gi, $block + "`n", [System.Text.UTF8Encoding]::new($false))
|
|
1572
|
+
$script:_GitignoreAction = 'created'
|
|
1573
|
+
return
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
$existing = [System.IO.File]::ReadAllText($gi)
|
|
1577
|
+
$normalized = ($existing -replace "`r`n", "`n") -replace "`r", "`n"
|
|
1578
|
+
$srcLines = $normalized -split "`n"
|
|
1579
|
+
|
|
1580
|
+
# Strip any existing AID region.
|
|
1581
|
+
$kept = [System.Collections.Generic.List[string]]::new()
|
|
1582
|
+
$inBlk = $false
|
|
1583
|
+
foreach ($ln in $srcLines) {
|
|
1584
|
+
if ($ln -eq $script:_AidGiBegin) { $inBlk = $true; continue }
|
|
1585
|
+
if ($inBlk -and $ln -eq $script:_AidGiEnd) { $inBlk = $false; continue }
|
|
1586
|
+
if ($inBlk) { continue }
|
|
1587
|
+
$kept.Add($ln)
|
|
1588
|
+
}
|
|
1589
|
+
# Trim trailing blank lines.
|
|
1590
|
+
while ($kept.Count -gt 0 -and $kept[$kept.Count - 1] -match '^[ \t]*$') {
|
|
1591
|
+
$kept.RemoveAt($kept.Count - 1)
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
$sb = [System.Text.StringBuilder]::new()
|
|
1595
|
+
foreach ($ln in $kept) { [void]$sb.Append($ln); [void]$sb.Append("`n") }
|
|
1596
|
+
if ($kept.Count -gt 0) { [void]$sb.Append("`n") }
|
|
1597
|
+
[void]$sb.Append($block); [void]$sb.Append("`n")
|
|
1598
|
+
$newContent = $sb.ToString()
|
|
1599
|
+
|
|
1600
|
+
if ($newContent -eq $normalized) {
|
|
1601
|
+
$script:_GitignoreAction = 'unchanged'
|
|
1602
|
+
} else {
|
|
1603
|
+
[System.IO.File]::WriteAllText($gi, $newContent, [System.Text.UTF8Encoding]::new($false))
|
|
1604
|
+
$script:_GitignoreAction = 'updated'
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1287
1608
|
# ---------------------------------------------------------------------------
|
|
1288
1609
|
# High-level Install-AidTool
|
|
1289
1610
|
# ---------------------------------------------------------------------------
|
|
@@ -1312,6 +1633,10 @@ function Install-AidTool {
|
|
|
1312
1633
|
$script:_CopyCountUpToDate = 0
|
|
1313
1634
|
$script:_CopyCountUpdated = 0
|
|
1314
1635
|
$script:_CopyCountSkipped = 0
|
|
1636
|
+
$script:_CopyCountFailed = 0
|
|
1637
|
+
# Reset project-provisioning state (work-007).
|
|
1638
|
+
$script:_SeededSettings = $false
|
|
1639
|
+
$script:_GitignoreAction = 'unchanged'
|
|
1315
1640
|
|
|
1316
1641
|
$rootAgentFile = script:Get-RootAgentFile -Tool $Tool
|
|
1317
1642
|
|
|
@@ -1339,16 +1664,13 @@ function Install-AidTool {
|
|
|
1339
1664
|
}
|
|
1340
1665
|
}
|
|
1341
1666
|
'codex' {
|
|
1667
|
+
# New unified layout: .codex\ only (agents, skills, aid all under .codex\).
|
|
1668
|
+
# .agents\ is the RETIRED split layout -- handled by Invoke-MigrateRetiredLayout.
|
|
1342
1669
|
$codexDir = Join-Path $StagingDir '.codex'
|
|
1343
|
-
$agentsDir = Join-Path $StagingDir '.agents'
|
|
1344
1670
|
if (Test-Path $codexDir -PathType Container) {
|
|
1345
1671
|
Copy-AidDir -SrcDir $codexDir -DstDir (Join-Path $Target '.codex') -Force $Force -AidVerbose $AidVerbose
|
|
1346
1672
|
& $collectPaths $codexDir $StagingDir $installPaths
|
|
1347
1673
|
}
|
|
1348
|
-
if (Test-Path $agentsDir -PathType Container) {
|
|
1349
|
-
Copy-AidDir -SrcDir $agentsDir -DstDir (Join-Path $Target '.agents') -Force $Force -AidVerbose $AidVerbose
|
|
1350
|
-
& $collectPaths $agentsDir $StagingDir $installPaths
|
|
1351
|
-
}
|
|
1352
1674
|
}
|
|
1353
1675
|
'cursor' {
|
|
1354
1676
|
$cursorDir = Join-Path $StagingDir '.cursor'
|
|
@@ -1373,6 +1695,12 @@ function Install-AidTool {
|
|
|
1373
1695
|
}
|
|
1374
1696
|
}
|
|
1375
1697
|
|
|
1698
|
+
# Abort loudly if any AID-owned file failed to copy -- do NOT proceed to
|
|
1699
|
+
# write a manifest that records a partial install as success (work-007 C4).
|
|
1700
|
+
if ($script:_CopyCountFailed -gt 0) {
|
|
1701
|
+
throw "AidInstallCore: $($script:_CopyCountFailed) file(s) failed to copy for '$Tool'. Install aborted (incomplete)."
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1376
1704
|
# Handle root agent file via in-place region update (Pillar 3).
|
|
1377
1705
|
$rootSrc = Join-Path $StagingDir $rootAgentFile
|
|
1378
1706
|
$rootDst = Join-Path $Target $rootAgentFile
|
|
@@ -1389,10 +1717,33 @@ function Install-AidTool {
|
|
|
1389
1717
|
$installPaths.Add($rootAgentFile)
|
|
1390
1718
|
}
|
|
1391
1719
|
|
|
1720
|
+
# Manifest-seam entry gate (PLAN risk #3, delivery-001->002 seam).
|
|
1721
|
+
# Runs BEFORE Write-AidManifest so a contaminated bundle never writes to disk.
|
|
1722
|
+
# Assert that the new bundle's path set does NOT contain any retired roots.
|
|
1723
|
+
# If a retired path leaked into the new manifest, fail loudly -- do not prune
|
|
1724
|
+
# against a contaminated manifest (content-isolation cornerstone).
|
|
1725
|
+
$retiredRoots = @('.agents/', '.cursor/rules/', '.agent/rules/')
|
|
1726
|
+
foreach ($rr in $retiredRoots) {
|
|
1727
|
+
foreach ($p in $installPaths) {
|
|
1728
|
+
$pNorm = $p -replace '\\', '/'
|
|
1729
|
+
if ($pNorm.StartsWith($rr, [System.StringComparison]::Ordinal)) {
|
|
1730
|
+
[Console]::Error.WriteLine("ERROR: aid-install-core: manifest-seam violation: retired root '$rr' leaked into the new bundle manifest (path: $p). Aborting install to protect user content.")
|
|
1731
|
+
return 1
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1392
1736
|
# Write manifest (merge).
|
|
1393
1737
|
Write-AidManifest -ManifestPath $manifest -Tool $Tool -Version $Version `
|
|
1394
1738
|
-Paths @($installPaths) -RootEntries @($rootEntries)
|
|
1395
1739
|
|
|
1740
|
+
# Retired-root migration sweep (FR7/FR7a).
|
|
1741
|
+
# Remove AID-owned content from retired layout dirs BEFORE the normal prune,
|
|
1742
|
+
# so that old .agents\, .cursor\rules\, .agent\rules\ trees are cleaned up.
|
|
1743
|
+
# A non-zero rc from Install-AidTool is treated as a mid-commit failure (caller
|
|
1744
|
+
# prints the "re-run to heal" message); this function returns 0 (WARN-not-fail).
|
|
1745
|
+
Invoke-MigrateRetiredLayout -Target $Target -Tool $Tool -AidVerbose $AidVerbose
|
|
1746
|
+
|
|
1396
1747
|
# Prune stale AID-owned files (Pillar 2, R7).
|
|
1397
1748
|
# Build a HashSet from the new manifest path set for O(1) lookup.
|
|
1398
1749
|
$pruneSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
@@ -1402,6 +1753,11 @@ function Install-AidTool {
|
|
|
1402
1753
|
# Write version marker.
|
|
1403
1754
|
Write-VersionMarker -Target $Target -Version $Version
|
|
1404
1755
|
|
|
1756
|
+
# Project-level provisioning (work-007): seed required settings.yml and
|
|
1757
|
+
# maintain the .gitignore AID region. Both idempotent; safe per-tool.
|
|
1758
|
+
script:Initialize-AidSettingsFile -Target $Target -Tool $Tool
|
|
1759
|
+
script:Update-AidGitignore -Target $Target
|
|
1760
|
+
|
|
1405
1761
|
# Print concise install summary (always shown; per-file lines only when AidVerbose).
|
|
1406
1762
|
$totalFiles = $script:_CopyCountCopied + $script:_CopyCountUpToDate + $script:_CopyCountUpdated + $script:_CopyCountSkipped
|
|
1407
1763
|
if ($totalFiles -gt 0) {
|
|
@@ -1418,6 +1774,13 @@ function Install-AidTool {
|
|
|
1418
1774
|
}
|
|
1419
1775
|
}
|
|
1420
1776
|
|
|
1777
|
+
# Report project-level provisioning actions (work-007; always shown).
|
|
1778
|
+
if ($script:_SeededSettings) { Write-Host " Created .aid/settings.yml from template" }
|
|
1779
|
+
switch ($script:_GitignoreAction) {
|
|
1780
|
+
'created' { Write-Host " Created .gitignore with AID exclusions" }
|
|
1781
|
+
'updated' { Write-Host " Updated .gitignore AID exclusions" }
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1421
1784
|
return 0
|
|
1422
1785
|
}
|
|
1423
1786
|
|
|
@@ -1511,13 +1874,20 @@ function Uninstall-AidTool {
|
|
|
1511
1874
|
# Remove this tool from manifest.
|
|
1512
1875
|
Remove-ManifestTool -ManifestPath $ManifestPath -Tool $Tool
|
|
1513
1876
|
|
|
1514
|
-
# If no manifest remains
|
|
1877
|
+
# If no manifest remains (last tool removed), remove the AID-provisioned
|
|
1878
|
+
# project files so a full uninstall leaves .aid/ clean (work-007).
|
|
1515
1879
|
if (-not (Test-Path $ManifestPath -PathType Leaf)) {
|
|
1516
1880
|
$aidMetaDir = [System.IO.Path]::GetDirectoryName($ManifestPath)
|
|
1517
1881
|
$versionMarker = Join-Path $aidMetaDir '.aid-version'
|
|
1518
1882
|
if (Test-Path $versionMarker -PathType Leaf) {
|
|
1519
1883
|
Remove-Item -LiteralPath $versionMarker -Force
|
|
1520
1884
|
}
|
|
1885
|
+
# Remove the install-time-seeded settings.yml (symmetric with the seed).
|
|
1886
|
+
# Only fires when NO tools remain; a partial uninstall keeps it.
|
|
1887
|
+
$seededSettings = Join-Path $aidMetaDir 'settings.yml'
|
|
1888
|
+
if (Test-Path $seededSettings -PathType Leaf) {
|
|
1889
|
+
Remove-Item -LiteralPath $seededSettings -Force
|
|
1890
|
+
}
|
|
1521
1891
|
if (Test-Path $aidMetaDir -PathType Container) {
|
|
1522
1892
|
$rem = Get-ChildItem -LiteralPath $aidMetaDir -Recurse -File -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
1523
1893
|
if (-not $rem) {
|
|
@@ -1547,7 +1917,12 @@ function Get-ManifestToolList {
|
|
|
1547
1917
|
$data.tools.PSObject.Properties | ForEach-Object {
|
|
1548
1918
|
$tid = $_.Name
|
|
1549
1919
|
$t = $_.Value
|
|
1550
|
-
|
|
1920
|
+
# Guard all per-tool property accesses via PSObject.Properties['key'] first.
|
|
1921
|
+
# Set-StrictMode -Version Latest (active in this module) throws
|
|
1922
|
+
# PropertyNotFoundException when accessing a missing property directly
|
|
1923
|
+
# (e.g. $t.root_agent_files on pre-work-005 manifests that lack it).
|
|
1924
|
+
# PSObject.Properties['key'] returns $null safely for absent properties.
|
|
1925
|
+
$ver = if ($t.PSObject.Properties['version']) { [string]$t.version } else { '' }
|
|
1551
1926
|
# Determine root agent file for this tool.
|
|
1552
1927
|
$rootAgent = switch ($tid) {
|
|
1553
1928
|
'claude-code' { 'CLAUDE.md' }
|
|
@@ -1555,9 +1930,10 @@ function Get-ManifestToolList {
|
|
|
1555
1930
|
}
|
|
1556
1931
|
# Read root agent status from manifest.
|
|
1557
1932
|
$rootStatus = ''
|
|
1558
|
-
if ($t.root_agent_files) {
|
|
1559
|
-
|
|
1560
|
-
|
|
1933
|
+
$rafEntries = if ($t.PSObject.Properties['root_agent_files']) { $t.root_agent_files } else { $null }
|
|
1934
|
+
if ($rafEntries) {
|
|
1935
|
+
foreach ($entry in $rafEntries) {
|
|
1936
|
+
if ($entry.PSObject.Properties['path'] -and ($entry.path -eq $rootAgent)) {
|
|
1561
1937
|
$rootStatus = if ($entry.PSObject.Properties['status']) { $entry.status } else { 'owned' }
|
|
1562
1938
|
break
|
|
1563
1939
|
}
|
|
@@ -1761,7 +2137,7 @@ function Get-AidStatus {
|
|
|
1761
2137
|
$aidVersion = ''
|
|
1762
2138
|
try {
|
|
1763
2139
|
$data = Get-Content -LiteralPath $manifest -Raw | ConvertFrom-Json
|
|
1764
|
-
if ($data.aid_version) { $aidVersion = $data.aid_version }
|
|
2140
|
+
if ($data.PSObject.Properties['aid_version'] -and $data.aid_version) { $aidVersion = $data.aid_version }
|
|
1765
2141
|
} catch {}
|
|
1766
2142
|
|
|
1767
2143
|
# Read CLI ref version from $env:AID_HOME/VERSION.
|