aw-spec-cli 0.1.0 → 0.1.2

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.
Files changed (173) hide show
  1. package/assets/.agents/skills/aw-spec-addcomponent/SKILL.md +129 -0
  2. package/assets/.agents/skills/aw-spec-agent-context-update/SKILL.md +31 -0
  3. package/assets/.agents/skills/aw-spec-analyze/SKILL.md +296 -0
  4. package/assets/.agents/skills/aw-spec-checklist/SKILL.md +374 -0
  5. package/assets/.agents/skills/aw-spec-clarify/SKILL.md +320 -0
  6. package/assets/.agents/skills/aw-spec-constitution/SKILL.md +194 -0
  7. package/assets/.agents/skills/aw-spec-fast-implement/SKILL.md +268 -0
  8. package/assets/.agents/skills/aw-spec-foundation/SKILL.md +358 -0
  9. package/assets/.agents/skills/aw-spec-implement/SKILL.md +279 -0
  10. package/assets/.agents/skills/aw-spec-minimal-implementation/SKILL.md +209 -0
  11. package/assets/.agents/skills/aw-spec-plan/SKILL.md +221 -0
  12. package/assets/.agents/skills/aw-spec-select-domain/SKILL.md +73 -0
  13. package/assets/.agents/skills/aw-spec-specify/SKILL.md +382 -0
  14. package/assets/.agents/skills/aw-spec-tasks/SKILL.md +259 -0
  15. package/assets/.agents/skills/aw-spec-taskstoissues/SKILL.md +107 -0
  16. package/assets/.aw-spec/domain.json +3 -0
  17. package/assets/.aw-spec/extensions/.registry +19 -0
  18. package/assets/.aw-spec/extensions/agent-context/README.md +57 -0
  19. package/assets/.aw-spec/extensions/agent-context/agent-context-config.yml +4 -0
  20. package/assets/.aw-spec/extensions/agent-context/commands/aw-spec.agent-context.update.md +26 -0
  21. package/assets/.aw-spec/extensions/agent-context/extension.yml +34 -0
  22. package/assets/.aw-spec/extensions/agent-context/scripts/bash/update-agent-context.sh +201 -0
  23. package/assets/.aw-spec/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +237 -0
  24. package/assets/.aw-spec/extensions.yml +23 -0
  25. package/assets/.aw-spec/feature.json +3 -0
  26. package/assets/.aw-spec/init-options.json +9 -0
  27. package/assets/.aw-spec/integration.json +15 -0
  28. package/assets/.aw-spec/integrations/aw-spec.manifest.json +17 -0
  29. package/assets/.aw-spec/integrations/claude.manifest.json +17 -0
  30. package/assets/.aw-spec/memory/constitution.md +50 -0
  31. package/assets/.aw-spec/scripts/bash/check-prerequisites.sh +189 -0
  32. package/assets/.aw-spec/scripts/bash/common.sh +582 -0
  33. package/assets/.aw-spec/scripts/bash/create-new-feature.sh +299 -0
  34. package/assets/.aw-spec/scripts/bash/setup-plan.sh +84 -0
  35. package/assets/.aw-spec/scripts/bash/setup-tasks.sh +91 -0
  36. package/assets/.aw-spec/templates/checklist-template.md +40 -0
  37. package/assets/.aw-spec/templates/constitution-template.md +57 -0
  38. package/assets/.aw-spec/templates/plan-template.md +113 -0
  39. package/assets/.aw-spec/templates/spec-template.md +131 -0
  40. package/assets/.aw-spec/templates/tasks-template.md +252 -0
  41. package/assets/.aw-spec/workflows/aw-spec/workflow.yml +77 -0
  42. package/assets/.aw-spec/workflows/workflow-registry.json +13 -0
  43. package/assets/.claude/skills/aw-spec-addcomponent/SKILL.md +129 -0
  44. package/assets/.claude/skills/aw-spec-agent-context-update/SKILL.md +31 -0
  45. package/assets/.claude/skills/aw-spec-analyze/SKILL.md +296 -0
  46. package/assets/.claude/skills/aw-spec-checklist/SKILL.md +374 -0
  47. package/assets/.claude/skills/aw-spec-clarify/SKILL.md +320 -0
  48. package/assets/.claude/skills/aw-spec-constitution/SKILL.md +194 -0
  49. package/assets/.claude/skills/aw-spec-fast-implement/SKILL.md +268 -0
  50. package/assets/.claude/skills/aw-spec-foundation/SKILL.md +358 -0
  51. package/assets/.claude/skills/aw-spec-implement/SKILL.md +279 -0
  52. package/assets/.claude/skills/aw-spec-minimal-implementation/SKILL.md +209 -0
  53. package/assets/.claude/skills/aw-spec-plan/SKILL.md +221 -0
  54. package/assets/.claude/skills/aw-spec-select-domain/SKILL.md +73 -0
  55. package/assets/.claude/skills/aw-spec-specify/SKILL.md +382 -0
  56. package/assets/.claude/skills/aw-spec-tasks/SKILL.md +259 -0
  57. package/assets/.claude/skills/aw-spec-taskstoissues/SKILL.md +107 -0
  58. package/assets/.kilocode/rules/specify-rules.md +4 -0
  59. package/assets/.kilocode/workflows/aw-spec-addcomponent.md +129 -0
  60. package/assets/.kilocode/workflows/aw-spec-agent-context.update.md +31 -0
  61. package/assets/.kilocode/workflows/aw-spec-analyze.md +296 -0
  62. package/assets/.kilocode/workflows/aw-spec-checklist.md +374 -0
  63. package/assets/.kilocode/workflows/aw-spec-clarify.md +320 -0
  64. package/assets/.kilocode/workflows/aw-spec-constitution.md +194 -0
  65. package/assets/.kilocode/workflows/aw-spec-fast-implement.md +263 -0
  66. package/assets/.kilocode/workflows/aw-spec-foundation.md +358 -0
  67. package/assets/.kilocode/workflows/aw-spec-implement.md +279 -0
  68. package/assets/.kilocode/workflows/aw-spec-minimal-implementation.md +204 -0
  69. package/assets/.kilocode/workflows/aw-spec-plan.md +221 -0
  70. package/assets/.kilocode/workflows/aw-spec-select-domain.md +73 -0
  71. package/assets/.kilocode/workflows/aw-spec-specify.md +382 -0
  72. package/assets/.kilocode/workflows/aw-spec-tasks.md +259 -0
  73. package/assets/.kilocode/workflows/aw-spec-taskstoissues.md +107 -0
  74. package/assets/docs/README.md +106 -0
  75. package/assets/docs/VerticalSlice/PACKAGING.md +185 -0
  76. package/assets/docs/VerticalSlice/VerticalSlice.pack.txt +2833 -0
  77. package/assets/docs/VerticalSlice/pack-foundation.ps1 +145 -0
  78. package/assets/docs/VerticalSlice/pack-foundation.sh +196 -0
  79. package/assets/docs/VerticalSlice/unpack-foundation.ps1 +126 -0
  80. package/assets/docs/VerticalSlice/unpack-foundation.sh +217 -0
  81. package/assets/docs/architecture/clean-architecture.md +71 -0
  82. package/assets/docs/architecture/cqrs.md +141 -0
  83. package/assets/docs/architecture/eventing.md +83 -0
  84. package/assets/docs/architecture/folder-structure.md +61 -0
  85. package/assets/docs/architecture/modular-monolith.md +47 -0
  86. package/assets/docs/architecture/persistence.md +269 -0
  87. package/assets/docs/architecture/testing.md +55 -0
  88. package/assets/docs/architecture/vertical-slices.md +201 -0
  89. package/assets/docs/command.txt +295 -0
  90. package/assets/docs/components/Email/README.md +112 -0
  91. package/assets/docs/components/Email/integration.md +260 -0
  92. package/assets/docs/components/Jobs/README.md +113 -0
  93. package/assets/docs/components/Jobs/integration.md +245 -0
  94. package/assets/docs/components/QrGenerator/README.md +78 -0
  95. package/assets/docs/components/QrGenerator/integration.md +185 -0
  96. package/assets/docs/components/README.md +57 -0
  97. package/assets/docs/domain/domain-driven-design.md +212 -0
  98. package/assets/docs/domainspecs/healthcare/ai-rules.domain.md +34 -0
  99. package/assets/docs/domainspecs/healthcare/api-versioning.domain.md +17 -0
  100. package/assets/docs/domainspecs/healthcare/auditing.domain.md +29 -0
  101. package/assets/docs/domainspecs/healthcare/authorization.domain.md +26 -0
  102. package/assets/docs/domainspecs/healthcare/clinical-terms.md +20 -0
  103. package/assets/docs/domainspecs/healthcare/coding-standards.domain.md +26 -0
  104. package/assets/docs/domainspecs/healthcare/comandrulemap.md +119 -0
  105. package/assets/docs/domainspecs/healthcare/consent.md +19 -0
  106. package/assets/docs/domainspecs/healthcare/cqrs.domain.md +12 -0
  107. package/assets/docs/domainspecs/healthcare/data-classification.domain.md +28 -0
  108. package/assets/docs/domainspecs/healthcare/dependency-policy.domain.md +11 -0
  109. package/assets/docs/domainspecs/healthcare/dicom.md +15 -0
  110. package/assets/docs/domainspecs/healthcare/domain-driven-design.domain.md +28 -0
  111. package/assets/docs/domainspecs/healthcare/domain-requirements.md +55 -0
  112. package/assets/docs/domainspecs/healthcare/eventing.domain.md +18 -0
  113. package/assets/docs/domainspecs/healthcare/fhir.md +36 -0
  114. package/assets/docs/domainspecs/healthcare/folder-structure.domain.md +23 -0
  115. package/assets/docs/domainspecs/healthcare/hipaa.md +34 -0
  116. package/assets/docs/domainspecs/healthcare/hl7.md +15 -0
  117. package/assets/docs/domainspecs/healthcare/integration-terms.md +21 -0
  118. package/assets/docs/domainspecs/healthcare/persistence.domain.md +20 -0
  119. package/assets/docs/domainspecs/healthcare/terminology.md +30 -0
  120. package/assets/docs/domainspecs/healthcare/testing.domain.md +15 -0
  121. package/assets/docs/domainspecs/healthcare/vertical-slices.domain.md +18 -0
  122. package/assets/docs/domainspecs/healthcare/zero-trust.domain.md +12 -0
  123. package/assets/docs/domainspecs/pharma/ai-rules.domain.md +32 -0
  124. package/assets/docs/domainspecs/pharma/api-versioning.domain.md +20 -0
  125. package/assets/docs/domainspecs/pharma/atc.md +22 -0
  126. package/assets/docs/domainspecs/pharma/auditing.domain.md +26 -0
  127. package/assets/docs/domainspecs/pharma/authorization.domain.md +25 -0
  128. package/assets/docs/domainspecs/pharma/coding-standards.domain.md +28 -0
  129. package/assets/docs/domainspecs/pharma/comandrulemap.md +136 -0
  130. package/assets/docs/domainspecs/pharma/cqrs.domain.md +16 -0
  131. package/assets/docs/domainspecs/pharma/data-classification.domain.md +24 -0
  132. package/assets/docs/domainspecs/pharma/data-provenance.md +21 -0
  133. package/assets/docs/domainspecs/pharma/dependency-policy.domain.md +11 -0
  134. package/assets/docs/domainspecs/pharma/domain-driven-design.domain.md +36 -0
  135. package/assets/docs/domainspecs/pharma/domain-requirements.md +99 -0
  136. package/assets/docs/domainspecs/pharma/eventing.domain.md +18 -0
  137. package/assets/docs/domainspecs/pharma/fhir.md +31 -0
  138. package/assets/docs/domainspecs/pharma/folder-structure.domain.md +24 -0
  139. package/assets/docs/domainspecs/pharma/hl7.md +17 -0
  140. package/assets/docs/domainspecs/pharma/idmp.md +26 -0
  141. package/assets/docs/domainspecs/pharma/integration-terms.md +21 -0
  142. package/assets/docs/domainspecs/pharma/ndc.md +28 -0
  143. package/assets/docs/domainspecs/pharma/persistence.domain.md +23 -0
  144. package/assets/docs/domainspecs/pharma/pharma-terms.md +27 -0
  145. package/assets/docs/domainspecs/pharma/product-lifecycle.domain.md +30 -0
  146. package/assets/docs/domainspecs/pharma/regulatory-compliance.md +19 -0
  147. package/assets/docs/domainspecs/pharma/terminology.md +32 -0
  148. package/assets/docs/domainspecs/pharma/testing.domain.md +19 -0
  149. package/assets/docs/domainspecs/pharma/vertical-slices.domain.md +17 -0
  150. package/assets/docs/domainspecs/pharma/zero-trust.domain.md +12 -0
  151. package/assets/docs/glossary/ai-rules.md +59 -0
  152. package/assets/docs/governance/api-versioning.md +99 -0
  153. package/assets/docs/governance/coding-standards.md +87 -0
  154. package/assets/docs/governance/dependency-policy.md +40 -0
  155. package/assets/docs/interop/ihe-conformance.md +31 -0
  156. package/assets/docs/pharma-constitution.my-ref.md +86 -0
  157. package/assets/docs/requirement-spec.txt +15 -0
  158. package/assets/docs/security/auditing.md +117 -0
  159. package/assets/docs/security/authorization-matrix.md +14 -0
  160. package/assets/docs/security/authorization.md +87 -0
  161. package/assets/docs/security/data-classification.md +38 -0
  162. package/assets/docs/security/zero-trust.md +76 -0
  163. package/assets/docs/technology-constitution.md +172 -0
  164. package/dist/commands/init.d.ts.map +1 -1
  165. package/dist/commands/init.js +3 -2
  166. package/dist/commands/init.js.map +1 -1
  167. package/dist/utils/copy-assets.d.ts +8 -1
  168. package/dist/utils/copy-assets.d.ts.map +1 -1
  169. package/dist/utils/copy-assets.js +1 -0
  170. package/dist/utils/copy-assets.js.map +1 -1
  171. package/dist/utils/paths.js +1 -1
  172. package/dist/utils/paths.js.map +1 -1
  173. package/package.json +1 -1
