aw-spec-cli 0.1.2 → 0.1.4

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 (77) hide show
  1. package/README.md +291 -0
  2. package/assets/docs/ModularMonolith/ModularMonolith.Reference.pack.txt +25248 -0
  3. package/assets/docs/ModularMonolith/ModularMonolith.pack.txt +5726 -0
  4. package/assets/docs/ModularMonolith/pack-foundation.ps1 +145 -0
  5. package/assets/docs/ModularMonolith/pack-foundation.sh +196 -0
  6. package/assets/docs/ModularMonolith/pack-reference-foundation.ps1 +157 -0
  7. package/assets/docs/ModularMonolith/pack-reference-foundation.sh +209 -0
  8. package/assets/docs/ModularMonolith/unpack-foundation.ps1 +126 -0
  9. package/assets/docs/ModularMonolith/unpack-foundation.sh +217 -0
  10. package/assets/docs/ModularMonolith/unpack-reference-foundation.ps1 +129 -0
  11. package/assets/docs/ModularMonolith/unpack-reference-foundation.sh +218 -0
  12. package/assets/docs/UI.Visualization/PACKAGING.md +191 -0
  13. package/assets/docs/UI.Visualization/UI.Visualization.Reference.pack.txt +11768 -0
  14. package/assets/docs/UI.Visualization/UI.Visualization.pack.txt +11214 -0
  15. package/assets/docs/UI.Visualization/pack-foundation.ps1 +165 -0
  16. package/assets/docs/UI.Visualization/pack-foundation.sh +206 -0
  17. package/assets/docs/UI.Visualization/pack-reference-foundation.ps1 +159 -0
  18. package/assets/docs/UI.Visualization/pack-reference-foundation.sh +205 -0
  19. package/assets/docs/UI.Visualization/unpack-foundation.ps1 +126 -0
  20. package/assets/docs/UI.Visualization/unpack-foundation.sh +217 -0
  21. package/assets/docs/UI.Visualization/unpack-reference-foundation.ps1 +129 -0
  22. package/assets/docs/UI.Visualization/unpack-reference-foundation.sh +218 -0
  23. package/assets/docs/VerticalSlice/VerticalSlice.Reference.pack.txt +19318 -0
  24. package/assets/docs/VerticalSlice/pack-reference-foundation.ps1 +157 -0
  25. package/assets/docs/VerticalSlice/pack-reference-foundation.sh +209 -0
  26. package/assets/docs/VerticalSlice/unpack-reference-foundation.ps1 +129 -0
  27. package/assets/docs/VerticalSlice/unpack-reference-foundation.sh +218 -0
  28. package/assets/docs/components/Email/README.md +7 -0
  29. package/assets/docs/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +84 -0
  30. package/assets/docs/components/Email/integration.md +57 -17
  31. package/assets/docs/components/Jobs/integration.md +29 -15
  32. package/assets/docs/components/QrGenerator/README.md +7 -0
  33. package/assets/docs/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +57 -0
  34. package/assets/docs/components/QrGenerator/integration.md +51 -6
  35. package/assets/docs/components/README.md +19 -2
  36. package/assets/docs/domainspecs/UIVisualization/chart-bubble.md +70 -0
  37. package/assets/docs/domainspecs/UIVisualization/chart-dimred.md +58 -0
  38. package/assets/docs/domainspecs/UIVisualization/chart-heatmap.md +62 -0
  39. package/assets/docs/domainspecs/UIVisualization/chart-network.md +69 -0
  40. package/assets/docs/domainspecs/UIVisualization/chart-parallel.md +75 -0
  41. package/assets/docs/domainspecs/UIVisualization/chart-scurve.md +66 -0
  42. package/assets/docs/domainspecs/UIVisualization/chart-survival.md +69 -0
  43. package/assets/docs/domainspecs/UIVisualization/chart-trellis.md +68 -0
  44. package/assets/docs/domainspecs/UIVisualization/chart-violin.md +61 -0
  45. package/assets/docs/domainspecs/UIVisualization/chart-volcano.md +63 -0
  46. package/assets/docs/domainspecs/UIVisualization/charting-standards.domain.md +80 -0
  47. package/assets/docs/domainspecs/UIVisualization/clean-architecture.domain.md +87 -0
  48. package/assets/docs/domainspecs/UIVisualization/coding-standards.domain.md +47 -0
  49. package/assets/docs/domainspecs/UIVisualization/comandrulemap.md +130 -0
  50. package/assets/docs/domainspecs/UIVisualization/component-contracts.domain.md +67 -0
  51. package/assets/docs/domainspecs/UIVisualization/data-source.domain.md +68 -0
  52. package/assets/docs/domainspecs/UIVisualization/domain-requirements.md +110 -0
  53. package/assets/docs/domainspecs/UIVisualization/folder-structure.domain.md +55 -0
  54. package/assets/docs/domainspecs/UIVisualization/state-management.domain.md +67 -0
  55. package/assets/docs/domainspecs/UIVisualization/testing.domain.md +52 -0
  56. package/assets/docs/domainspecs/UIVisualization/ui-viz-terms.md +39 -0
  57. package/assets/docs/domainspecs/healthcare/comandrulemap.md +47 -0
  58. package/assets/docs/domainspecs/pharma/comandrulemap.md +50 -0
  59. package/dist/commands/init.d.ts.map +1 -1
  60. package/dist/commands/init.js +54 -7
  61. package/dist/commands/init.js.map +1 -1
  62. package/dist/commands/update.d.ts +3 -0
  63. package/dist/commands/update.d.ts.map +1 -0
  64. package/dist/commands/update.js +142 -0
  65. package/dist/commands/update.js.map +1 -0
  66. package/dist/index.js +2 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/utils/manifest.d.ts +31 -0
  69. package/dist/utils/manifest.d.ts.map +1 -0
  70. package/dist/utils/manifest.js +91 -0
  71. package/dist/utils/manifest.js.map +1 -0
  72. package/dist/utils/update-engine.d.ts +52 -0
  73. package/dist/utils/update-engine.d.ts.map +1 -0
  74. package/dist/utils/update-engine.js +203 -0
  75. package/dist/utils/update-engine.js.map +1 -0
  76. package/package.json +5 -2
  77. package/assets/docs/requirement-spec.txt +0 -15
