aid-installer 2.0.0 → 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/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.0
1
+ 2.0.1
package/bin/aid CHANGED
@@ -95,6 +95,10 @@ _aid_is_project_dir() {
95
95
  # C1: Per-repo format stamp constant.
96
96
  # The current .aid/ layout version. Bumped ONLY on a breaking layout change,
97
97
  # never on every CLI release. Defined exactly once; all comparisons read this.
98
+ # NOTE (work-007 C6): the install-time settings seed also stamps this value.
99
+ # On a bump, update ALL carriers together: this line, bin/aid.ps1
100
+ # AidSupportedFormat, and lib/AidInstallCore.psm1 $script:_AidSupportedFormat.
101
+ # (lib/aid-install-core.sh reads THIS var via ${AID_SUPPORTED_FORMAT:-1}.)
98
102
  # ---------------------------------------------------------------------------
99
103
  readonly AID_SUPPORTED_FORMAT=1
100
104
 
package/bin/aid.ps1 CHANGED
@@ -135,6 +135,9 @@ function script:Test-AidIsProjectDir {
135
135
  # The current .aid/ layout version. Bumped ONLY on a breaking layout change,
136
136
  # never on every CLI release. Defined exactly once; all comparisons read this.
137
137
  # Integer must equal the bash AID_SUPPORTED_FORMAT in bin/aid.
138
+ # NOTE (work-007 C6): the install-time settings seed also stamps this value. On a
139
+ # bump, update ALL carriers together: this line, bin/aid AID_SUPPORTED_FORMAT, and
140
+ # lib/AidInstallCore.psm1 $script:_AidSupportedFormat (the PS seed's source).
138
141
  # ---------------------------------------------------------------------------
139
142
  Set-Variable -Name AidSupportedFormat -Value 1 -Option Constant -Scope Script
140
143
 
@@ -62,10 +62,18 @@ $script:_CopyCountCopied = 0
62
62
  $script:_CopyCountUpToDate = 0
63
63
  $script:_CopyCountUpdated = 0
64
64
  $script:_CopyCountSkipped = 0
65
+ $script:_CopyCountFailed = 0
65
66
 
66
67
  # Module-level prune counter. Reset by Invoke-PruneToolDirs before each prune pass.
67
68
  $script:_PruneRemoved = 0
68
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
+
69
77
  # ---------------------------------------------------------------------------
70
78
  # Constants
71
79
  # ---------------------------------------------------------------------------
@@ -85,6 +93,21 @@ function script:Get-RootAgentFile {
85
93
  }
86
94
  }
87
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
+
88
111
  # ---------------------------------------------------------------------------
89
112
  # Utility
90
113
  # ---------------------------------------------------------------------------
@@ -361,7 +384,13 @@ function Copy-AidFile {
361
384
  }
362
385
 
363
386
  if (-not (Test-Path $Dst -PathType Leaf)) {
364
- Copy-Item -LiteralPath $Src -Destination $Dst -Force
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
+ }
365
394
  $script:_CopyCountCopied++
366
395
  if ($AidVerbose) { Write-Host "Copied: $Dst" }
367
396
  return
@@ -377,15 +406,22 @@ function Copy-AidFile {
377
406
  return
378
407
  }
379
408
 
380
- # File exists and differs.
381
- if ($Force) {
382
- Copy-Item -LiteralPath $Src -Destination $Dst -Force
383
- $script:_CopyCountUpdated++
384
- if ($AidVerbose) { Write-Host "Updated: $Dst" }
385
- } else {
386
- $script:_CopyCountSkipped++
387
- if ($AidVerbose) { Write-Host "Skipped (differs; use --force): $Dst" }
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
388
422
  }
423
+ $script:_CopyCountUpdated++
424
+ if ($AidVerbose) { Write-Host "Updated: $Dst" }
389
425
  }
390
426
 
391
427
  # Copy-AidDir <srcDir> <dstDir> [force] [aidVerbose]
