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,582 @@
1
+ #!/usr/bin/env bash
2
+ # Common functions and variables for all scripts
3
+
4
+ # Find repository root by searching upward for .aw-spec directory
5
+ # This is the primary marker for aw-spec projects
6
+ find_specify_root() {
7
+ local dir="${1:-$(pwd)}"
8
+ # Normalize to absolute path to prevent infinite loop with relative paths
9
+ # Use -- to handle paths starting with - (e.g., -P, -L)
10
+ dir="$(cd -- "$dir" 2>/dev/null && pwd)" || return 1
11
+ local prev_dir=""
12
+ while true; do
13
+ if [ -d "$dir/.aw-spec" ]; then
14
+ echo "$dir"
15
+ return 0
16
+ fi
17
+ # Stop if we've reached filesystem root or dirname stops changing
18
+ if [ "$dir" = "/" ] || [ "$dir" = "$prev_dir" ]; then
19
+ break
20
+ fi
21
+ prev_dir="$dir"
22
+ dir="$(dirname "$dir")"
23
+ done
24
+ return 1
25
+ }
26
+
27
+ # Get repository root, prioritizing .aw-spec directory
28
+ # This prevents using a parent repository when aw-spec is initialized in a subdirectory
29
+ get_repo_root() {
30
+ # First, look for .aw-spec directory (aw-spec's own marker)
31
+ local specify_root
32
+ if specify_root=$(find_specify_root); then
33
+ echo "$specify_root"
34
+ return
35
+ fi
36
+
37
+ # Final fallback to script location
38
+ local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39
+ (cd "$script_dir/../../.." && pwd)
40
+ }
41
+
42
+ # Get current feature name from explicit state only.
43
+ # Returns the feature identifier or empty string if none is set.
44
+ # Feature state is set by SPECIFY_FEATURE (from create-new-feature or
45
+ # the git extension) or implicitly via .aw-spec/feature.json.
46
+ get_current_branch() {
47
+ if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
48
+ echo "$SPECIFY_FEATURE"
49
+ return
50
+ fi
51
+
52
+ # No explicit feature set — caller must handle this via feature.json
53
+ # in get_feature_paths(). Return empty to signal "unknown".
54
+ echo ""
55
+ }
56
+
57
+ # Safely read .aw-spec/feature.json's "feature_directory" value.
58
+ # Prints the raw value (possibly relative) to stdout, or empty string if the file
59
+ # is missing, unparseable, or does not contain the key. Always returns 0 so callers
60
+ # under `set -e` cannot be aborted by parser failure.
61
+ # Parser order mirrors the historical get_feature_paths behavior: jq -> python3 -> grep/sed.
62
+ read_feature_json_feature_directory() {
63
+ local repo_root="$1"
64
+ local fj="$repo_root/.aw-spec/feature.json"
65
+ [[ -f "$fj" ]] || { printf '%s' ''; return 0; }
66
+
67
+ local _fd=''
68
+ if command -v jq >/dev/null 2>&1; then
69
+ if ! _fd=$(jq -r '.feature_directory // empty' "$fj" 2>/dev/null); then
70
+ _fd=''
71
+ fi
72
+ elif command -v python3 >/dev/null 2>&1; then
73
+ # Use Python so pretty-printed/multi-line JSON still parses correctly.
74
+ if ! _fd=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); v=d.get('feature_directory'); print(v if v else '')" "$fj" 2>/dev/null); then
75
+ _fd=''
76
+ fi
77
+ else
78
+ # Last-resort single-line grep/sed fallback. The `|| true` guards against
79
+ # grep returning 1 (no match) aborting under `set -e` / `pipefail`.
80
+ _fd=$( { grep -E '"feature_directory"[[:space:]]*:' "$fj" 2>/dev/null || true; } \
81
+ | head -n 1 \
82
+ | sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' )
83
+ fi
84
+
85
+ printf '%s' "$_fd"
86
+ return 0
87
+ }
88
+
89
+ # Persist a feature_directory value to .aw-spec/feature.json.
90
+ # Writes only when the file is missing or the value differs from what's stored.
91
+ # Accepts the raw (possibly relative) path — callers should pass the original
92
+ # user-supplied value, not the normalized absolute path.
93
+ _persist_feature_json() {
94
+ local repo_root="$1"
95
+ local feature_dir_value="$2"
96
+ local fj="$repo_root/.aw-spec/feature.json"
97
+
98
+ # Strip repo_root prefix if the value is absolute and under repo_root
99
+ if [[ "$feature_dir_value" == "$repo_root/"* ]]; then
100
+ feature_dir_value="${feature_dir_value#"$repo_root/"}"
101
+ fi
102
+
103
+ # Read current value (if any) and skip write when unchanged
104
+ local current_val
105
+ current_val=$(read_feature_json_feature_directory "$repo_root")
106
+ if [[ "$current_val" == "$feature_dir_value" ]]; then
107
+ return 0
108
+ fi
109
+
110
+ # Ensure .aw-spec/ directory exists
111
+ mkdir -p "$repo_root/.aw-spec"
112
+
113
+ # Write feature.json — prefer jq for safe JSON, fall back to printf
114
+ if command -v jq >/dev/null 2>&1; then
115
+ jq -cn --arg fd "$feature_dir_value" '{feature_directory:$fd}' > "$fj"
116
+ else
117
+ printf '{"feature_directory":"%s"}\n' "$(json_escape "$feature_dir_value")" > "$fj"
118
+ fi
119
+ }
120
+
121
+ get_feature_paths() {
122
+ local repo_root=$(get_repo_root)
123
+ local current_branch=$(get_current_branch)
124
+
125
+ # Resolve feature directory. Priority:
126
+ # 1. SPECIFY_FEATURE_DIRECTORY env var (explicit override)
127
+ # 2. .aw-spec/feature.json "feature_directory" key (persisted by specify command)
128
+ # 3. Error — no feature context available
129
+ local feature_dir
130
+ if [[ -n "${SPECIFY_FEATURE_DIRECTORY:-}" ]]; then
131
+ feature_dir="$SPECIFY_FEATURE_DIRECTORY"
132
+ # Normalize relative paths to absolute under repo root
133
+ [[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
134
+ # Persist to feature.json so future sessions without the env var still work
135
+ _persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY"
136
+ elif [[ -f "$repo_root/.aw-spec/feature.json" ]]; then
137
+ local _fd
138
+ _fd=$(read_feature_json_feature_directory "$repo_root")
139
+ if [[ -n "$_fd" ]]; then
140
+ feature_dir="$_fd"
141
+ # Normalize relative paths to absolute under repo root
142
+ [[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
143
+ else
144
+ echo "ERROR: Feature directory not found. Set SPECIFY_FEATURE_DIRECTORY or ensure .aw-spec/feature.json contains feature_directory." >&2
145
+ return 1
146
+ fi
147
+ else
148
+ echo "ERROR: Feature directory not found. Set SPECIFY_FEATURE_DIRECTORY or run the specify command to create .aw-spec/feature.json." >&2
149
+ return 1
150
+ fi
151
+
152
+ # Use printf '%q' to safely quote values, preventing shell injection
153
+ # via crafted branch names or paths containing special characters
154
+ printf 'REPO_ROOT=%q\n' "$repo_root"
155
+ printf 'CURRENT_BRANCH=%q\n' "$current_branch"
156
+ printf 'FEATURE_DIR=%q\n' "$feature_dir"
157
+ printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"
158
+ printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"
159
+ printf 'TASKS=%q\n' "$feature_dir/tasks.md"
160
+ printf 'RESEARCH=%q\n' "$feature_dir/research.md"
161
+ printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md"
162
+ printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md"
163
+ printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts"
164
+ }
165
+
166
+ # Check if jq is available for safe JSON construction
167
+ has_jq() {
168
+ command -v jq >/dev/null 2>&1
169
+ }
170
+
171
+ get_invoke_separator() {
172
+ local repo_root="${1:-$(get_repo_root)}"
173
+ if [[ "${_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT:-}" == "$repo_root" && -n "${_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE:-}" ]]; then
174
+ printf '%s\n' "$_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE"
175
+ return 0
176
+ fi
177
+
178
+ local integration_json="$repo_root/.aw-spec/integration.json"
179
+ local separator="."
180
+ local parsed_with_jq=0
181
+
182
+ if [[ -f "$integration_json" ]]; then
183
+ if command -v jq >/dev/null 2>&1; then
184
+ local jq_separator
185
+ if jq_separator=$(jq -r '(.default_integration // .integration // "") as $k | if $k == "" then "." else (.integration_settings[$k].invoke_separator // ".") end' "$integration_json" 2>/dev/null); then
186
+ parsed_with_jq=1
187
+ case "$jq_separator" in
188
+ "."|"-") separator="$jq_separator" ;;
189
+ esac
190
+ fi
191
+ fi
192
+
193
+ if [[ "$parsed_with_jq" -eq 0 ]] && command -v python3 >/dev/null 2>&1; then
194
+ if separator=$(python3 - "$integration_json" <<'PY' 2>/dev/null
195
+ import json
196
+ import sys
197
+
198
+ try:
199
+ with open(sys.argv[1], encoding="utf-8") as fh:
200
+ state = json.load(fh)
201
+ key = state.get("default_integration") or state.get("integration") or ""
202
+ settings = state.get("integration_settings")
203
+ separator = "."
204
+ if isinstance(key, str) and isinstance(settings, dict):
205
+ entry = settings.get(key)
206
+ if isinstance(entry, dict) and entry.get("invoke_separator") in {".", "-"}:
207
+ separator = entry["invoke_separator"]
208
+ print(separator)
209
+ except Exception:
210
+ print(".")
211
+ PY
212
+ ); then
213
+ case "$separator" in
214
+ "."|"-") ;;
215
+ *) separator="." ;;
216
+ esac
217
+ else
218
+ separator="."
219
+ fi
220
+ fi
221
+ fi
222
+
223
+ _SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root"
224
+ _SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE="$separator"
225
+ printf '%s\n' "$separator"
226
+ }
227
+
228
+ format_aw-spec_command() {
229
+ local command_name="$1"
230
+ local repo_root="${2:-$(get_repo_root)}"
231
+ local separator
232
+ if [[ "${_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT:-}" == "$repo_root" && -n "${_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE:-}" ]]; then
233
+ separator="$_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE"
234
+ else
235
+ separator=$(get_invoke_separator "$repo_root")
236
+ _SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root"
237
+ _SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE="$separator"
238
+ fi
239
+
240
+ command_name="${command_name#/}"
241
+ command_name="${command_name#aw-spec.}"
242
+ command_name="${command_name#aw-spec-}"
243
+ command_name="${command_name//./$separator}"
244
+
245
+ printf '/aw-spec%s%s\n' "$separator" "$command_name"
246
+ }
247
+
248
+ # Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
249
+ # Handles backslash, double-quote, and JSON-required control character escapes (RFC 8259).
250
+ json_escape() {
251
+ local s="$1"
252
+ s="${s//\\/\\\\}"
253
+ s="${s//\"/\\\"}"
254
+ s="${s//$'\n'/\\n}"
255
+ s="${s//$'\t'/\\t}"
256
+ s="${s//$'\r'/\\r}"
257
+ s="${s//$'\b'/\\b}"
258
+ s="${s//$'\f'/\\f}"
259
+ # Escape any remaining U+0001-U+001F control characters as \uXXXX.
260
+ # (U+0000/NUL cannot appear in bash strings and is excluded.)
261
+ # LC_ALL=C ensures ${#s} counts bytes and ${s:$i:1} yields single bytes,
262
+ # so multi-byte UTF-8 sequences (first byte >= 0xC0) pass through intact.
263
+ local LC_ALL=C
264
+ local i char code
265
+ for (( i=0; i<${#s}; i++ )); do
266
+ char="${s:$i:1}"
267
+ printf -v code '%d' "'$char" 2>/dev/null || code=256
268
+ if (( code >= 1 && code <= 31 )); then
269
+ printf '\\u%04x' "$code"
270
+ else
271
+ printf '%s' "$char"
272
+ fi
273
+ done
274
+ }
275
+
276
+ check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
277
+ check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
278
+
279
+ # Resolve a template name to a file path using the priority stack:
280
+ # 1. .aw-spec/templates/overrides/
281
+ # 2. .aw-spec/presets/<preset-id>/templates/ (sorted by priority from .registry)
282
+ # 3. .aw-spec/extensions/<ext-id>/templates/
283
+ # 4. .aw-spec/templates/ (core)
284
+ resolve_template() {
285
+ local template_name="$1"
286
+ local repo_root="$2"
287
+ local base="$repo_root/.aw-spec/templates"
288
+
289
+ # Priority 1: Project overrides
290
+ local override="$base/overrides/${template_name}.md"
291
+ [ -f "$override" ] && echo "$override" && return 0
292
+
293
+ # Priority 2: Installed presets (sorted by priority from .registry)
294
+ local presets_dir="$repo_root/.aw-spec/presets"
295
+ if [ -d "$presets_dir" ]; then
296
+ local registry_file="$presets_dir/.registry"
297
+ if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
298
+ # Read preset IDs sorted by priority (lower number = higher precedence).
299
+ # The python3 call is wrapped in an if-condition so that set -e does not
300
+ # abort the function when python3 exits non-zero (e.g. invalid JSON).
301
+ local sorted_presets=""
302
+ if sorted_presets=$(AW_SPEC_REGISTRY="$registry_file" python3 -c "
303
+ import json, sys, os
304
+ try:
305
+ with open(os.environ['AW_SPEC_REGISTRY']) as f:
306
+ data = json.load(f)
307
+ presets = data.get('presets', {})
308
+ for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10):
309
+ if isinstance(meta, dict) and meta.get('enabled', True) is not False:
310
+ print(pid)
311
+ except Exception:
312
+ sys.exit(1)
313
+ " 2>/dev/null); then
314
+ if [ -n "$sorted_presets" ]; then
315
+ # python3 succeeded and returned preset IDs — search in priority order
316
+ while IFS= read -r preset_id; do
317
+ local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
318
+ [ -f "$candidate" ] && echo "$candidate" && return 0
319
+ done <<< "$sorted_presets"
320
+ fi
321
+ # python3 succeeded but registry has no presets — nothing to search
322
+ else
323
+ # python3 failed (missing, or registry parse error) — fall back to unordered directory scan
324
+ for preset in "$presets_dir"/*/; do
325
+ [ -d "$preset" ] || continue
326
+ local candidate="$preset/templates/${template_name}.md"
327
+ [ -f "$candidate" ] && echo "$candidate" && return 0
328
+ done
329
+ fi
330
+ else
331
+ # Fallback: alphabetical directory order (no python3 available)
332
+ for preset in "$presets_dir"/*/; do
333
+ [ -d "$preset" ] || continue
334
+ local candidate="$preset/templates/${template_name}.md"
335
+ [ -f "$candidate" ] && echo "$candidate" && return 0
336
+ done
337
+ fi
338
+ fi
339
+
340
+ # Priority 3: Extension-provided templates
341
+ local ext_dir="$repo_root/.aw-spec/extensions"
342
+ if [ -d "$ext_dir" ]; then
343
+ for ext in "$ext_dir"/*/; do
344
+ [ -d "$ext" ] || continue
345
+ # Skip hidden directories (e.g. .backup, .cache)
346
+ case "$(basename "$ext")" in .*) continue;; esac
347
+ local candidate="$ext/templates/${template_name}.md"
348
+ [ -f "$candidate" ] && echo "$candidate" && return 0
349
+ done
350
+ fi
351
+
352
+ # Priority 4: Core templates
353
+ local core="$base/${template_name}.md"
354
+ [ -f "$core" ] && echo "$core" && return 0
355
+
356
+ # Template not found in any location.
357
+ # Return 1 so callers can distinguish "not found" from "found".
358
+ # Callers running under set -e should use: TEMPLATE=$(resolve_template ...) || true
359
+ return 1
360
+ }
361
+
362
+ # Resolve a template name to composed content using composition strategies.
363
+ # Reads strategy metadata from preset manifests and composes content
364
+ # from multiple layers using prepend, append, or wrap strategies.
365
+ #
366
+ # Usage: CONTENT=$(resolve_template_content "template-name" "$REPO_ROOT")
367
+ # Returns composed content string on stdout; exit code 1 if not found.
368
+ resolve_template_content() {
369
+ local template_name="$1"
370
+ local repo_root="$2"
371
+ local base="$repo_root/.aw-spec/templates"
372
+
373
+ # Collect all layers (highest priority first)
374
+ local -a layer_paths=()
375
+ local -a layer_strategies=()
376
+
377
+ # Priority 1: Project overrides (always "replace")
378
+ local override="$base/overrides/${template_name}.md"
379
+ if [ -f "$override" ]; then
380
+ layer_paths+=("$override")
381
+ layer_strategies+=("replace")
382
+ fi
383
+
384
+ # Priority 2: Installed presets (sorted by priority from .registry)
385
+ local presets_dir="$repo_root/.aw-spec/presets"
386
+ if [ -d "$presets_dir" ]; then
387
+ local registry_file="$presets_dir/.registry"
388
+ local sorted_presets=""
389
+ if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
390
+ if sorted_presets=$(AW_SPEC_REGISTRY="$registry_file" python3 -c "
391
+ import json, sys, os
392
+ try:
393
+ with open(os.environ['AW_SPEC_REGISTRY']) as f:
394
+ data = json.load(f)
395
+ presets = data.get('presets', {})
396
+ for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10):
397
+ if isinstance(meta, dict) and meta.get('enabled', True) is not False:
398
+ print(pid)
399
+ except Exception:
400
+ sys.exit(1)
401
+ " 2>/dev/null); then
402
+ if [ -n "$sorted_presets" ]; then
403
+ local yaml_warned=false
404
+ while IFS= read -r preset_id; do
405
+ # Read strategy and file path from preset manifest
406
+ local strategy="replace"
407
+ local manifest_file=""
408
+ local manifest="$presets_dir/$preset_id/preset.yml"
409
+ if [ -f "$manifest" ] && command -v python3 >/dev/null 2>&1; then
410
+ # Requires PyYAML; falls back to replace/convention if unavailable
411
+ local result
412
+ local py_stderr
413
+ py_stderr=$(mktemp)
414
+ result=$(AW_SPEC_MANIFEST="$manifest" AW_SPEC_TMPL="$template_name" python3 -c "
415
+ import sys, os
416
+ try:
417
+ import yaml
418
+ except ImportError:
419
+ print('yaml_missing', file=sys.stderr)
420
+ print('replace\t')
421
+ sys.exit(0)
422
+ try:
423
+ with open(os.environ['AW_SPEC_MANIFEST']) as f:
424
+ data = yaml.safe_load(f)
425
+ for t in data.get('provides', {}).get('templates', []):
426
+ if t.get('name') == os.environ['AW_SPEC_TMPL'] and t.get('type', 'template') == 'template':
427
+ print(t.get('strategy', 'replace') + '\t' + t.get('file', ''))
428
+ sys.exit(0)
429
+ print('replace\t')
430
+ except Exception:
431
+ print('replace\t')
432
+ " 2>"$py_stderr")
433
+ local parse_status=$?
434
+ if [ $parse_status -eq 0 ] && [ -n "$result" ]; then
435
+ IFS=$'\t' read -r strategy manifest_file <<< "$result"
436
+ strategy=$(printf '%s' "$strategy" | tr '[:upper:]' '[:lower:]')
437
+ fi
438
+ if [ "$yaml_warned" = false ] && grep -q 'yaml_missing' "$py_stderr" 2>/dev/null; then
439
+ echo "Warning: PyYAML not available; composition strategies may be ignored" >&2
440
+ yaml_warned=true
441
+ fi
442
+ rm -f "$py_stderr"
443
+ fi
444
+ # Try manifest file path first, then convention path
445
+ local candidate=""
446
+ if [ -n "$manifest_file" ]; then
447
+ # Reject absolute paths and parent traversal
448
+ case "$manifest_file" in
449
+ /*|*../*|../*) manifest_file="" ;;
450
+ esac
451
+ fi
452
+ if [ -n "$manifest_file" ]; then
453
+ local mf="$presets_dir/$preset_id/$manifest_file"
454
+ [ -f "$mf" ] && candidate="$mf"
455
+ fi
456
+ if [ -z "$candidate" ]; then
457
+ local cf="$presets_dir/$preset_id/templates/${template_name}.md"
458
+ [ -f "$cf" ] && candidate="$cf"
459
+ fi
460
+ if [ -n "$candidate" ]; then
461
+ layer_paths+=("$candidate")
462
+ layer_strategies+=("$strategy")
463
+ fi
464
+ done <<< "$sorted_presets"
465
+ fi
466
+ else
467
+ # python3 failed — fall back to unordered directory scan (replace only)
468
+ for preset in "$presets_dir"/*/; do
469
+ [ -d "$preset" ] || continue
470
+ local candidate="$preset/templates/${template_name}.md"
471
+ if [ -f "$candidate" ]; then
472
+ layer_paths+=("$candidate")
473
+ layer_strategies+=("replace")
474
+ fi
475
+ done
476
+ fi
477
+ else
478
+ # No python3 or registry — fall back to unordered directory scan (replace only)
479
+ for preset in "$presets_dir"/*/; do
480
+ [ -d "$preset" ] || continue
481
+ local candidate="$preset/templates/${template_name}.md"
482
+ if [ -f "$candidate" ]; then
483
+ layer_paths+=("$candidate")
484
+ layer_strategies+=("replace")
485
+ fi
486
+ done
487
+ fi
488
+ fi
489
+
490
+ # Priority 3: Extension-provided templates (always "replace")
491
+ local ext_dir="$repo_root/.aw-spec/extensions"
492
+ if [ -d "$ext_dir" ]; then
493
+ for ext in "$ext_dir"/*/; do
494
+ [ -d "$ext" ] || continue
495
+ case "$(basename "$ext")" in .*) continue;; esac
496
+ local candidate="$ext/templates/${template_name}.md"
497
+ if [ -f "$candidate" ]; then
498
+ layer_paths+=("$candidate")
499
+ layer_strategies+=("replace")
500
+ fi
501
+ done
502
+ fi
503
+
504
+ # Priority 4: Core templates (always "replace")
505
+ local core="$base/${template_name}.md"
506
+ if [ -f "$core" ]; then
507
+ layer_paths+=("$core")
508
+ layer_strategies+=("replace")
509
+ fi
510
+
511
+ local count=${#layer_paths[@]}
512
+ [ "$count" -eq 0 ] && return 1
513
+
514
+ # Check if any layer uses a non-replace strategy
515
+ local has_composition=false
516
+ for s in "${layer_strategies[@]}"; do
517
+ [ "$s" != "replace" ] && has_composition=true && break
518
+ done
519
+
520
+ # If the top (highest-priority) layer is replace, it wins entirely —
521
+ # lower layers are irrelevant regardless of their strategies.
522
+ if [ "${layer_strategies[0]}" = "replace" ]; then
523
+ cat "${layer_paths[0]}"
524
+ return 0
525
+ fi
526
+
527
+ if [ "$has_composition" = false ]; then
528
+ cat "${layer_paths[0]}"
529
+ return 0
530
+ fi
531
+
532
+ # Find the effective base: scan from highest priority (index 0) downward
533
+ # to find the nearest replace layer. Only compose layers above that base.
534
+ local base_idx=-1
535
+ local i
536
+ for (( i=0; i<count; i++ )); do
537
+ if [ "${layer_strategies[$i]}" = "replace" ]; then
538
+ base_idx=$i
539
+ break
540
+ fi
541
+ done
542
+
543
+ if [ $base_idx -lt 0 ]; then
544
+ return 1 # no base layer found
545
+ fi
546
+
547
+ # Read the base content; compose layers above the base (higher priority)
548
+ local content
549
+ content=$(cat "${layer_paths[$base_idx]}"; printf x)
550
+ content="${content%x}"
551
+
552
+ for (( i=base_idx-1; i>=0; i-- )); do
553
+ local path="${layer_paths[$i]}"
554
+ local strat="${layer_strategies[$i]}"
555
+ local layer_content
556
+ # Preserve trailing newlines
557
+ layer_content=$(cat "$path"; printf x)
558
+ layer_content="${layer_content%x}"
559
+
560
+ case "$strat" in
561
+ replace) content="$layer_content" ;;
562
+ prepend) content="$(printf '%s\n\n%s' "$layer_content" "$content")" ;;
563
+ append) content="$(printf '%s\n\n%s' "$content" "$layer_content")" ;;
564
+ wrap)
565
+ case "$layer_content" in
566
+ *'{CORE_TEMPLATE}'*) ;;
567
+ *) echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2; return 1 ;;
568
+ esac
569
+ while [[ "$layer_content" == *'{CORE_TEMPLATE}'* ]]; do
570
+ local before="${layer_content%%\{CORE_TEMPLATE\}*}"
571
+ local after="${layer_content#*\{CORE_TEMPLATE\}}"
572
+ layer_content="${before}${content}${after}"
573
+ done
574
+ content="$layer_content"
575
+ ;;
576
+ *) echo "Error: unknown strategy '$strat'" >&2; return 1 ;;
577
+ esac
578
+ done
579
+
580
+ printf '%s' "$content"
581
+ return 0
582
+ }