@@ -0,0 +1,145 @@
1
+ #requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ Pack a verified foundation solution into a single AW-Spec text pack file.
5
+
6
+ .DESCRIPTION
7
+ Walks -SourceDir, excludes build/IDE artifacts, and writes every remaining file
8
+ into one pack file using <<<FILE>>> / <<<ENDFILE>>> markers. Text files are stored
9
+ verbatim (UTF-8, LF); binary files are base64-encoded. The resulting pack file is
10
+ the single bundled artifact consumed by /aw-spec-foundation (see PACKAGING.md).
11
+
12
+ .EXAMPLE
13
+ ./pack-foundation.ps1 -SourceDir ../../.foundation-build -OutFile ./VerticalSlice.pack.txt
14
+ #>
15
+ [CmdletBinding()]
16
+ param(
17
+ [Parameter(Mandatory)] [string] $SourceDir,
18
+ [string] $OutFile,
19
+ [string] $Sentinel = 'AppWeaver.Foundation',
20
+ [string] $Foundation = 'VerticalSlice',
21
+ [switch] $AllowSecrets
22
+ )
23
+
24
+ $ErrorActionPreference = 'Stop'
25
+
26
+ $src = (Resolve-Path -LiteralPath $SourceDir).Path
27
+ if (-not (Test-Path -LiteralPath $src -PathType Container)) {
28
+ throw "SourceDir is not a folder: $SourceDir"
29
+ }
30
+ if (-not $OutFile) { $OutFile = Join-Path (Get-Location).Path "$Foundation.pack.txt" }
31
+
32
+ # Entire subtree excluded if any path segment matches one of these
33
+ $excludeDirs = @('bin','obj','.vs','.git','.idea','.vscode','TestResults','node_modules','packages','App_Data','artifacts','.next','logs')
34
+ # Files excluded by name glob (build noise, local dbs, and secret-bearing files)
35
+ # NOTE: the root '.env' is intentionally NOT excluded — it carries required sample config
36
+ # and is bundled on purpose. Per-environment overrides ('.env.local', '.env.production', ...)
37
+ # stay excluded via the '.env.*' glob. Real credentials are still caught by the content scan.
38
+ $excludeFiles = @(
39
+ '*.user','*.suo','*.DotSettings.user','*.db','*.db-shm','*.db-wal','*.sqlite','*.log',
40
+ '.env.*','*.pfx','*.pem','*.key','*.snk','id_rsa','id_rsa.pub','secrets.json'
41
+ )
42
+
43
+ # Content patterns that indicate a real secret; packing aborts on a hit unless -AllowSecrets.
44
+ $secretPatterns = @(
45
+ @{ Name = 'NuGet ClearTextPassword'; Rx = 'ClearTextPassword' }
46
+ @{ Name = 'Private key block'; Rx = 'BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY' }
47
+ @{ Name = 'AWS access key id'; Rx = 'AKIA[0-9A-Z]{16}' }
48
+ @{ Name = 'Azure DevOps PAT-ish'; Rx = '[A-Za-z0-9]{52}AZDO[A-Za-z0-9]' }
49
+ )
50
+
51
+ function Test-Excluded([string] $relPath) {
52
+ foreach ($seg in ($relPath -split '[\\/]')) {
53
+ if ($excludeDirs -contains $seg) { return $true }
54
+ }
55
+ $leaf = Split-Path $relPath -Leaf
56
+ foreach ($g in $excludeFiles) { if ($leaf -like $g) { return $true } }
57
+ return $false
58
+ }
59
+
60
+ function Test-Binary([byte[]] $bytes) {
61
+ $n = [Math]::Min(8000, $bytes.Length)
62
+ for ($i = 0; $i -lt $n; $i++) { if ($bytes[$i] -eq 0) { return $true } }
63
+ return $false
64
+ }
65
+
66
+ # Collect + filter
67
+ $picked = New-Object System.Collections.Generic.List[object]
68
+ foreach ($f in (Get-ChildItem -LiteralPath $src -Recurse -File -Force)) {
69
+ $rel = ($f.FullName.Substring($src.Length).TrimStart('\','/')) -replace '\\','/'
70
+ if (Test-Excluded $rel) { continue }
71
+ $picked.Add([pscustomobject]@{ Rel = $rel; Full = $f.FullName })
72
+ }
73
+ $picked = @($picked | Sort-Object Rel)
74
+ if ($picked.Count -eq 0) { throw "No files to pack under: $src" }
75
+
76
+ # Secret scan — fail closed so credentials never get baked into the committed bundle
77
+ $findings = New-Object System.Collections.Generic.List[string]
78
+ foreach ($p in $picked) {
79
+ try {
80
+ $t = [System.IO.File]::ReadAllText($p.Full)
81
+ } catch { continue }
82
+ foreach ($pat in $secretPatterns) {
83
+ if ($t -match $pat.Rx) { $findings.Add(" $($p.Rel) -> $($pat.Name)") }
84
+ }
85
+ }
86
+ if ($findings.Count -gt 0) {
87
+ $msg = "Potential secret(s) detected — refusing to pack (rotate + remove them, or pass -AllowSecrets):`n" + ($findings -join "`n")
88
+ if ($AllowSecrets) { Write-Warning $msg } else { throw $msg }
89
+ }
90
+
91
+ # Sentinel sanity check
92
+ $hits = 0
93
+ foreach ($p in $picked) {
94
+ try {
95
+ $t = [System.IO.File]::ReadAllText($p.Full)
96
+ if ($t.Contains($Sentinel)) { $hits++ }
97
+ } catch { }
98
+ }
99
+ if ($hits -eq 0) {
100
+ Write-Warning "Sentinel '$Sentinel' not found in any file. Did you build the sample under that exact name?"
101
+ } else {
102
+ Write-Host "Sentinel '$Sentinel' found in $hits file(s)."
103
+ }
104
+
105
+ # Build the pack (explicit LF newlines throughout)
106
+ $sb = [System.Text.StringBuilder]::new()
107
+ function AddLine([string] $s) { [void]$sb.Append($s); [void]$sb.Append("`n") }
108
+
109
+ AddLine '# AW-SPEC FOUNDATION PACK'
110
+ AddLine "# foundation: $Foundation"
111
+ AddLine "# sentinel: $Sentinel"
112
+ AddLine "# generated: $([DateTime]::UtcNow.ToString('o'))"
113
+ AddLine "# files: $($picked.Count)"
114
+ AddLine '# format: aw-spec-pack/v1'
115
+ AddLine '# --- replacement contract (ordinal, case-sensitive, longest-first) ---'
116
+ AddLine '# <Sentinel> -> <AppName> (dotted: namespaces, csproj/sln, refs)'
117
+ AddLine '# <Sentinel _-form> -> <AppName _-form> (underscored: Aspire project class names)'
118
+ AddLine '# <sentinel --form> -> <appname --form> (lower-hyphen: UserSecretsId, db/containers)'
119
+ AddLine '# <sentinel .-form> -> <appname lower-dot> (lower-dot: lowercased identifiers)'
120
+ AddLine '# Applied to file CONTENTS and file/dir PATHS. Never touch other AppWeaver.* tokens.'
121
+ AddLine '# ============================================================'
122
+ AddLine ''
123
+
124
+ foreach ($p in $picked) {
125
+ $bytes = [System.IO.File]::ReadAllBytes($p.Full)
126
+ if (Test-Binary $bytes) {
127
+ AddLine "<<<FILE path=`"$($p.Rel)`" encoding=`"base64`">>>"
128
+ AddLine ([Convert]::ToBase64String($bytes))
129
+ AddLine '<<<ENDFILE>>>'
130
+ } else {
131
+ $text = ([System.IO.File]::ReadAllText($p.Full)) -replace "`r`n", "`n"
132
+ AddLine "<<<FILE path=`"$($p.Rel)`">>>"
133
+ [void]$sb.Append($text)
134
+ if (-not $text.EndsWith("`n")) { [void]$sb.Append("`n") }
135
+ AddLine '<<<ENDFILE>>>'
136
+ }
137
+ }
138
+
139
+ $outDir = Split-Path -Parent $OutFile
140
+ if ($outDir -and -not (Test-Path -LiteralPath $outDir)) {
141
+ New-Item -ItemType Directory -Force -Path $outDir | Out-Null
142
+ }
143
+ [System.IO.File]::WriteAllText($OutFile, $sb.ToString(), [System.Text.UTF8Encoding]::new($false))
144
+
145
+ Write-Host "Packed $($picked.Count) file(s) -> $OutFile"
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # pack-foundation.sh — bash/macOS+Unix port of pack-foundation.ps1
4
+ #
5
+ # SYNOPSIS
6
+ # Pack a verified foundation solution into a single AW-Spec text pack file.
7
+ #
8
+ # DESCRIPTION
9
+ # Walks --source-dir, excludes build/IDE artifacts and secret-bearing files, and
10
+ # writes every remaining file into one pack file using <<<FILE>>> / <<<ENDFILE>>>
11
+ # markers. Text files are stored verbatim (UTF-8, LF); binary files are
12
+ # base64-encoded. The resulting pack file is the single bundled artifact consumed
13
+ # by /aw-spec-foundation (see PACKAGING.md). This is byte-for-byte format-compatible
14
+ # with pack-foundation.ps1 / unpack-foundation.ps1.
15
+ #
16
+ # EXAMPLE
17
+ # ./pack-foundation.sh --source-dir ../../.foundation-build --out-file ./VerticalSlice.pack.txt --allow-secrets
18
+ #
19
+ set -euo pipefail
20
+
21
+ # ------------------------------------------------------------------ defaults ---
22
+ SENTINEL='AppWeaver.Foundation'
23
+ FOUNDATION='VerticalSlice'
24
+ SOURCE_DIR=''
25
+ OUT_FILE=''
26
+ ALLOW_SECRETS=0
27
+
28
+ usage() {
29
+ cat >&2 <<'EOF'
30
+ Usage: pack-foundation.sh --source-dir <dir> [--out-file <file>] [--sentinel <name>]
31
+ [--foundation <name>] [--allow-secrets]
32
+
33
+ -s, --source-dir <dir> Folder holding the foundation solution (required)
34
+ -o, --out-file <file> Output pack file (default: ./<foundation>.pack.txt)
35
+ --sentinel <name> Sentinel app name (default: AppWeaver.Foundation)
36
+ --foundation <name> Foundation name (default: VerticalSlice)
37
+ --allow-secrets Pack even if credential markers are detected
38
+ -h, --help Show this help
39
+
40
+ Positional fallback: pack-foundation.sh <source-dir> [out-file]
41
+ EOF
42
+ }
43
+
44
+ # -------------------------------------------------------------- arg parsing ---
45
+ while [ $# -gt 0 ]; do
46
+ case "$1" in
47
+ -s|--source-dir) SOURCE_DIR="${2:?--source-dir needs a value}"; shift 2 ;;
48
+ -o|--out-file) OUT_FILE="${2:?--out-file needs a value}"; shift 2 ;;
49
+ --sentinel) SENTINEL="${2:?--sentinel needs a value}"; shift 2 ;;
50
+ --foundation) FOUNDATION="${2:?--foundation needs a value}"; shift 2 ;;
51
+ --allow-secrets) ALLOW_SECRETS=1; shift ;;
52
+ -h|--help) usage; exit 0 ;;
53
+ --) shift; break ;;
54
+ -*) echo "Error: unknown option: $1" >&2; usage; exit 2 ;;
55
+ *)
56
+ if [ -z "$SOURCE_DIR" ]; then SOURCE_DIR="$1"
57
+ elif [ -z "$OUT_FILE" ]; then OUT_FILE="$1"
58
+ else echo "Error: unexpected argument: $1" >&2; usage; exit 2
59
+ fi
60
+ shift ;;
61
+ esac
62
+ done
63
+
64
+ [ -n "$SOURCE_DIR" ] || { echo "Error: --source-dir is required." >&2; usage; exit 2; }
65
+ [ -d "$SOURCE_DIR" ] || { echo "Error: SourceDir is not a folder: $SOURCE_DIR" >&2; exit 1; }
66
+
67
+ SRC="$(cd "$SOURCE_DIR" && pwd -P)"
68
+ [ -n "$OUT_FILE" ] || OUT_FILE="$(pwd)/$FOUNDATION.pack.txt"
69
+
70
+ # -------------------------------------------------------------- exclusions ---
71
+ # Entire subtree excluded if any path segment matches one of these.
72
+ EXCLUDE_DIRS=(bin obj .vs .git .idea .vscode TestResults node_modules packages App_Data artifacts .next logs)
73
+ # Files excluded by leaf-name glob (build noise, local dbs, and secret-bearing files).
74
+ # NOTE: the root `.env` is intentionally NOT excluded — it carries required sample config
75
+ # and is bundled on purpose. Per-environment overrides (`.env.local`, `.env.production`, ...)
76
+ # stay excluded via the `.env.*` glob. Real credentials are still caught by the content scan.
77
+ EXCLUDE_FILES=('*.user' '*.suo' '*.DotSettings.user' '*.db' '*.db-shm' '*.db-wal' '*.sqlite' '*.log' \
78
+ '.env.*' '*.pfx' '*.pem' '*.key' '*.snk' 'id_rsa' 'id_rsa.pub' 'secrets.json')
79
+
80
+ # Content patterns that indicate a real secret; packing aborts on a hit unless --allow-secrets.
81
+ SECRET_NAMES=('NuGet ClearTextPassword' 'Private key block' 'AWS access key id' 'Azure DevOps PAT-ish')
82
+ SECRET_RX=('ClearTextPassword' \
83
+ 'BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY' \
84
+ 'AKIA[0-9A-Z]{16}' \
85
+ '[A-Za-z0-9]{52}AZDO[A-Za-z0-9]')
86
+
87
+ is_excluded() { # $1 = repo-relative path with '/' separators
88
+ local rel="$1" seg leaf g
89
+ local -a segs
90
+ IFS='/' read -ra segs <<< "$rel"
91
+ for seg in "${segs[@]}"; do
92
+ for g in "${EXCLUDE_DIRS[@]}"; do
93
+ [ "$seg" = "$g" ] && return 0
94
+ done
95
+ done
96
+ leaf="${rel##*/}"
97
+ for g in "${EXCLUDE_FILES[@]}"; do
98
+ # shellcheck disable=SC2254 # intentional glob match
99
+ case "$leaf" in $g) return 0 ;; esac
100
+ done
101
+ return 1
102
+ }
103
+
104
+ is_binary() { # $1 = file path; true if a NUL byte appears in the first 8000 bytes
105
+ local f="$1" n1 n2
106
+ n1="$(head -c 8000 "$f" | wc -c | tr -d '[:space:]')"
107
+ n2="$(head -c 8000 "$f" | LC_ALL=C tr -d '\000' | wc -c | tr -d '[:space:]')"
108
+ [ "$n1" != "$n2" ]
109
+ }
110
+
111
+ # ----------------------------------------------------------- collect + sort ---
112
+ picked=()
113
+ while IFS= read -r full; do
114
+ rel="${full#"$SRC"/}"
115
+ is_excluded "$rel" && continue
116
+ picked+=("$rel")
117
+ done < <(find "$SRC" -type f | LC_ALL=C sort)
118
+
119
+ [ "${#picked[@]}" -gt 0 ] || { echo "Error: No files to pack under: $SRC" >&2; exit 1; }
120
+
121
+ # ----------------------------------------------------------- secret scan -----
122
+ # Fail closed so credentials never get baked into the committed bundle.
123
+ findings=()
124
+ for rel in "${picked[@]}"; do
125
+ f="$SRC/$rel"
126
+ for i in "${!SECRET_RX[@]}"; do
127
+ if LC_ALL=C grep -aEq -- "${SECRET_RX[$i]}" "$f" 2>/dev/null; then
128
+ findings+=(" $rel -> ${SECRET_NAMES[$i]}")
129
+ fi
130
+ done
131
+ done
132
+ if [ "${#findings[@]}" -gt 0 ]; then
133
+ if [ "$ALLOW_SECRETS" -eq 1 ]; then
134
+ echo "WARNING: Potential secret(s) detected — packing anyway (--allow-secrets):" >&2
135
+ printf '%s\n' "${findings[@]}" >&2
136
+ else
137
+ echo "Error: Potential secret(s) detected — refusing to pack (rotate + remove them, or pass --allow-secrets):" >&2
138
+ printf '%s\n' "${findings[@]}" >&2
139
+ exit 1
140
+ fi
141
+ fi
142
+
143
+ # ----------------------------------------------------------- sentinel check --
144
+ hits=0
145
+ for rel in "${picked[@]}"; do
146
+ if LC_ALL=C grep -aFq -- "$SENTINEL" "$SRC/$rel" 2>/dev/null; then
147
+ hits=$((hits + 1))
148
+ fi
149
+ done
150
+ if [ "$hits" -eq 0 ]; then
151
+ echo "WARNING: Sentinel '$SENTINEL' not found in any file. Did you build the sample under that exact name?" >&2
152
+ else
153
+ echo "Sentinel '$SENTINEL' found in $hits file(s)."
154
+ fi
155
+
156
+ # ----------------------------------------------------------- write the pack --
157
+ outdir="$(dirname "$OUT_FILE")"
158
+ [ -d "$outdir" ] || mkdir -p "$outdir"
159
+
160
+ {
161
+ printf '%s\n' '# AW-SPEC FOUNDATION PACK'
162
+ printf '# foundation: %s\n' "$FOUNDATION"
163
+ printf '# sentinel: %s\n' "$SENTINEL"
164
+ printf '# generated: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%S.0000000Z)"
165
+ printf '# files: %s\n' "${#picked[@]}"
166
+ printf '%s\n' '# format: aw-spec-pack/v1'
167
+ printf '%s\n' '# --- replacement contract (ordinal, case-sensitive, longest-first) ---'
168
+ printf '%s\n' '# <Sentinel> -> <AppName> (dotted: namespaces, csproj/sln, refs)'
169
+ printf '%s\n' '# <Sentinel _-form> -> <AppName _-form> (underscored: Aspire project class names)'
170
+ printf '%s\n' '# <sentinel --form> -> <appname --form> (lower-hyphen: UserSecretsId, db/containers)'
171
+ printf '%s\n' '# <sentinel .-form> -> <appname lower-dot> (lower-dot: lowercased identifiers)'
172
+ printf '%s\n' '# Applied to file CONTENTS and file/dir PATHS. Never touch other AppWeaver.* tokens.'
173
+ printf '%s\n' '# ============================================================'
174
+ printf '\n'
175
+ } > "$OUT_FILE"
176
+
177
+ for rel in "${picked[@]}"; do
178
+ f="$SRC/$rel"
179
+ if is_binary "$f"; then
180
+ {
181
+ printf '<<<FILE path="%s" encoding="base64">>>\n' "$rel"
182
+ base64 < "$f" | tr -d '\n'
183
+ printf '\n'
184
+ printf '%s\n' '<<<ENDFILE>>>'
185
+ } >> "$OUT_FILE"
186
+ else
187
+ {
188
+ printf '<<<FILE path="%s">>>\n' "$rel"
189
+ # CRLF -> LF, and guarantee the content ends with exactly one trailing newline.
190
+ awk '{ sub(/\r$/, ""); print }' "$f"
191
+ printf '%s\n' '<<<ENDFILE>>>'
192
+ } >> "$OUT_FILE"
193
+ fi
194
+ done
195
+
196
+ echo "Packed ${#picked[@]} file(s) -> $OUT_FILE"
@@ -0,0 +1,126 @@
1
+ #requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ Unpack an AW-Spec foundation pack file into a target application, renaming the
5
+ sentinel app name to the caller's app name.
6
+
7
+ .DESCRIPTION
8
+ Reads -PackFile, takes the sentinel from its header (override with -Sentinel),
9
+ derives four case forms of the target app name, and writes every file block to
10
+ -OutDir with the sentinel rewritten in both file CONTENTS and file/dir PATHS.
11
+ Text blocks are rewritten and written as UTF-8; base64 blocks are decoded as-is.
12
+ Refuses to overwrite existing files unless -Force.
13
+
14
+ .EXAMPLE
15
+ ./unpack-foundation.ps1 -PackFile ./VerticalSlice.pack.txt -TargetAppName AppWeaver.Health -OutDir ../../
16
+ #>
17
+ [CmdletBinding()]
18
+ param(
19
+ [Parameter(Mandatory)] [string] $PackFile,
20
+ [Parameter(Mandatory)] [string] $TargetAppName,
21
+ [string] $OutDir = '.',
22
+ [string] $Sentinel,
23
+ [switch] $Force,
24
+ [switch] $DryRun
25
+ )
26
+
27
+ $ErrorActionPreference = 'Stop'
28
+
29
+ $pack = (Resolve-Path -LiteralPath $PackFile).Path
30
+ $lines = ([System.IO.File]::ReadAllText($pack)) -split "`r?`n"
31
+
32
+ # --- sentinel from header unless overridden ---
33
+ if (-not $Sentinel) {
34
+ foreach ($l in $lines) {
35
+ if ($l -match '^#\s*sentinel:\s*(.+?)\s*$') { $Sentinel = $Matches[1]; break }
36
+ }
37
+ }
38
+ if (-not $Sentinel) { throw "Sentinel not found in pack header; supply it with -Sentinel." }
39
+
40
+ # --- derive case forms ---
41
+ function Conv-Underscore ([string] $s) { $s -replace '\.','_' }
42
+ function Conv-LowerHyphen ([string] $s) { ($s.ToLowerInvariant()) -replace '\.','-' }
43
+ function Conv-LowerDot ([string] $s) { $s.ToLowerInvariant() }
44
+
45
+ # NOTE: do NOT use a hashtable here — PowerShell hashtable keys are case-INSENSITIVE,
46
+ # which would collapse the dotted ("AppWeaver.Foundation") and lower-dot
47
+ # ("appweaver.foundation") forms into one entry and drop the namespace rename.
48
+ $forms = @(
49
+ @{ From = $Sentinel; To = $TargetAppName }
50
+ @{ From = (Conv-Underscore $Sentinel); To = (Conv-Underscore $TargetAppName) }
51
+ @{ From = (Conv-LowerHyphen $Sentinel); To = (Conv-LowerHyphen $TargetAppName) }
52
+ @{ From = (Conv-LowerDot $Sentinel); To = (Conv-LowerDot $TargetAppName) }
53
+ )
54
+ $replacements = @()
55
+ $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal)
56
+ foreach ($pair in $forms) {
57
+ $from = [string]$pair.From
58
+ $to = [string]$pair.To
59
+ if ([string]::IsNullOrEmpty($from)) { continue }
60
+ if ($from -ceq $to) { continue } # -ceq: case-sensitive no-op check
61
+ if (-not $seen.Add($from)) { continue } # ordinal dedupe of identical From tokens
62
+ $replacements += [pscustomobject]@{ From = $from; To = $to }
63
+ }
64
+ # longest token first so overlaps resolve correctly
65
+ $replacements = @($replacements | Sort-Object { $_.From.Length } -Descending)
66
+
67
+ function Apply-Repl([string] $text) {
68
+ foreach ($r in $replacements) { $text = $text.Replace($r.From, $r.To) }
69
+ return $text
70
+ }
71
+
72
+ # --- output root ---
73
+ if (-not (Test-Path -LiteralPath $OutDir)) { New-Item -ItemType Directory -Force -Path $OutDir | Out-Null }
74
+ $outRoot = (Resolve-Path -LiteralPath $OutDir).Path
75
+ $sep = [System.IO.Path]::DirectorySeparatorChar
76
+
77
+ # --- parse file blocks ---
78
+ $files = @()
79
+ $i = 0
80
+ while ($i -lt $lines.Count) {
81
+ if ($lines[$i] -match '^<<<FILE path="(.+?)"(?:\s+encoding="(\w+)")?>>>\s*$') {
82
+ $rel = $Matches[1]; $enc = $Matches[2]
83
+ $i++
84
+ $buf = New-Object System.Collections.Generic.List[string]
85
+ while ($i -lt $lines.Count -and $lines[$i] -ne '<<<ENDFILE>>>') {
86
+ $buf.Add($lines[$i]); $i++
87
+ }
88
+ $files += [pscustomobject]@{ Rel = $rel; Enc = $enc; Content = ($buf -join "`n") }
89
+ }
90
+ $i++
91
+ }
92
+ if ($files.Count -eq 0) { throw "No <<<FILE>>> blocks found in pack: $pack" }
93
+
94
+ function Resolve-Dest([string] $rel) {
95
+ $renamed = (Apply-Repl $rel).Replace('/', $sep)
96
+ return (Join-Path $outRoot $renamed)
97
+ }
98
+
99
+ # --- conflict guard ---
100
+ $conflicts = foreach ($f in $files) { $d = Resolve-Dest $f.Rel; if (Test-Path -LiteralPath $d) { $d } }
101
+ if ($conflicts -and -not $Force) {
102
+ throw ("Refusing to overwrite existing files (re-run with -Force):`n " + ($conflicts -join "`n "))
103
+ }
104
+
105
+ # --- write ---
106
+ $written = 0
107
+ foreach ($f in $files) {
108
+ $dest = Resolve-Dest $f.Rel
109
+ $destDir = Split-Path -Parent $dest
110
+ if ($DryRun) { Write-Host "WOULD WRITE $($f.Rel) -> $dest"; continue }
111
+ if ($destDir -and -not (Test-Path -LiteralPath $destDir)) {
112
+ New-Item -ItemType Directory -Force -Path $destDir | Out-Null
113
+ }
114
+ if ($f.Enc -eq 'base64') {
115
+ [System.IO.File]::WriteAllBytes($dest, [Convert]::FromBase64String(($f.Content -replace '\s','')))
116
+ } else {
117
+ [System.IO.File]::WriteAllText($dest, (Apply-Repl $f.Content), [System.Text.UTF8Encoding]::new($false))
118
+ }
119
+ $written++
120
+ }
121
+
122
+ if ($DryRun) {
123
+ Write-Host "DryRun: $($files.Count) file(s) would be written to $outRoot (sentinel '$Sentinel' -> '$TargetAppName')."
124
+ } else {
125
+ Write-Host "Unpacked $written file(s) to $outRoot (sentinel '$Sentinel' -> '$TargetAppName')."
126
+ }
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # unpack-foundation.sh — bash/macOS+Unix port of unpack-foundation.ps1
4
+ #
5
+ # SYNOPSIS
6
+ # Unpack an AW-Spec foundation pack file into a target application, renaming the
7
+ # sentinel app name to the caller's app name.
8
+ #
9
+ # DESCRIPTION
10
+ # Reads --pack-file, takes the sentinel from its header (override with --sentinel),
11
+ # derives four case forms of the target app name, and writes every file block to
12
+ # --out-dir with the sentinel rewritten in both file CONTENTS and file/dir PATHS.
13
+ # Text blocks are rewritten and written as UTF-8; base64 blocks are decoded as-is.
14
+ # Refuses to overwrite existing files unless --force. Reads packs produced by either
15
+ # pack-foundation.sh or pack-foundation.ps1.
16
+ #
17
+ # EXAMPLE
18
+ # ./unpack-foundation.sh --pack-file ./VerticalSlice.pack.txt --target-app-name AppWeaver.Health --out-dir ../../
19
+ #
20
+ set -euo pipefail
21
+
22
+ # ------------------------------------------------------------------ defaults ---
23
+ PACK_FILE=''
24
+ TARGET_APP_NAME=''
25
+ OUT_DIR='.'
26
+ SENTINEL=''
27
+ FORCE=0
28
+ DRYRUN=0
29
+
30
+ usage() {
31
+ cat >&2 <<'EOF'
32
+ Usage: unpack-foundation.sh --pack-file <file> --target-app-name <name>
33
+ [--out-dir <dir>] [--sentinel <name>] [--force] [--dry-run]
34
+
35
+ -p, --pack-file <file> Pack file to unpack (required)
36
+ -t, --target-app-name <name> New app name to rename the sentinel into (required)
37
+ -o, --out-dir <dir> Output root (default: . / repo root by skill)
38
+ --sentinel <name> Override the sentinel from the pack header
39
+ --force Overwrite files that already exist
40
+ --dry-run List what would be written, write nothing
41
+ -h, --help Show this help
42
+
43
+ Positional fallback: unpack-foundation.sh <pack-file> <target-app-name> [out-dir]
44
+ EOF
45
+ }
46
+
47
+ # -------------------------------------------------------------- arg parsing ---
48
+ while [ $# -gt 0 ]; do
49
+ case "$1" in
50
+ -p|--pack-file) PACK_FILE="${2:?--pack-file needs a value}"; shift 2 ;;
51
+ -t|--target-app-name) TARGET_APP_NAME="${2:?--target-app-name needs a value}"; shift 2 ;;
52
+ -o|--out-dir) OUT_DIR="${2:?--out-dir needs a value}"; shift 2 ;;
53
+ --sentinel) SENTINEL="${2:?--sentinel needs a value}"; shift 2 ;;
54
+ --force) FORCE=1; shift ;;
55
+ --dry-run) DRYRUN=1; shift ;;
56
+ -h|--help) usage; exit 0 ;;
57
+ --) shift; break ;;
58
+ -*) echo "Error: unknown option: $1" >&2; usage; exit 2 ;;
59
+ *)
60
+ if [ -z "$PACK_FILE" ]; then PACK_FILE="$1"
61
+ elif [ -z "$TARGET_APP_NAME" ]; then TARGET_APP_NAME="$1"
62
+ else OUT_DIR="$1"
63
+ fi
64
+ shift ;;
65
+ esac
66
+ done
67
+
68
+ [ -n "$PACK_FILE" ] || { echo "Error: --pack-file is required." >&2; usage; exit 2; }
69
+ [ -n "$TARGET_APP_NAME" ] || { echo "Error: --target-app-name is required." >&2; usage; exit 2; }
70
+ [ -f "$PACK_FILE" ] || { echo "Error: pack file not found: $PACK_FILE" >&2; exit 1; }
71
+
72
+ # Keep replacement tokens safe for bash literal substitution / path joins.
73
+ case "$TARGET_APP_NAME" in
74
+ *[!A-Za-z0-9._-]*)
75
+ echo "Error: --target-app-name may contain only letters, digits, '.', '_' or '-': $TARGET_APP_NAME" >&2
76
+ exit 2 ;;
77
+ esac
78
+
79
+ # ------------------------------------------------ sentinel from header --------
80
+ if [ -z "$SENTINEL" ]; then
81
+ SENTINEL="$(LC_ALL=C sed -n 's/^#[[:space:]]*sentinel:[[:space:]]*\(.*[^[:space:]]\)[[:space:]]*$/\1/p' "$PACK_FILE" | head -n1)"
82
+ fi
83
+ [ -n "$SENTINEL" ] || { echo "Error: Sentinel not found in pack header; supply it with --sentinel." >&2; exit 1; }
84
+
85
+ # ------------------------------------------------ derive case forms -----------
86
+ to_lower() { printf '%s' "$1" | LC_ALL=C tr '[:upper:]' '[:lower:]'; }
87
+
88
+ S_DOT="$SENTINEL"; T_DOT="$TARGET_APP_NAME" # dotted
89
+ S_UND="${SENTINEL//./_}"; T_UND="${TARGET_APP_NAME//./_}" # underscored
90
+ s_low="$(to_lower "$SENTINEL")"; t_low="$(to_lower "$TARGET_APP_NAME")"
91
+ S_HYP="${s_low//./-}"; T_HYP="${t_low//./-}" # lower-hyphen
92
+ S_LDOT="$s_low"; T_LDOT="$t_low" # lower-dot
93
+
94
+ # Build replacement list: dedupe by From (ordinal), drop no-ops, sort longest-first.
95
+ FROM=(); TO=()
96
+ add_form() {
97
+ local from="$1" to="$2" e
98
+ [ -n "$from" ] || return 0
99
+ [ "$from" = "$to" ] && return 0
100
+ if [ "${#FROM[@]}" -gt 0 ]; then
101
+ for e in "${FROM[@]}"; do [ "$e" = "$from" ] && return 0; done
102
+ fi
103
+ FROM+=("$from"); TO+=("$to")
104
+ }
105
+ add_form "$S_DOT" "$T_DOT"
106
+ add_form "$S_UND" "$T_UND"
107
+ add_form "$S_HYP" "$T_HYP"
108
+ add_form "$S_LDOT" "$T_LDOT"
109
+
110
+ # Order indices by FROM length descending (stable for equal lengths).
111
+ ORDER=()
112
+ {
113
+ local_n="${#FROM[@]}"
114
+ used=(); for ((k = 0; k < local_n; k++)); do used[$k]=0; done
115
+ for ((pass = 0; pass < local_n; pass++)); do
116
+ best=-1; bestlen=-1
117
+ for ((j = 0; j < local_n; j++)); do
118
+ if [ "${used[$j]}" -eq 0 ] && [ "${#FROM[$j]}" -gt "$bestlen" ]; then
119
+ bestlen="${#FROM[$j]}"; best="$j"
120
+ fi
121
+ done
122
+ used[$best]=1; ORDER+=("$best")
123
+ done
124
+ }
125
+
126
+ REPL_OUT=''
127
+ apply_repl() { # literal, ordinal, longest-first; result in $REPL_OUT (no subshell trim)
128
+ local text="$1" idx from to
129
+ if [ "${#ORDER[@]}" -gt 0 ]; then # empty when target == sentinel (no-op rename)
130
+ for idx in "${ORDER[@]}"; do
131
+ from="${FROM[$idx]}"; to="${TO[$idx]}"
132
+ text="${text//"$from"/$to}" # quoted pattern => literal (non-glob) match
133
+ done
134
+ fi
135
+ REPL_OUT="$text"
136
+ }
137
+
138
+ # ------------------------------------------------ output root ----------------
139
+ [ -d "$OUT_DIR" ] || mkdir -p "$OUT_DIR"
140
+ OUT_ROOT="$(cd "$OUT_DIR" && pwd -P)"
141
+
142
+ # ------------------------------------------------ base64 decoder (portable) --
143
+ B64D=()
144
+ if printf 'aGk=' | base64 -d >/dev/null 2>&1; then B64D=(base64 -d)
145
+ elif printf 'aGk=' | base64 -D >/dev/null 2>&1; then B64D=(base64 -D)
146
+ elif printf 'aGk=' | base64 --decode >/dev/null 2>&1; then B64D=(base64 --decode)
147
+ else B64D=(openssl base64 -d -A)
148
+ fi
149
+
150
+ # ------------------------------------------------ parse file blocks ----------
151
+ rels=(); encs=(); contents=()
152
+ in_block=0; cur_rel=''; cur_enc=''; cur_content=''; first_line=1
153
+ file_re='^<<<FILE path="([^"]+)"( encoding="([^"]+)")?>>>[[:space:]]*$'
154
+
155
+ while IFS= read -r line || [ -n "$line" ]; do
156
+ line="${line%$'\r'}" # tolerate CRLF packs (e.g. git autocrlf on Windows checkout)
157
+ if [ "$in_block" -eq 0 ]; then
158
+ if [[ "$line" =~ $file_re ]]; then
159
+ cur_rel="${BASH_REMATCH[1]}"
160
+ cur_enc="${BASH_REMATCH[3]:-}"
161
+ in_block=1; first_line=1; cur_content=''
162
+ fi
163
+ else
164
+ if [ "$line" = '<<<ENDFILE>>>' ]; then
165
+ rels+=("$cur_rel"); encs+=("$cur_enc"); contents+=("$cur_content")
166
+ in_block=0
167
+ elif [ "$first_line" -eq 1 ]; then
168
+ cur_content="$line"; first_line=0
169
+ else
170
+ cur_content="$cur_content"$'\n'"$line"
171
+ fi
172
+ fi
173
+ done < "$PACK_FILE"
174
+
175
+ [ "${#rels[@]}" -gt 0 ] || { echo "Error: No <<<FILE>>> blocks found in pack: $PACK_FILE" >&2; exit 1; }
176
+
177
+ # ------------------------------------------------ resolve destinations -------
178
+ dests=()
179
+ for ((i = 0; i < ${#rels[@]}; i++)); do
180
+ apply_repl "${rels[$i]}"
181
+ dests+=("$OUT_ROOT/$REPL_OUT")
182
+ done
183
+
184
+ # ------------------------------------------------ conflict guard -------------
185
+ if [ "$FORCE" -eq 0 ] && [ "$DRYRUN" -eq 0 ]; then
186
+ conflicts=()
187
+ for d in "${dests[@]}"; do [ -e "$d" ] && conflicts+=("$d"); done
188
+ if [ "${#conflicts[@]}" -gt 0 ]; then
189
+ echo "Error: Refusing to overwrite existing files (re-run with --force):" >&2
190
+ printf ' %s\n' "${conflicts[@]}" >&2
191
+ exit 1
192
+ fi
193
+ fi
194
+
195
+ # ------------------------------------------------ write ----------------------
196
+ written=0
197
+ for ((i = 0; i < ${#rels[@]}; i++)); do
198
+ rel="${rels[$i]}"; enc="${encs[$i]}"; dest="${dests[$i]}"
199
+ if [ "$DRYRUN" -eq 1 ]; then
200
+ echo "WOULD WRITE $rel -> $dest"
201
+ continue
202
+ fi
203
+ mkdir -p "$(dirname "$dest")"
204
+ if [ "$enc" = 'base64' ]; then
205
+ printf '%s' "${contents[$i]}" | tr -d '[:space:]' | "${B64D[@]}" > "$dest"
206
+ else
207
+ apply_repl "${contents[$i]}"
208
+ printf '%s' "$REPL_OUT" > "$dest"
209
+ fi
210
+ written=$((written + 1))
211
+ done
212
+
213
+ if [ "$DRYRUN" -eq 1 ]; then
214
+ echo "DryRun: ${#rels[@]} file(s) would be written to $OUT_ROOT (sentinel '$SENTINEL' -> '$TARGET_APP_NAME')."
215
+ else
216
+ echo "Unpacked $written file(s) to $OUT_ROOT (sentinel '$SENTINEL' -> '$TARGET_APP_NAME')."
217
+ fi