@@ -0,0 +1,157 @@
1
+ #requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ Pack a verified feature reference template into a single AW-Spec text pack file.
5
+
6
+ .DESCRIPTION
7
+ Walks -SourceDir (a templates/features/<Name>.SimpleSample folder), excludes build/IDE
8
+ artifacts, template meta-files, and secret-bearing files, and writes every remaining file
9
+ into one pack file using <<<FILE>>> / <<<ENDFILE>>> markers. Text files are stored verbatim
10
+ (UTF-8, LF); binary files are base64-encoded. The resulting pack file is the single bundled
11
+ artifact consumed by /aw-spec-addcomponent or /aw-spec-implement unpack workflows.
12
+
13
+ .EXAMPLE
14
+ ./pack-reference-foundation.ps1 `
15
+ -SourceDir ../../templates/features/VerticalSlice.SimpleSample `
16
+ -OutFile ./VerticalSlice.Reference.pack.txt `
17
+ -Sentinel AppWeaver.VerticalSlice `
18
+ -Foundation VerticalSlice.Reference
19
+
20
+ .EXAMPLE
21
+ ./pack-reference-foundation.ps1 `
22
+ -SourceDir ../../templates/features/ModularMonolith.SimpleSample `
23
+ -OutFile ../ModularMonolith/ModularMonolith.Reference.pack.txt `
24
+ -Sentinel AppWeaver.ModularMonolith `
25
+ -Foundation ModularMonolith.Reference
26
+ #>
27
+ [CmdletBinding()]
28
+ param(
29
+ [Parameter(Mandatory)] [string] $SourceDir,
30
+ [string] $OutFile,
31
+ [string] $Sentinel = 'AppWeaver.VerticalSlice',
32
+ [string] $Foundation = 'VerticalSlice.Reference',
33
+ [switch] $AllowSecrets
34
+ )
35
+
36
+ $ErrorActionPreference = 'Stop'
37
+
38
+ $src = (Resolve-Path -LiteralPath $SourceDir).Path
39
+ if (-not (Test-Path -LiteralPath $src -PathType Container)) {
40
+ throw "SourceDir is not a folder: $SourceDir"
41
+ }
42
+ if (-not $OutFile) { $OutFile = Join-Path (Get-Location).Path "$Foundation.pack.txt" }
43
+
44
+ # Entire subtree excluded if any path segment matches one of these.
45
+ $excludeDirs = @('bin','obj','.vs','.git','.idea','.vscode','TestResults','node_modules','packages','App_Data','artifacts','.next','logs')
46
+
47
+ # Files excluded by leaf-name glob (build noise, local dbs, secret-bearing files, and
48
+ # template meta-files that should never appear in unpacked output).
49
+ $excludeFiles = @(
50
+ '*.user','*.suo','*.DotSettings.user','*.db','*.db-shm','*.db-wal','*.sqlite','*.log',
51
+ '.env','*.env.*','.env.*','*.pfx','*.pem','*.key','*.snk','id_rsa','id_rsa.pub','secrets.json',
52
+ '.DS_Store', # macOS metadata
53
+ '*.my-ref.*', # template annotation files (Action: Exclude in TemplateConfig)
54
+ '*TemplateConfig.config' # template configuration meta-files
55
+ )
56
+
57
+ # Content patterns that indicate a real secret; packing aborts on a hit unless -AllowSecrets.
58
+ $secretPatterns = @(
59
+ @{ Name = 'NuGet ClearTextPassword'; Rx = 'ClearTextPassword' }
60
+ @{ Name = 'Private key block'; Rx = 'BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY' }
61
+ @{ Name = 'AWS access key id'; Rx = 'AKIA[0-9A-Z]{16}' }
62
+ @{ Name = 'Azure DevOps PAT-ish'; Rx = '[A-Za-z0-9]{52}AZDO[A-Za-z0-9]' }
63
+ )
64
+
65
+ function Test-Excluded([string] $relPath) {
66
+ foreach ($seg in ($relPath -split '[\\/]')) {
67
+ if ($excludeDirs -contains $seg) { return $true }
68
+ }
69
+ $leaf = Split-Path $relPath -Leaf
70
+ foreach ($g in $excludeFiles) { if ($leaf -like $g) { return $true } }
71
+ return $false
72
+ }
73
+
74
+ function Test-Binary([byte[]] $bytes) {
75
+ $n = [Math]::Min(8000, $bytes.Length)
76
+ for ($i = 0; $i -lt $n; $i++) { if ($bytes[$i] -eq 0) { return $true } }
77
+ return $false
78
+ }
79
+
80
+ # Collect + filter
81
+ $picked = New-Object System.Collections.Generic.List[object]
82
+ foreach ($f in (Get-ChildItem -LiteralPath $src -Recurse -File -Force)) {
83
+ $rel = ($f.FullName.Substring($src.Length).TrimStart('\','/')) -replace '\\','/'
84
+ if (Test-Excluded $rel) { continue }
85
+ $picked.Add([pscustomobject]@{ Rel = $rel; Full = $f.FullName })
86
+ }
87
+ $picked = @($picked | Sort-Object Rel)
88
+ if ($picked.Count -eq 0) { throw "No files to pack under: $src" }
89
+
90
+ # Secret scan — fail closed so credentials never get baked into the committed bundle.
91
+ $findings = New-Object System.Collections.Generic.List[string]
92
+ foreach ($p in $picked) {
93
+ try { $t = [System.IO.File]::ReadAllText($p.Full) } catch { continue }
94
+ foreach ($pat in $secretPatterns) {
95
+ if ($t -match $pat.Rx) { $findings.Add(" $($p.Rel) -> $($pat.Name)") }
96
+ }
97
+ }
98
+ if ($findings.Count -gt 0) {
99
+ $msg = "Potential secret(s) detected — refusing to pack (rotate + remove them, or pass -AllowSecrets):`n" + ($findings -join "`n")
100
+ if ($AllowSecrets) { Write-Warning $msg } else { throw $msg }
101
+ }
102
+
103
+ # Sentinel sanity check
104
+ $hits = 0
105
+ foreach ($p in $picked) {
106
+ try {
107
+ $t = [System.IO.File]::ReadAllText($p.Full)
108
+ if ($t.Contains($Sentinel)) { $hits++ }
109
+ } catch { }
110
+ }
111
+ if ($hits -eq 0) {
112
+ Write-Warning "Sentinel '$Sentinel' not found in any file. Did you build the sample under that exact name?"
113
+ } else {
114
+ Write-Host "Sentinel '$Sentinel' found in $hits file(s)."
115
+ }
116
+
117
+ # Build the pack (explicit LF newlines throughout)
118
+ $sb = [System.Text.StringBuilder]::new()
119
+ function AddLine([string] $s) { [void]$sb.Append($s); [void]$sb.Append("`n") }
120
+
121
+ AddLine '# AW-SPEC REFERENCE PACK'
122
+ AddLine "# foundation: $Foundation"
123
+ AddLine "# sentinel: $Sentinel"
124
+ AddLine "# generated: $([DateTime]::UtcNow.ToString('o'))"
125
+ AddLine "# files: $($picked.Count)"
126
+ AddLine '# format: aw-spec-pack/v1'
127
+ AddLine '# --- replacement contract (ordinal, case-sensitive, longest-first) ---'
128
+ AddLine '# <Sentinel> -> <AppName> (dotted: namespaces, csproj/sln, refs)'
129
+ AddLine '# <Sentinel _-form> -> <AppName _-form> (underscored: Aspire project class names)'
130
+ AddLine '# <sentinel --form> -> <appname --form> (lower-hyphen: UserSecretsId, db/containers)'
131
+ AddLine '# <sentinel .-form> -> <appname lower-dot> (lower-dot: lowercased identifiers)'
132
+ AddLine '# Applied to file CONTENTS and file/dir PATHS. Never touch other AppWeaver.* tokens.'
133
+ AddLine '# ============================================================'
134
+ AddLine ''
135
+
136
+ foreach ($p in $picked) {
137
+ $bytes = [System.IO.File]::ReadAllBytes($p.Full)
138
+ if (Test-Binary $bytes) {
139
+ AddLine "<<<FILE path=`"$($p.Rel)`" encoding=`"base64`">>>"
140
+ AddLine ([Convert]::ToBase64String($bytes))
141
+ AddLine '<<<ENDFILE>>>'
142
+ } else {
143
+ $text = ([System.IO.File]::ReadAllText($p.Full)) -replace "`r`n", "`n"
144
+ AddLine "<<<FILE path=`"$($p.Rel)`">>>"
145
+ [void]$sb.Append($text)
146
+ if (-not $text.EndsWith("`n")) { [void]$sb.Append("`n") }
147
+ AddLine '<<<ENDFILE>>>'
148
+ }
149
+ }
150
+
151
+ $outDir = Split-Path -Parent $OutFile
152
+ if ($outDir -and -not (Test-Path -LiteralPath $outDir)) {
153
+ New-Item -ItemType Directory -Force -Path $outDir | Out-Null
154
+ }
155
+ [System.IO.File]::WriteAllText($OutFile, $sb.ToString(), [System.Text.UTF8Encoding]::new($false))
156
+
157
+ Write-Host "Packed $($picked.Count) file(s) -> $OutFile"
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # pack-reference-foundation.sh — bash/macOS port of pack-reference-foundation.ps1
4
+ #
5
+ # SYNOPSIS
6
+ # Pack a verified feature reference template into a single AW-Spec text pack file.
7
+ #
8
+ # DESCRIPTION
9
+ # Walks --source-dir (a templates/features/<Name>.SimpleSample folder), excludes build/IDE
10
+ # artifacts, template meta-files, and secret-bearing files, and writes every remaining file
11
+ # into one pack file using <<<FILE>>> / <<<ENDFILE>>> markers. Text files are stored verbatim
12
+ # (UTF-8, LF); binary files are base64-encoded. Format-compatible with unpack-reference-foundation.
13
+ #
14
+ # EXAMPLE
15
+ # ./pack-reference-foundation.sh \
16
+ # --source-dir ../../templates/features/VerticalSlice.SimpleSample \
17
+ # --out-file ./VerticalSlice.Reference.pack.txt \
18
+ # --sentinel AppWeaver.VerticalSlice \
19
+ # --foundation VerticalSlice.Reference
20
+ #
21
+ # ./pack-reference-foundation.sh \
22
+ # --source-dir ../../templates/features/ModularMonolith.SimpleSample \
23
+ # --out-file ../ModularMonolith/ModularMonolith.Reference.pack.txt \
24
+ # --sentinel AppWeaver.ModularMonolith \
25
+ # --foundation ModularMonolith.Reference
26
+ #
27
+ set -euo pipefail
28
+
29
+ # ------------------------------------------------------------------ defaults ---
30
+ SENTINEL='AppWeaver.VerticalSlice'
31
+ FOUNDATION='VerticalSlice.Reference'
32
+ SOURCE_DIR=''
33
+ OUT_FILE=''
34
+ ALLOW_SECRETS=0
35
+
36
+ usage() {
37
+ cat >&2 <<'EOF'
38
+ Usage: pack-reference-foundation.sh --source-dir <dir> [--out-file <file>]
39
+ [--sentinel <name>] [--foundation <name>]
40
+ [--allow-secrets]
41
+
42
+ -s, --source-dir <dir> templates/features/<Name>.SimpleSample folder (required)
43
+ -o, --out-file <file> Output pack file (default: ./<foundation>.pack.txt)
44
+ --sentinel <name> Sentinel app name (default: AppWeaver.VerticalSlice)
45
+ --foundation <name> Foundation/pack name (default: VerticalSlice.Reference)
46
+ --allow-secrets Pack even if credential markers are detected
47
+ -h, --help Show this help
48
+
49
+ Positional fallback: pack-reference-foundation.sh <source-dir> [out-file]
50
+ EOF
51
+ }
52
+
53
+ # -------------------------------------------------------------- arg parsing ---
54
+ while [ $# -gt 0 ]; do
55
+ case "$1" in
56
+ -s|--source-dir) SOURCE_DIR="${2:?--source-dir needs a value}"; shift 2 ;;
57
+ -o|--out-file) OUT_FILE="${2:?--out-file needs a value}"; shift 2 ;;
58
+ --sentinel) SENTINEL="${2:?--sentinel needs a value}"; shift 2 ;;
59
+ --foundation) FOUNDATION="${2:?--foundation needs a value}"; shift 2 ;;
60
+ --allow-secrets) ALLOW_SECRETS=1; shift ;;
61
+ -h|--help) usage; exit 0 ;;
62
+ --) shift; break ;;
63
+ -*) echo "Error: unknown option: $1" >&2; usage; exit 2 ;;
64
+ *)
65
+ if [ -z "$SOURCE_DIR" ]; then SOURCE_DIR="$1"
66
+ elif [ -z "$OUT_FILE" ]; then OUT_FILE="$1"
67
+ else echo "Error: unexpected argument: $1" >&2; usage; exit 2
68
+ fi
69
+ shift ;;
70
+ esac
71
+ done
72
+
73
+ [ -n "$SOURCE_DIR" ] || { echo "Error: --source-dir is required." >&2; usage; exit 2; }
74
+ [ -d "$SOURCE_DIR" ] || { echo "Error: SourceDir is not a folder: $SOURCE_DIR" >&2; exit 1; }
75
+
76
+ SRC="$(cd "$SOURCE_DIR" && pwd -P)"
77
+ [ -n "$OUT_FILE" ] || OUT_FILE="$(pwd)/$FOUNDATION.pack.txt"
78
+
79
+ # -------------------------------------------------------------- exclusions ---
80
+ # Entire subtree excluded if any path segment matches one of these.
81
+ EXCLUDE_DIRS=(bin obj .vs .git .idea .vscode TestResults node_modules packages App_Data artifacts .next logs)
82
+
83
+ # Files excluded by leaf-name glob — build noise, local dbs, secret-bearing files,
84
+ # macOS metadata, and template meta-files that must never appear in unpacked output.
85
+ EXCLUDE_FILES=(
86
+ '*.user' '*.suo' '*.DotSettings.user' '*.db' '*.db-shm' '*.db-wal' '*.sqlite' '*.log'
87
+ '.env' '*.env.*' '.env.*' '*.pfx' '*.pem' '*.key' '*.snk' 'id_rsa' 'id_rsa.pub' 'secrets.json'
88
+ '.DS_Store'
89
+ '*.my-ref.*'
90
+ '*TemplateConfig.config'
91
+ )
92
+
93
+ # Content patterns that indicate a real secret; packing aborts on a hit unless --allow-secrets.
94
+ SECRET_NAMES=('NuGet ClearTextPassword' 'Private key block' 'AWS access key id' 'Azure DevOps PAT-ish')
95
+ SECRET_RX=(
96
+ 'ClearTextPassword'
97
+ 'BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY'
98
+ 'AKIA[0-9A-Z]{16}'
99
+ '[A-Za-z0-9]{52}AZDO[A-Za-z0-9]'
100
+ )
101
+
102
+ is_excluded() {
103
+ local rel="$1" seg leaf g
104
+ local -a segs
105
+ IFS='/' read -ra segs <<< "$rel"
106
+ for seg in "${segs[@]}"; do
107
+ for g in "${EXCLUDE_DIRS[@]}"; do
108
+ [ "$seg" = "$g" ] && return 0
109
+ done
110
+ done
111
+ leaf="${rel##*/}"
112
+ for g in "${EXCLUDE_FILES[@]}"; do
113
+ # shellcheck disable=SC2254
114
+ case "$leaf" in $g) return 0 ;; esac
115
+ done
116
+ return 1
117
+ }
118
+
119
+ is_binary() {
120
+ local f="$1" n1 n2
121
+ n1="$(head -c 8000 "$f" | wc -c | tr -d '[:space:]')"
122
+ n2="$(head -c 8000 "$f" | LC_ALL=C tr -d '\000' | wc -c | tr -d '[:space:]')"
123
+ [ "$n1" != "$n2" ]
124
+ }
125
+
126
+ # ----------------------------------------------------------- collect + sort ---
127
+ picked=()
128
+ while IFS= read -r full; do
129
+ rel="${full#"$SRC"/}"
130
+ is_excluded "$rel" && continue
131
+ picked+=("$rel")
132
+ done < <(find "$SRC" -type f | LC_ALL=C sort)
133
+
134
+ [ "${#picked[@]}" -gt 0 ] || { echo "Error: No files to pack under: $SRC" >&2; exit 1; }
135
+
136
+ # ----------------------------------------------------------- secret scan -----
137
+ findings=()
138
+ for rel in "${picked[@]}"; do
139
+ f="$SRC/$rel"
140
+ for i in "${!SECRET_RX[@]}"; do
141
+ if LC_ALL=C grep -aEq -- "${SECRET_RX[$i]}" "$f" 2>/dev/null; then
142
+ findings+=(" $rel -> ${SECRET_NAMES[$i]}")
143
+ fi
144
+ done
145
+ done
146
+ if [ "${#findings[@]}" -gt 0 ]; then
147
+ if [ "$ALLOW_SECRETS" -eq 1 ]; then
148
+ echo "WARNING: Potential secret(s) detected — packing anyway (--allow-secrets):" >&2
149
+ printf '%s\n' "${findings[@]}" >&2
150
+ else
151
+ echo "Error: Potential secret(s) detected — refusing to pack (rotate + remove them, or pass --allow-secrets):" >&2
152
+ printf '%s\n' "${findings[@]}" >&2
153
+ exit 1
154
+ fi
155
+ fi
156
+
157
+ # ----------------------------------------------------------- sentinel check --
158
+ hits=0
159
+ for rel in "${picked[@]}"; do
160
+ if LC_ALL=C grep -aFq -- "$SENTINEL" "$SRC/$rel" 2>/dev/null; then
161
+ hits=$((hits + 1))
162
+ fi
163
+ done
164
+ if [ "$hits" -eq 0 ]; then
165
+ echo "WARNING: Sentinel '$SENTINEL' not found in any file. Did you build the sample under that exact name?" >&2
166
+ else
167
+ echo "Sentinel '$SENTINEL' found in $hits file(s)."
168
+ fi
169
+
170
+ # ----------------------------------------------------------- write the pack --
171
+ outdir="$(dirname "$OUT_FILE")"
172
+ [ -d "$outdir" ] || mkdir -p "$outdir"
173
+
174
+ {
175
+ printf '%s\n' '# AW-SPEC REFERENCE PACK'
176
+ printf '# foundation: %s\n' "$FOUNDATION"
177
+ printf '# sentinel: %s\n' "$SENTINEL"
178
+ printf '# generated: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%S.0000000Z)"
179
+ printf '# files: %s\n' "${#picked[@]}"
180
+ printf '%s\n' '# format: aw-spec-pack/v1'
181
+ printf '%s\n' '# --- replacement contract (ordinal, case-sensitive, longest-first) ---'
182
+ printf '%s\n' '# <Sentinel> -> <AppName> (dotted: namespaces, csproj/sln, refs)'
183
+ printf '%s\n' '# <Sentinel _-form> -> <AppName _-form> (underscored: Aspire project class names)'
184
+ printf '%s\n' '# <sentinel --form> -> <appname --form> (lower-hyphen: UserSecretsId, db/containers)'
185
+ printf '%s\n' '# <sentinel .-form> -> <appname lower-dot> (lower-dot: lowercased identifiers)'
186
+ printf '%s\n' '# Applied to file CONTENTS and file/dir PATHS. Never touch other AppWeaver.* tokens.'
187
+ printf '%s\n' '# ============================================================'
188
+ printf '\n'
189
+ } > "$OUT_FILE"
190
+
191
+ for rel in "${picked[@]}"; do
192
+ f="$SRC/$rel"
193
+ if is_binary "$f"; then
194
+ {
195
+ printf '<<<FILE path="%s" encoding="base64">>>\n' "$rel"
196
+ base64 < "$f" | tr -d '\n'
197
+ printf '\n'
198
+ printf '%s\n' '<<<ENDFILE>>>'
199
+ } >> "$OUT_FILE"
200
+ else
201
+ {
202
+ printf '<<<FILE path="%s">>>\n' "$rel"
203
+ awk '{ sub(/\r$/, ""); print }' "$f"
204
+ printf '%s\n' '<<<ENDFILE>>>'
205
+ } >> "$OUT_FILE"
206
+ fi
207
+ done
208
+
209
+ echo "Packed ${#picked[@]} file(s) -> $OUT_FILE"
@@ -0,0 +1,129 @@
1
+ #requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ Unpack an AW-Spec reference pack file into a target project, renaming the
5
+ sentinel app name to the caller's app name.
6
+
7
+ .DESCRIPTION
8
+ Reads -PackFile (produced by pack-reference-foundation.ps1), takes the sentinel from
9
+ its header (override with -Sentinel), derives four case forms of the target app name,
10
+ and writes every file block to -OutDir with the sentinel rewritten in both file
11
+ CONTENTS and file/dir PATHS. Text blocks are rewritten and written as UTF-8;
12
+ base64 blocks are decoded as-is. Refuses to overwrite existing files unless -Force.
13
+
14
+ .EXAMPLE
15
+ ./unpack-reference-foundation.ps1 `
16
+ -PackFile ./VerticalSlice.Reference.pack.txt `
17
+ -TargetAppName AppWeaver.Pharma `
18
+ -OutDir ../../
19
+ #>
20
+ [CmdletBinding()]
21
+ param(
22
+ [Parameter(Mandatory)] [string] $PackFile,
23
+ [Parameter(Mandatory)] [string] $TargetAppName,
24
+ [string] $OutDir = '.',
25
+ [string] $Sentinel,
26
+ [switch] $Force,
27
+ [switch] $DryRun
28
+ )
29
+
30
+ $ErrorActionPreference = 'Stop'
31
+
32
+ $pack = (Resolve-Path -LiteralPath $PackFile).Path
33
+ $lines = ([System.IO.File]::ReadAllText($pack)) -split "`r?`n"
34
+
35
+ # --- sentinel from header unless overridden ---
36
+ if (-not $Sentinel) {
37
+ foreach ($l in $lines) {
38
+ if ($l -match '^#\s*sentinel:\s*(.+?)\s*$') { $Sentinel = $Matches[1]; break }
39
+ }
40
+ }
41
+ if (-not $Sentinel) { throw "Sentinel not found in pack header; supply it with -Sentinel." }
42
+
43
+ # --- derive case forms ---
44
+ function Conv-Underscore ([string] $s) { $s -replace '\.','_' }
45
+ function Conv-LowerHyphen ([string] $s) { ($s.ToLowerInvariant()) -replace '\.','-' }
46
+ function Conv-LowerDot ([string] $s) { $s.ToLowerInvariant() }
47
+
48
+ # NOTE: do NOT use a hashtable here — PowerShell hashtable keys are case-INSENSITIVE,
49
+ # which would collapse the dotted ("AppWeaver.VerticalSlice") and lower-dot
50
+ # ("appweaver.verticalslice") forms into one entry and drop the namespace rename.
51
+ $forms = @(
52
+ @{ From = $Sentinel; To = $TargetAppName }
53
+ @{ From = (Conv-Underscore $Sentinel); To = (Conv-Underscore $TargetAppName) }
54
+ @{ From = (Conv-LowerHyphen $Sentinel); To = (Conv-LowerHyphen $TargetAppName) }
55
+ @{ From = (Conv-LowerDot $Sentinel); To = (Conv-LowerDot $TargetAppName) }
56
+ )
57
+ $replacements = @()
58
+ $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal)
59
+ foreach ($pair in $forms) {
60
+ $from = [string]$pair.From
61
+ $to = [string]$pair.To
62
+ if ([string]::IsNullOrEmpty($from)) { continue }
63
+ if ($from -ceq $to) { continue }
64
+ if (-not $seen.Add($from)) { continue }
65
+ $replacements += [pscustomobject]@{ From = $from; To = $to }
66
+ }
67
+ # longest token first so overlaps resolve correctly
68
+ $replacements = @($replacements | Sort-Object { $_.From.Length } -Descending)
69
+
70
+ function Apply-Repl([string] $text) {
71
+ foreach ($r in $replacements) { $text = $text.Replace($r.From, $r.To) }
72
+ return $text
73
+ }
74
+
75
+ # --- output root ---
76
+ if (-not (Test-Path -LiteralPath $OutDir)) { New-Item -ItemType Directory -Force -Path $OutDir | Out-Null }
77
+ $outRoot = (Resolve-Path -LiteralPath $OutDir).Path
78
+ $sep = [System.IO.Path]::DirectorySeparatorChar
79
+
80
+ # --- parse file blocks ---
81
+ $files = @()
82
+ $i = 0
83
+ while ($i -lt $lines.Count) {
84
+ if ($lines[$i] -match '^<<<FILE path="(.+?)"(?:\s+encoding="(\w+)")?>>>\s*$') {
85
+ $rel = $Matches[1]; $enc = $Matches[2]
86
+ $i++
87
+ $buf = New-Object System.Collections.Generic.List[string]
88
+ while ($i -lt $lines.Count -and $lines[$i] -ne '<<<ENDFILE>>>') {
89
+ $buf.Add($lines[$i]); $i++
90
+ }
91
+ $files += [pscustomobject]@{ Rel = $rel; Enc = $enc; Content = ($buf -join "`n") }
92
+ }
93
+ $i++
94
+ }
95
+ if ($files.Count -eq 0) { throw "No <<<FILE>>> blocks found in pack: $pack" }
96
+
97
+ function Resolve-Dest([string] $rel) {
98
+ $renamed = (Apply-Repl $rel).Replace('/', $sep)
99
+ return (Join-Path $outRoot $renamed)
100
+ }
101
+
102
+ # --- conflict guard ---
103
+ $conflicts = foreach ($f in $files) { $d = Resolve-Dest $f.Rel; if (Test-Path -LiteralPath $d) { $d } }
104
+ if ($conflicts -and -not $Force) {
105
+ throw ("Refusing to overwrite existing files (re-run with -Force):`n " + ($conflicts -join "`n "))
106
+ }
107
+
108
+ # --- write ---
109
+ $written = 0
110
+ foreach ($f in $files) {
111
+ $dest = Resolve-Dest $f.Rel
112
+ $destDir = Split-Path -Parent $dest
113
+ if ($DryRun) { Write-Host "WOULD WRITE $($f.Rel) -> $dest"; continue }
114
+ if ($destDir -and -not (Test-Path -LiteralPath $destDir)) {
115
+ New-Item -ItemType Directory -Force -Path $destDir | Out-Null
116
+ }
117
+ if ($f.Enc -eq 'base64') {
118
+ [System.IO.File]::WriteAllBytes($dest, [Convert]::FromBase64String(($f.Content -replace '\s','')))
119
+ } else {
120
+ [System.IO.File]::WriteAllText($dest, (Apply-Repl $f.Content), [System.Text.UTF8Encoding]::new($false))
121
+ }
122
+ $written++
123
+ }
124
+
125
+ if ($DryRun) {
126
+ Write-Host "DryRun: $($files.Count) file(s) would be written to $outRoot (sentinel '$Sentinel' -> '$TargetAppName')."
127
+ } else {
128
+ Write-Host "Unpacked $written file(s) to $outRoot (sentinel '$Sentinel' -> '$TargetAppName')."
129
+ }