@@ -438,8 +474,13 @@ function script:Test-AidHeadingStem {
438
474
  $stem = $Line.Substring(3) # strip "## "
439
475
  # Strip trailing parenthetical: " (anything)"
440
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.
441
481
  switch ($stem) {
442
482
  'Knowledge Base' { return $true }
483
+ 'Workflow' { return $true }
443
484
  'Review output format' { return $true }
444
485
  'Permissions' { return $true }
445
486
  'Tracking discipline' { return $true }
@@ -1452,6 +1493,118 @@ function Write-VersionMarker {
1452
1493
  [System.IO.File]::WriteAllBytes($markerPath, $bytes)
1453
1494
  }
1454
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
+
1455
1608
  # ---------------------------------------------------------------------------
1456
1609
  # High-level Install-AidTool
1457
1610
  # ---------------------------------------------------------------------------
@@ -1480,6 +1633,10 @@ function Install-AidTool {
1480
1633
  $script:_CopyCountUpToDate = 0
1481
1634
  $script:_CopyCountUpdated = 0
1482
1635
  $script:_CopyCountSkipped = 0
1636
+ $script:_CopyCountFailed = 0
1637
+ # Reset project-provisioning state (work-007).
1638
+ $script:_SeededSettings = $false
1639
+ $script:_GitignoreAction = 'unchanged'
1483
1640
 
1484
1641
  $rootAgentFile = script:Get-RootAgentFile -Tool $Tool
1485
1642
 
@@ -1538,6 +1695,12 @@ function Install-AidTool {
1538
1695
  }
1539
1696
  }
1540
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
+
1541
1704
  # Handle root agent file via in-place region update (Pillar 3).
1542
1705
  $rootSrc = Join-Path $StagingDir $rootAgentFile
1543
1706
  $rootDst = Join-Path $Target $rootAgentFile
@@ -1590,6 +1753,11 @@ function Install-AidTool {
1590
1753
  # Write version marker.
1591
1754
  Write-VersionMarker -Target $Target -Version $Version
1592
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
+
1593
1761
  # Print concise install summary (always shown; per-file lines only when AidVerbose).
1594
1762
  $totalFiles = $script:_CopyCountCopied + $script:_CopyCountUpToDate + $script:_CopyCountUpdated + $script:_CopyCountSkipped
1595
1763
  if ($totalFiles -gt 0) {
@@ -1606,6 +1774,13 @@ function Install-AidTool {
1606
1774
  }
1607
1775
  }
1608
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
+
1609
1784
  return 0
1610
1785
  }
1611
1786
 
@@ -1699,13 +1874,20 @@ function Uninstall-AidTool {
1699
1874
  # Remove this tool from manifest.
1700
1875
  Remove-ManifestTool -ManifestPath $ManifestPath -Tool $Tool
1701
1876
 
1702
- # If no manifest remains, remove version marker and .aid dir if empty.
1877
+ # If no manifest remains (last tool removed), remove the AID-provisioned
1878
+ # project files so a full uninstall leaves .aid/ clean (work-007).
1703
1879
  if (-not (Test-Path $ManifestPath -PathType Leaf)) {
1704
1880
  $aidMetaDir = [System.IO.Path]::GetDirectoryName($ManifestPath)
1705
1881
  $versionMarker = Join-Path $aidMetaDir '.aid-version'
1706
1882
  if (Test-Path $versionMarker -PathType Leaf) {
1707
1883
  Remove-Item -LiteralPath $versionMarker -Force
1708
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
+ }
1709
1891
  if (Test-Path $aidMetaDir -PathType Container) {
1710
1892
  $rem = Get-ChildItem -LiteralPath $aidMetaDir -Recurse -File -ErrorAction SilentlyContinue | Select-Object -First 1
1711
1893
  if (-not $rem) {
@@ -81,6 +81,19 @@ _root_agent_file() {
81
81
  esac
82
82
  }
83
83
 
84
+ # _root_dir <tool> - print the tool's install-tree root dir name (relative to the
85
+ # project root). The AID-own subtree lives at <root>/aid/ (e.g. .claude/aid/).
86
+ # Mirrors the per-tool dispatch in install_tool.
87
+ _root_dir() {
88
+ case "$1" in
89
+ claude-code) echo ".claude" ;;
90
+ codex) echo ".codex" ;;
91
+ cursor) echo ".cursor" ;;
92
+ copilot-cli) echo ".github" ;;
93
+ antigravity) echo ".agent" ;;
94
+ esac
95
+ }
96
+
84
97
  # ---------------------------------------------------------------------------
85
98
  # Utility
86
99
  # ---------------------------------------------------------------------------
@@ -315,7 +328,11 @@ copy_file() {
315
328
  mkdir -p "$dst_dir"
316
329
 
317
330
  if [[ ! -e "$dst" ]]; then
318
- cp "$src" "$dst"
331
+ if ! cp "$src" "$dst"; then
332
+ echo "ERROR: aid-install-core: copy failed: ${dst}" >&2
333
+ _COPY_COUNT_FAILED=$((_COPY_COUNT_FAILED + 1))
334
+ return 0
335
+ fi
319
336
  _COPY_COUNT_COPIED=$((_COPY_COUNT_COPIED + 1))
320
337
  [[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Copied: ${dst}"
321
338
  return 0
@@ -327,15 +344,21 @@ copy_file() {
327
344
  return 0
328
345
  fi
329
346
 
330
- # File exists and differs.
331
- if [[ "$force" -eq 1 ]]; then
332
- cp "$src" "$dst"
333
- _COPY_COUNT_UPDATED=$((_COPY_COUNT_UPDATED + 1))
334
- [[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Updated: ${dst}"
335
- else
336
- _COPY_COUNT_SKIPPED=$((_COPY_COUNT_SKIPPED + 1))
337
- [[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Skipped (differs; use --force): ${dst}"
347
+ # File exists and differs -> always overwrite (work-007).
348
+ # AID-owned files track the bundle, which is the source of truth, so an
349
+ # add/update must bring them current. `force` is retained in the signature
350
+ # for back-compat and bash<->PS parity but no longer gates this overwrite:
351
+ # skip-on-diff silently left stale files behind on in-place upgrades (e.g. an
352
+ # old flat-path skill surviving next to relocated .aid/ scripts). User-owned
353
+ # root agent files (CLAUDE.md/AGENTS.md) never reach copy_file -- they are
354
+ # handled by _copy_root_agent_file, which keeps its own force gating.
355
+ if ! cp "$src" "$dst"; then
356
+ echo "ERROR: aid-install-core: copy failed: ${dst}" >&2
357
+ _COPY_COUNT_FAILED=$((_COPY_COUNT_FAILED + 1))
358
+ return 0
338
359
  fi
360
+ _COPY_COUNT_UPDATED=$((_COPY_COUNT_UPDATED + 1))
361
+ [[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Updated: ${dst}"
339
362
  }
340
363
 
341
364
  # copy_dir <src_dir> <dst_dir> [force]
@@ -428,7 +451,11 @@ _copy_root_agent_file() {
428
451
  # then lines after the closing marker.
429
452
  local tmp
430
453
  tmp="$(mktemp "${dst_dir}/.aid-root-agent.XXXXXX")"
431
- awk -v region="$src_region" '
454
+ # awk -v applies C-escape processing to the value; double every backslash
455
+ # so a region containing "\" (or "\n"-like text) passes through literally
456
+ # (work-007 C7). No-op for the current backslash-free region.
457
+ local src_region_esc="${src_region//\\/\\\\}"
458
+ awk -v region="$src_region_esc" '
432
459
  BEGIN { in_aid=0; printed_region=0 }
433
460
  /^<!-- AID:BEGIN -->/ {
434
461
  if (!printed_region) {
@@ -471,11 +498,13 @@ _copy_root_agent_file() {
471
498
  # stem match and re-insert as a marked region.
472
499
  #
473
500
  # AID section stems to excise (exact heading stem; tolerate trailing
474
- # parenthetical like " (global)" or " (IMPERATIVE)"):
501
+ # parenthetical like " (global)" or " (IMPERATIVE)") -- MUST match every
502
+ # "## " heading in the region (see is_aid_heading):
503
+ # ## Tracking discipline
475
504
  # ## Knowledge Base
505
+ # ## Workflow
476
506
  # ## Review output format
477
507
  # ## Permissions
478
- # ## Tracking discipline
479
508
  #
480
509
  # A section runs from its "## Stem..." heading line until the next "## "
481
510
  # heading (exclusive) or end-of-file.
@@ -487,27 +516,34 @@ _copy_root_agent_file() {
487
516
  # Extract the new marked region from source.
488
517
  local new_region
489
518
  new_region="$(awk '/^<!-- AID:BEGIN -->/{found=1} found{print} /^<!-- AID:END -->/{if(found){exit}}' "$src")"
519
+ # awk -v applies C-escape processing; double backslashes so the region passes
520
+ # through literally (work-007 C7). No-op for the current backslash-free region.
521
+ local new_region_esc="${new_region//\\/\\\\}"
490
522
 
491
523
  local tmp
492
524
  tmp="$(mktemp "${dst_dir}/.aid-root-agent.XXXXXX")"
493
525
 
494
- # Use awk to perform the excise-and-reinsert in one pass.
526
+ # Use awk to perform the excise-and-reinsert in one pass (region passed
527
+ # backslash-doubled via new_region_esc -- see C7 note above).
495
528
  # The awk script:
496
529
  # - Identifies AID-managed section headings by stem prefix match.
497
530
  # - Suppresses those sections (and their body lines).
498
531
  # - At the position of the first suppressed section, emits the new region.
499
532
  # - All other lines are emitted verbatim.
500
- awk -v new_region="$new_region" '
533
+ awk -v new_region="$new_region_esc" '
501
534
  function is_aid_heading(line, stem) {
502
535
  # Match "## StemText" optionally followed by " (anything)".
503
- # Stems: Knowledge Base, Review output format, Permissions,
504
- # Tracking discipline.
536
+ # Stems must cover EVERY "## " heading inside the shipped AID:BEGIN/END
537
+ # region (Tracking discipline, Knowledge Base, Workflow, Review output
538
+ # format, Permissions) -- a stem missing here causes a duplicate section
539
+ # on the C2 (no-marker) migration path (work-007: Workflow was omitted).
505
540
  if (line !~ /^## /) return 0
506
541
  stem = line
507
542
  gsub(/^## /, "", stem)
508
543
  # Strip trailing parenthetical suffix: " (..." -> ""
509
544
  gsub(/ \([^)]*\)$/, "", stem)
510
545
  if (stem == "Knowledge Base") return 1
546
+ if (stem == "Workflow") return 1
511
547
  if (stem == "Review output format") return 1
512
548
  if (stem == "Permissions") return 1
513
549
  if (stem == "Tracking discipline") return 1
@@ -1881,6 +1917,133 @@ write_version_marker() {
1881
1917
  printf '%s\n' "$version" > "${target}/.aid/.aid-version"
1882
1918
  }
1883
1919
 
1920
+ # ---------------------------------------------------------------------------
1921
+ # Project-level provisioning (work-007): required runtime file + VCS hygiene.
1922
+ # Both are idempotent and safe to run once per tool during a multi-tool add.
1923
+ # ---------------------------------------------------------------------------
1924
+
1925
+ # seed_settings_yml <target> <tool>
1926
+ # Seed <target>/.aid/settings.yml from the tool's just-installed template when it
1927
+ # does not already exist. NEVER overwrites an existing settings.yml -- it holds
1928
+ # user config (project identity, grades). Not recorded in the manifest: it is
1929
+ # user data and must survive `aid remove`/prune and never flow through the copy
1930
+ # engine. Sets _CORE_SEEDED_SETTINGS=1 when a file was created, else 0.
1931
+ seed_settings_yml() {
1932
+ local target="$1" tool="$2"
1933
+ local root dst tmpl
1934
+ _CORE_SEEDED_SETTINGS=0
1935
+ root="$(_root_dir "$tool")"
1936
+ [[ -z "$root" ]] && return 0
1937
+ dst="${target}/.aid/settings.yml"
1938
+ tmpl="${target}/${root}/aid/templates/settings.yml"
1939
+ [[ -f "$dst" ]] && return 0 # never clobber user config
1940
+ # Template absent -> a tool bundle shipped without aid/templates/settings.yml.
1941
+ # Surface it (work-007 C5): a silent skip re-triggers the "no settings.yml"
1942
+ # class this seed exists to fix, and the format gate would then warn forever.
1943
+ if [[ ! -f "$tmpl" ]]; then
1944
+ echo "WARN: aid-install-core: settings template missing (${tmpl}); .aid/settings.yml not seeded for '${tool}'." >&2
1945
+ return 0
1946
+ fi
1947
+ mkdir -p "${target}/.aid"
1948
+ # Seed from the template, but STAMP the format_version as the first line so
1949
+ # the settings-format gate does not warn on every subsequent command (the raw
1950
+ # template ships unstamped; era-b synthesis stamps -- we match it here). The
1951
+ # value mirrors bin/aid's AID_SUPPORTED_FORMAT (fallback 1 for install.sh).
1952
+ local _fmt _seed_tmp
1953
+ _fmt="${AID_SUPPORTED_FORMAT:-1}"
1954
+ _seed_tmp="$(mktemp "${dst}.aid-tmp.XXXXXX")" || {
1955
+ echo "WARN: aid-install-core: could not create temp file to seed .aid/settings.yml for '${tool}'." >&2
1956
+ return 0
1957
+ }
1958
+ if grep -q '^format_version:' "$tmpl" 2>/dev/null; then
1959
+ cp "$tmpl" "$_seed_tmp"
1960
+ else
1961
+ { printf 'format_version: %s\n' "$_fmt"; cat "$tmpl"; } > "$_seed_tmp"
1962
+ fi
1963
+ if ! mv -f "$_seed_tmp" "$dst"; then
1964
+ rm -f "$_seed_tmp" 2>/dev/null
1965
+ echo "WARN: aid-install-core: could not write .aid/settings.yml for '${tool}'." >&2
1966
+ return 0
1967
+ fi
1968
+ _CORE_SEEDED_SETTINGS=1
1969
+ return 0
1970
+ }
1971
+
1972
+ # AID-managed .gitignore region markers. The lines BETWEEN the markers are owned
1973
+ # by the installer and rewritten on every add/update; everything OUTSIDE the
1974
+ # markers is preserved verbatim (mirrors the root-agent region-update pattern).
1975
+ _AID_GI_BEGIN="# >>> AID managed -- do not edit (aid add/update maintains this block) >>>"
1976
+ _AID_GI_END="# <<< AID managed <<<"
1977
+
1978
+ # _aid_gitignore_block - print the current AID-managed .gitignore block (markers
1979
+ # inclusive). The exclusion set is the single source of truth for transient
1980
+ # .aid/ artifacts that must never be committed.
1981
+ _aid_gitignore_block() {
1982
+ printf '%s\n' "$_AID_GI_BEGIN"
1983
+ printf '%s\n' \
1984
+ ".aid/.temp/" \
1985
+ ".aid/.trash/" \
1986
+ ".aid/.heartbeat/" \
1987
+ ".aid/generated/" \
1988
+ ".aid/knowledge/.cache/" \
1989
+ ".aid/knowledge/.manual-checklist.json" \
1990
+ ".aid/knowledge/.spot-check-facts.txt"
1991
+ printf '%s' "$_AID_GI_END"
1992
+ }
1993
+
1994
+ # update_gitignore <target>
1995
+ # Ensure <target>/.gitignore carries the AID-managed region with the current
1996
+ # transient .aid/ exclusions. Creates the file if absent; otherwise strips any
1997
+ # existing AID region, preserves all user content, and appends a fresh region
1998
+ # (with a single blank-line separator). Idempotent: a second run produces a
1999
+ # byte-identical file. Sets _CORE_GITIGNORE_ACTION to created|updated|unchanged.
2000
+ update_gitignore() {
2001
+ local target="$1"
2002
+ local gi="${target}/.gitignore"
2003
+ local block tmp
2004
+ _CORE_GITIGNORE_ACTION="unchanged"
2005
+ block="$(_aid_gitignore_block)"
2006
+
2007
+ if [[ ! -f "$gi" ]]; then
2008
+ printf '%s\n' "$block" > "$gi"
2009
+ _CORE_GITIGNORE_ACTION="created"
2010
+ return 0
2011
+ fi
2012
+
2013
+ # Normalize the existing file to LF for BOTH processing and comparison, so a
2014
+ # line-ending-only difference (a CRLF .gitignore) does NOT force a rewrite
2015
+ # every run. Parity with PS Update-AidGitignore, which compares CRLF->LF-
2016
+ # normalized content (work-007 C3). If unchanged, the original file is left
2017
+ # byte-untouched (CRLF preserved); only a real region change triggers a write.
2018
+ local norm
2019
+ norm="$(mktemp "${target}/.gitignore.norm.XXXXXX")"
2020
+ tr -d '\r' < "$gi" > "$norm"
2021
+
2022
+ tmp="$(mktemp "${target}/.gitignore.aid.XXXXXX")"
2023
+ awk -v b="$_AID_GI_BEGIN" -v e="$_AID_GI_END" -v blk="$block" '
2024
+ $0==b {inblk=1; next}
2025
+ inblk && $0==e {inblk=0; next}
2026
+ inblk {next}
2027
+ {lines[n++]=$0}
2028
+ END {
2029
+ while (n>0 && lines[n-1] ~ /^[ \t]*$/) n--
2030
+ for (i=0;i<n;i++) print lines[i]
2031
+ if (n>0) print ""
2032
+ print blk
2033
+ }
2034
+ ' "$norm" > "$tmp"
2035
+
2036
+ if cmp -s "$tmp" "$norm"; then
2037
+ rm -f "$tmp" "$norm"
2038
+ _CORE_GITIGNORE_ACTION="unchanged"
2039
+ else
2040
+ rm -f "$norm"
2041
+ mv -f "$tmp" "$gi"
2042
+ _CORE_GITIGNORE_ACTION="updated"
2043
+ fi
2044
+ return 0
2045
+ }
2046
+
1884
2047
  # ---------------------------------------------------------------------------
1885
2048
  # High-level install_tool
1886
2049
  # ---------------------------------------------------------------------------
@@ -1903,6 +2066,7 @@ install_tool() {
1903
2066
  _COPY_COUNT_UPTODATE=0
1904
2067
  _COPY_COUNT_UPDATED=0
1905
2068
  _COPY_COUNT_SKIPPED=0
2069
+ _COPY_COUNT_FAILED=0
1906
2070
 
1907
2071
  local root_agent
1908
2072
  root_agent="$(_root_agent_file "$tool")"
@@ -1963,6 +2127,13 @@ install_tool() {
1963
2127
  ;;
1964
2128
  esac
1965
2129
 
2130
+ # Abort loudly if any AID-owned file failed to copy -- do NOT proceed to
2131
+ # write a manifest that records a partial install as success (work-007 C4).
2132
+ if [[ "${_COPY_COUNT_FAILED:-0}" -gt 0 ]]; then
2133
+ echo "ERROR: aid-install-core: ${_COPY_COUNT_FAILED} file(s) failed to copy for '${tool}'. Install aborted (incomplete)." >&2
2134
+ return 1
2135
+ fi
2136
+
1966
2137
  # Handle root agent file via in-place region update (Pillar 3).
1967
2138
  local root_src="${staging}/${root_agent}"
1968
2139
  local root_dst="${target}/${root_agent}"
@@ -2002,8 +2173,13 @@ install_tool() {
2002
2173
  done
2003
2174
  unset _rr _leaked
2004
2175
 
2005
- # Write manifest (merge).
2006
- manifest_write "$manifest" "$tool" "$version" "install_paths" "root_entries"
2176
+ # Write manifest (merge). A failure here MUST abort loudly (parity with PS
2177
+ # Write-AidManifest, which throws) -- otherwise a failed manifest write is a
2178
+ # silent success and later `aid status`/`aid remove`/prune won't see the tool.
2179
+ if ! manifest_write "$manifest" "$tool" "$version" "install_paths" "root_entries"; then
2180
+ echo "ERROR: aid-install-core: manifest write failed for ${manifest}. Install aborted (files copied but not recorded)." >&2
2181
+ return 1
2182
+ fi
2007
2183
 
2008
2184
  # Retired-root migration sweep (FR7/FR7a).
2009
2185
  # Remove AID-owned content from retired layout dirs BEFORE the normal prune,
@@ -2025,6 +2201,11 @@ install_tool() {
2025
2201
  # Write version marker.
2026
2202
  write_version_marker "$target" "$version"
2027
2203
 
2204
+ # Project-level provisioning (work-007): seed the required settings.yml and
2205
+ # maintain the .gitignore AID region. Both idempotent; safe per-tool.
2206
+ seed_settings_yml "$target" "$tool"
2207
+ update_gitignore "$target"
2208
+
2028
2209
  # Print concise summary (always shown; per-file lines only when AID_VERBOSE=1).
2029
2210
  local _total_files=$((_COPY_COUNT_COPIED + _COPY_COUNT_UPTODATE + _COPY_COUNT_UPDATED + _COPY_COUNT_SKIPPED))
2030
2211
  if [[ "$_total_files" -gt 0 ]]; then
@@ -2047,6 +2228,13 @@ install_tool() {
2047
2228
  fi
2048
2229
  fi
2049
2230
 
2231
+ # Report project-level provisioning actions (work-007; always shown).
2232
+ [[ "${_CORE_SEEDED_SETTINGS:-0}" -eq 1 ]] && echo " Created .aid/settings.yml from template"
2233
+ case "${_CORE_GITIGNORE_ACTION:-unchanged}" in
2234
+ created) echo " Created .gitignore with AID exclusions" ;;
2235
+ updated) echo " Updated .gitignore AID exclusions" ;;
2236
+ esac
2237
+
2050
2238
  return 0
2051
2239
  }
2052
2240
 
@@ -2143,14 +2331,17 @@ uninstall_tool() {
2143
2331
  # Remove this tool from manifest.
2144
2332
  manifest_remove_tool "$manifest" "$tool"
2145
2333
 
2146
- # If no manifest remains, remove the .aid version marker too.
2334
+ # If no manifest remains (last tool removed), remove the AID-provisioned
2335
+ # project files so a full uninstall leaves .aid/ clean (work-007).
2147
2336
  if [[ ! -f "$manifest" ]]; then
2148
- local version_marker
2149
- version_marker="$(dirname "$manifest")/.aid-version"
2150
- rm -f "$version_marker"
2151
- # Remove .aid dir if empty.
2152
2337
  local aid_meta_dir
2153
2338
  aid_meta_dir="$(dirname "$manifest")"
2339
+ rm -f "${aid_meta_dir}/.aid-version"
2340
+ # Remove the install-time-seeded settings.yml (symmetric with seed_settings_yml).
2341
+ # Only fires when NO tools remain -- a partial uninstall keeps settings.yml
2342
+ # for the remaining tools.
2343
+ rm -f "${aid_meta_dir}/settings.yml"
2344
+ # Remove .aid dir if empty.
2154
2345
  if [[ -d "$aid_meta_dir" ]]; then
2155
2346
  local rem
2156
2347
  rem="$(find "$aid_meta_dir" -type f 2>/dev/null | head -1)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aid-installer",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "AID (AI Integrated Development) CLI installer - puts the persistent aid command on PATH via npm",
5
5
  "author": "Andre Vianna",
6
6
  "homepage": "https://aid.casuloailabs.com",