@ncoderz/awa 1.0.0 → 1.2.0

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 (128) hide show
  1. package/README.md +96 -16
  2. package/dist/chunk-3SSUJFKN.js +625 -0
  3. package/dist/chunk-3SSUJFKN.js.map +1 -0
  4. package/dist/config-2TOQATI3.js +10 -0
  5. package/dist/config-2TOQATI3.js.map +1 -0
  6. package/dist/index.js +2190 -414
  7. package/dist/index.js.map +1 -1
  8. package/package.json +10 -4
  9. package/templates/awa/.agent/skills/awa-align/SKILL.md +3 -0
  10. package/templates/awa/.agent/skills/awa-check/SKILL.md +4 -0
  11. package/templates/awa/.agent/skills/awa-usage/SKILL.md +3 -0
  12. package/templates/awa/.agent/workflows/awa-align.md +3 -0
  13. package/templates/awa/.agent/workflows/awa-check.md +4 -0
  14. package/templates/awa/.agents/skills/awa-align/SKILL.md +3 -0
  15. package/templates/awa/.agents/skills/awa-check/SKILL.md +4 -0
  16. package/templates/awa/.agents/skills/awa-usage/SKILL.md +3 -0
  17. package/templates/awa/.awa/.agent/schemas/ALIGN_REPORT.schema.yaml +83 -0
  18. package/templates/awa/.awa/.agent/schemas/API.schema.yaml +7 -0
  19. package/templates/awa/.awa/.agent/schemas/ARCHITECTURE.schema.yaml +260 -0
  20. package/templates/awa/.awa/.agent/schemas/DESIGN.schema.yaml +361 -0
  21. package/templates/awa/.awa/.agent/schemas/EXAMPLES.schema.yaml +98 -0
  22. package/templates/awa/.awa/.agent/schemas/FEAT.schema.yaml +143 -0
  23. package/templates/awa/.awa/.agent/schemas/PLAN.schema.yaml +151 -0
  24. package/templates/awa/.awa/.agent/schemas/README.schema.yaml +137 -0
  25. package/templates/awa/.awa/.agent/schemas/REQ.schema.yaml +169 -0
  26. package/templates/awa/.awa/.agent/schemas/TASK.schema.yaml +200 -0
  27. package/templates/awa/.claude/agents/awa.md +2 -2
  28. package/templates/awa/.claude/skills/awa-align/SKILL.md +3 -0
  29. package/templates/awa/.claude/skills/awa-check/SKILL.md +4 -0
  30. package/templates/awa/.claude/skills/awa-usage/SKILL.md +3 -0
  31. package/templates/awa/.codex/prompts/awa-align.md +3 -0
  32. package/templates/awa/.codex/prompts/awa-check.md +4 -0
  33. package/templates/awa/.cursor/rules/awa-agent.md +1 -1
  34. package/templates/awa/.cursor/rules/awa-align.md +8 -0
  35. package/templates/awa/.cursor/rules/awa-check.md +9 -0
  36. package/templates/awa/.gemini/commands/awa-align.md +3 -0
  37. package/templates/awa/.gemini/commands/awa-check.md +4 -0
  38. package/templates/awa/.gemini/skills/awa-align/SKILL.md +3 -0
  39. package/templates/awa/.gemini/skills/awa-check/SKILL.md +4 -0
  40. package/templates/awa/.gemini/skills/awa-usage/SKILL.md +3 -0
  41. package/templates/awa/.github/agents/awa.agent.md +2 -2
  42. package/templates/awa/.github/prompts/awa.align.prompt.md +8 -0
  43. package/templates/awa/.github/prompts/awa.check.prompt.md +9 -0
  44. package/templates/awa/.github/skills/awa-align/SKILL.md +8 -0
  45. package/templates/awa/.github/skills/awa-check/SKILL.md +9 -0
  46. package/templates/awa/.github/skills/awa-usage/SKILL.md +8 -0
  47. package/templates/awa/.kilocode/rules/awa-agent.md +1 -1
  48. package/templates/awa/.kilocode/skills/awa-align/SKILL.md +3 -0
  49. package/templates/awa/.kilocode/skills/awa-check/SKILL.md +4 -0
  50. package/templates/awa/.kilocode/skills/awa-usage/SKILL.md +3 -0
  51. package/templates/awa/.kilocode/workflows/awa-align.md +3 -0
  52. package/templates/awa/.kilocode/workflows/awa-check.md +4 -0
  53. package/templates/awa/.opencode/agents/awa.md +2 -2
  54. package/templates/awa/.opencode/commands/awa-align.md +3 -0
  55. package/templates/awa/.opencode/commands/awa-check.md +4 -0
  56. package/templates/awa/.opencode/skills/awa-align/SKILL.md +3 -0
  57. package/templates/awa/.opencode/skills/awa-check/SKILL.md +4 -0
  58. package/templates/awa/.opencode/skills/awa-usage/SKILL.md +3 -0
  59. package/templates/awa/.qwen/commands/awa-align.md +3 -0
  60. package/templates/awa/.qwen/commands/awa-check.md +4 -0
  61. package/templates/awa/.qwen/skills/awa-align/SKILL.md +3 -0
  62. package/templates/awa/.qwen/skills/awa-check/SKILL.md +4 -0
  63. package/templates/awa/.qwen/skills/awa-usage/SKILL.md +3 -0
  64. package/templates/awa/.roo/rules/awa-agent.md +1 -1
  65. package/templates/awa/.roo/skills/awa-align/SKILL.md +3 -0
  66. package/templates/awa/.roo/skills/awa-check/SKILL.md +4 -0
  67. package/templates/awa/.roo/skills/awa-usage/SKILL.md +3 -0
  68. package/templates/awa/.windsurf/rules/awa-agent.md +1 -1
  69. package/templates/awa/.windsurf/skills/awa-align/SKILL.md +3 -0
  70. package/templates/awa/.windsurf/skills/awa-check/SKILL.md +4 -0
  71. package/templates/awa/.windsurf/skills/awa-usage/SKILL.md +3 -0
  72. package/templates/awa/AGENTS.md +1 -1
  73. package/templates/awa/CLAUDE.md +1 -1
  74. package/templates/awa/GEMINI.md +1 -1
  75. package/templates/awa/QWEN.md +1 -1
  76. package/templates/awa/_README.md +3 -2
  77. package/templates/awa/_delete.txt +49 -0
  78. package/templates/awa/_partials/{_cmd.awa-validate-alignment.md → _cmd.awa-align.md} +1 -1
  79. package/templates/awa/_partials/_cmd.awa-check.md +6 -0
  80. package/templates/awa/_partials/_skill.awa-align.md +6 -0
  81. package/templates/awa/_partials/_skill.awa-check.md +6 -0
  82. package/templates/awa/_partials/_skill.awa-usage.md +6 -0
  83. package/templates/awa/_partials/{awa.validate-alignment.md → awa.align.md} +2 -2
  84. package/templates/awa/_partials/awa.architecture.md +1 -1
  85. package/templates/awa/_partials/awa.check.md +73 -0
  86. package/templates/awa/_partials/awa.code.md +1 -0
  87. package/templates/awa/_partials/awa.core.md +24 -10
  88. package/templates/awa/_partials/awa.design.md +3 -2
  89. package/templates/awa/_partials/awa.documentation.md +1 -1
  90. package/templates/awa/_partials/awa.examples.md +1 -1
  91. package/templates/awa/_partials/awa.feature.md +1 -1
  92. package/templates/awa/_partials/awa.plan.md +1 -1
  93. package/templates/awa/_partials/awa.refactor.md +1 -0
  94. package/templates/awa/_partials/awa.requirements.md +2 -1
  95. package/templates/awa/_partials/awa.tasks.md +3 -3
  96. package/templates/awa/_partials/awa.upgrade.md +13 -12
  97. package/templates/awa/_partials/awa.usage.md +265 -0
  98. package/templates/awa/_tests/claude.toml +7 -0
  99. package/templates/awa/_tests/copilot.toml +6 -0
  100. package/templates/awa/.agent/skills/awa-validate-alignment/SKILL.md +0 -3
  101. package/templates/awa/.agent/workflows/awa-validate-alignment.md +0 -3
  102. package/templates/awa/.agents/skills/awa-validate-alignment/SKILL.md +0 -3
  103. package/templates/awa/.awa/.agent/schemas/ALIGN_REPORT.schema.md +0 -156
  104. package/templates/awa/.awa/.agent/schemas/API.schema.md +0 -4
  105. package/templates/awa/.awa/.agent/schemas/ARCHITECTURE.schema.md +0 -176
  106. package/templates/awa/.awa/.agent/schemas/DESIGN.schema.md +0 -253
  107. package/templates/awa/.awa/.agent/schemas/EXAMPLES.schema.md +0 -51
  108. package/templates/awa/.awa/.agent/schemas/FEAT.schema.md +0 -61
  109. package/templates/awa/.awa/.agent/schemas/PLAN.schema.md +0 -8
  110. package/templates/awa/.awa/.agent/schemas/README.schema.md +0 -133
  111. package/templates/awa/.awa/.agent/schemas/REQ.schema.md +0 -125
  112. package/templates/awa/.awa/.agent/schemas/TASK.schema.md +0 -137
  113. package/templates/awa/.claude/skills/awa-validate-alignment/SKILL.md +0 -3
  114. package/templates/awa/.codex/prompts/awa-validate-alignment.md +0 -3
  115. package/templates/awa/.cursor/rules/awa-validate-alignment.md +0 -8
  116. package/templates/awa/.gemini/commands/awa-validate-alignment.md +0 -3
  117. package/templates/awa/.gemini/skills/awa-validate-alignment/SKILL.md +0 -3
  118. package/templates/awa/.github/prompts/awa.validate-alignment.prompt.md +0 -8
  119. package/templates/awa/.github/skills/awa-validate-alignment/SKILL.md +0 -8
  120. package/templates/awa/.kilocode/skills/awa-validate-alignment/SKILL.md +0 -3
  121. package/templates/awa/.kilocode/workflows/awa-validate-alignment.md +0 -3
  122. package/templates/awa/.opencode/commands/awa-validate-alignment.md +0 -3
  123. package/templates/awa/.opencode/skills/awa-validate-alignment/SKILL.md +0 -3
  124. package/templates/awa/.qwen/commands/awa-validate-alignment.md +0 -3
  125. package/templates/awa/.qwen/skills/awa-validate-alignment/SKILL.md +0 -3
  126. package/templates/awa/.roo/skills/awa-validate-alignment/SKILL.md +0 -3
  127. package/templates/awa/.windsurf/skills/awa-validate-alignment/SKILL.md +0 -3
  128. package/templates/awa/_partials/_skill.awa-validate-alignment.md +0 -6
package/dist/index.js CHANGED
@@ -1,4 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ ConfigError,
4
+ DiffError,
5
+ GenerationError,
6
+ TemplateError,
7
+ configLoader,
8
+ deleteFile,
9
+ ensureDir,
10
+ getCacheDir,
11
+ getTemplateDir,
12
+ logger,
13
+ pathExists,
14
+ readBinaryFile,
15
+ readTextFile,
16
+ rmDir,
17
+ walkDirectory,
18
+ writeTextFile
19
+ } from "./chunk-3SSUJFKN.js";
2
20
 
3
21
  // src/cli/index.ts
4
22
  import { Command } from "commander";
@@ -6,431 +24,1336 @@ import { Command } from "commander";
6
24
  // src/_generated/package_info.ts
7
25
  var PACKAGE_INFO = {
8
26
  "name": "@ncoderz/awa",
9
- "version": "1.0.0",
27
+ "version": "1.2.0",
10
28
  "author": "Richard Sewell <richard.sewell@ncoderz.com>",
11
29
  "license": "MIT",
12
30
  "description": "awa is an Agent Workflow for AIs. It is also a CLI tool to powerfully manage agent workflow files using templates."
13
31
  };
14
32
 
15
- // src/commands/diff.ts
16
- import { intro, outro } from "@clack/prompts";
17
-
18
- // src/core/config.ts
19
- import { parse } from "smol-toml";
20
-
21
- // src/types/index.ts
22
- var DiffError = class extends Error {
23
- constructor(message) {
24
- super(message);
25
- this.name = "DiffError";
33
+ // src/core/check/code-spec-checker.ts
34
+ function checkCodeAgainstSpec(markers, specs, config) {
35
+ const findings = [];
36
+ const idRegex = new RegExp(`^${config.idPattern}$`);
37
+ for (const marker of markers.markers) {
38
+ if (marker.type === "component") {
39
+ continue;
40
+ }
41
+ if (!idRegex.test(marker.id)) {
42
+ findings.push({
43
+ severity: "error",
44
+ code: "invalid-id-format",
45
+ message: `Marker ID '${marker.id}' does not match expected pattern: ${config.idPattern}`,
46
+ filePath: marker.filePath,
47
+ line: marker.line,
48
+ id: marker.id
49
+ });
50
+ }
26
51
  }
27
- };
28
- var ConfigError = class extends Error {
29
- constructor(message, code, filePath) {
30
- super(message);
31
- this.code = code;
32
- this.filePath = filePath;
33
- this.name = "ConfigError";
52
+ for (const marker of markers.markers) {
53
+ if (marker.type === "component") {
54
+ if (!specs.componentNames.has(marker.id)) {
55
+ findings.push({
56
+ severity: "error",
57
+ code: "orphaned-marker",
58
+ message: `Component marker '${marker.id}' not found in any spec file`,
59
+ filePath: marker.filePath,
60
+ line: marker.line,
61
+ id: marker.id
62
+ });
63
+ }
64
+ } else {
65
+ if (!specs.allIds.has(marker.id)) {
66
+ findings.push({
67
+ severity: "error",
68
+ code: "orphaned-marker",
69
+ message: `Marker '${marker.id}' not found in any spec file`,
70
+ filePath: marker.filePath,
71
+ line: marker.line,
72
+ id: marker.id
73
+ });
74
+ }
75
+ }
34
76
  }
35
- };
36
- var TemplateError = class extends Error {
37
- constructor(message, code, source) {
38
- super(message);
39
- this.code = code;
40
- this.source = source;
41
- this.name = "TemplateError";
77
+ const testedIds = new Set(markers.markers.filter((m) => m.type === "test").map((m) => m.id));
78
+ for (const acId of specs.acIds) {
79
+ if (!testedIds.has(acId)) {
80
+ const loc = specs.idLocations.get(acId);
81
+ const specFile = loc ? void 0 : specs.specFiles.find((sf) => sf.acIds.includes(acId));
82
+ findings.push({
83
+ severity: "warning",
84
+ code: "uncovered-ac",
85
+ message: `Acceptance criterion '${acId}' has no @awa-test reference`,
86
+ filePath: loc?.filePath ?? specFile?.filePath,
87
+ line: loc?.line,
88
+ id: acId
89
+ });
90
+ }
42
91
  }
43
- };
44
- var GenerationError = class extends Error {
45
- constructor(message, code) {
46
- super(message);
47
- this.code = code;
48
- this.name = "GenerationError";
92
+ return { findings };
93
+ }
94
+
95
+ // src/core/check/marker-scanner.ts
96
+ import { readFile } from "fs/promises";
97
+
98
+ // src/core/check/glob.ts
99
+ import { glob } from "fs/promises";
100
+ function matchSimpleGlob(path, pattern) {
101
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "<<GLOBSTAR>>").replace(/\*/g, "[^/]*").replace(/<<GLOBSTAR>>/g, ".*");
102
+ return new RegExp(`(^|/)${regex}($|/)`).test(path);
103
+ }
104
+ async function collectFiles(globs, ignore) {
105
+ const files = [];
106
+ const dirPrefixes = ignore.filter((ig) => ig.endsWith("/**")).map((ig) => ig.slice(0, -3));
107
+ for (const pattern of globs) {
108
+ for await (const filePath of glob(pattern, {
109
+ exclude: (p) => dirPrefixes.includes(p) || ignore.some((ig) => matchSimpleGlob(p, ig))
110
+ })) {
111
+ if (!ignore.some((ig) => matchSimpleGlob(filePath, ig))) {
112
+ files.push(filePath);
113
+ }
114
+ }
49
115
  }
50
- };
116
+ return [...new Set(files)];
117
+ }
51
118
 
52
- // src/utils/fs.ts
53
- import { mkdir, readdir, readFile, rm, stat, writeFile } from "fs/promises";
54
- import { homedir } from "os";
55
- import { dirname, join } from "path";
56
- import { fileURLToPath } from "url";
57
- async function ensureDir(dirPath) {
58
- await mkdir(dirPath, { recursive: true });
119
+ // src/core/check/marker-scanner.ts
120
+ var MARKER_TYPE_MAP = {
121
+ "@awa-impl": "impl",
122
+ "@awa-test": "test",
123
+ "@awa-component": "component"
124
+ };
125
+ var IGNORE_FILE_RE = /@awa-ignore-file\b/;
126
+ var IGNORE_NEXT_LINE_RE = /@awa-ignore-next-line\b/;
127
+ var IGNORE_LINE_RE = /@awa-ignore\b/;
128
+ var IGNORE_START_RE = /@awa-ignore-start\b/;
129
+ var IGNORE_END_RE = /@awa-ignore-end\b/;
130
+ async function scanMarkers(config) {
131
+ const files = await collectCodeFiles(config.codeGlobs, config.codeIgnore);
132
+ const markers = [];
133
+ const findings = [];
134
+ for (const filePath of files) {
135
+ if (config.ignoreMarkers.some((ig) => matchSimpleGlob(filePath, ig))) {
136
+ continue;
137
+ }
138
+ const result = await scanFile(filePath, config.markers);
139
+ markers.push(...result.markers);
140
+ findings.push(...result.findings);
141
+ }
142
+ return { markers, findings };
143
+ }
144
+ function buildMarkerRegex(markerNames) {
145
+ const escaped = markerNames.map((m) => m.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
146
+ return new RegExp(`(${escaped.join("|")}):\\s*(.+)`, "g");
59
147
  }
60
- async function pathExists(path) {
148
+ var ID_TOKEN_RE = /^([A-Z][A-Z0-9]*(?:[-_][A-Za-z0-9]+)*(?:\.\d+)?)/;
149
+ async function scanFile(filePath, markerNames) {
150
+ let content;
61
151
  try {
62
- await stat(path);
63
- return true;
152
+ content = await readFile(filePath, "utf-8");
64
153
  } catch {
65
- return false;
154
+ return { markers: [], findings: [] };
66
155
  }
156
+ if (IGNORE_FILE_RE.test(content)) {
157
+ return { markers: [], findings: [] };
158
+ }
159
+ const regex = buildMarkerRegex(markerNames);
160
+ const lines = content.split("\n");
161
+ const markers = [];
162
+ const findings = [];
163
+ let ignoreNextLine = false;
164
+ let ignoreBlock = false;
165
+ for (const [i, line] of lines.entries()) {
166
+ if (IGNORE_START_RE.test(line)) {
167
+ ignoreBlock = true;
168
+ continue;
169
+ }
170
+ if (IGNORE_END_RE.test(line)) {
171
+ ignoreBlock = false;
172
+ continue;
173
+ }
174
+ if (ignoreBlock) {
175
+ continue;
176
+ }
177
+ if (ignoreNextLine) {
178
+ ignoreNextLine = false;
179
+ continue;
180
+ }
181
+ if (IGNORE_NEXT_LINE_RE.test(line)) {
182
+ ignoreNextLine = true;
183
+ continue;
184
+ }
185
+ if (IGNORE_LINE_RE.test(line)) {
186
+ continue;
187
+ }
188
+ regex.lastIndex = 0;
189
+ let match = regex.exec(line);
190
+ while (match !== null) {
191
+ const markerName = match[1] ?? "";
192
+ const idsRaw = match[2] ?? "";
193
+ const type = resolveMarkerType(markerName, markerNames);
194
+ const ids = idsRaw.split(",").map((id) => id.trim()).filter(Boolean);
195
+ for (const id of ids) {
196
+ const tokenMatch = ID_TOKEN_RE.exec(id);
197
+ const cleanId = tokenMatch?.[1]?.trim() ?? "";
198
+ if (cleanId && tokenMatch) {
199
+ const remainder = id.slice(tokenMatch[0].length).trim();
200
+ if (remainder) {
201
+ findings.push({
202
+ severity: "error",
203
+ code: "marker-trailing-text",
204
+ message: `Marker has trailing text after ID '${cleanId}': '${remainder}' \u2014 use comma-separated IDs only`,
205
+ filePath,
206
+ line: i + 1,
207
+ id: cleanId
208
+ });
209
+ }
210
+ markers.push({ type, id: cleanId, filePath, line: i + 1 });
211
+ }
212
+ }
213
+ match = regex.exec(line);
214
+ }
215
+ }
216
+ return { markers, findings };
67
217
  }
68
- async function readTextFile(path) {
69
- return readFile(path, "utf-8");
70
- }
71
- async function readBinaryFile(path) {
72
- return readFile(path);
218
+ function resolveMarkerType(markerName, configuredMarkers) {
219
+ const mapped = MARKER_TYPE_MAP[markerName];
220
+ if (mapped) return mapped;
221
+ const idx = configuredMarkers.indexOf(markerName);
222
+ if (idx === 1) return "test";
223
+ if (idx === 2) return "component";
224
+ return "impl";
73
225
  }
74
- async function writeTextFile(path, content) {
75
- await ensureDir(dirname(path));
76
- await writeFile(path, content, "utf-8");
226
+ async function collectCodeFiles(codeGlobs, ignore) {
227
+ return collectFiles(codeGlobs, ignore);
77
228
  }
78
- async function* walkDirectory(dir) {
79
- const entries = await readdir(dir, { withFileTypes: true });
80
- for (const entry of entries) {
81
- const fullPath = join(dir, entry.name);
82
- if (entry.isDirectory()) {
83
- if (entry.name.startsWith("_")) {
84
- continue;
85
- }
86
- yield* walkDirectory(fullPath);
87
- } else if (entry.isFile()) {
88
- if (entry.name.startsWith("_")) {
89
- continue;
90
- }
91
- yield fullPath;
92
- }
229
+
230
+ // src/core/check/reporter.ts
231
+ import chalk from "chalk";
232
+ function report(findings, format) {
233
+ if (format === "json") {
234
+ reportJson(findings);
235
+ } else {
236
+ reportText(findings);
93
237
  }
94
238
  }
95
- function getCacheDir() {
96
- return join(homedir(), ".cache", "awa", "templates");
239
+ function reportJson(findings) {
240
+ const errors = findings.filter((f) => f.severity === "error");
241
+ const warnings = findings.filter((f) => f.severity === "warning");
242
+ const output = {
243
+ valid: errors.length === 0,
244
+ errors: errors.length,
245
+ warnings: warnings.length,
246
+ findings: findings.map((f) => ({
247
+ severity: f.severity,
248
+ code: f.code,
249
+ message: f.message,
250
+ ...f.filePath ? { filePath: f.filePath } : {},
251
+ ...f.line ? { line: f.line } : {},
252
+ ...f.id ? { id: f.id } : {},
253
+ ...f.ruleSource ? { ruleSource: f.ruleSource } : {},
254
+ ...f.rule ? { rule: f.rule } : {}
255
+ }))
256
+ };
257
+ console.log(JSON.stringify(output, null, 2));
97
258
  }
98
- function getTemplateDir() {
99
- const currentFile = fileURLToPath(import.meta.url);
100
- const currentDir = dirname(currentFile);
101
- if (currentDir.includes("/dist")) {
102
- return join(dirname(currentDir), "templates");
259
+ function reportText(findings) {
260
+ const errors = findings.filter((f) => f.severity === "error");
261
+ const warnings = findings.filter((f) => f.severity === "warning");
262
+ if (errors.length > 0) {
263
+ console.log(chalk.red(`
264
+ ${errors.length} error(s):
265
+ `));
266
+ for (const f of errors) {
267
+ const location = formatLocation(f.filePath, f.line);
268
+ console.log(chalk.red(" \u2716"), f.message, location ? chalk.dim(location) : "");
269
+ printRuleContext(f);
270
+ }
271
+ }
272
+ if (warnings.length > 0) {
273
+ console.log(chalk.yellow(`
274
+ ${warnings.length} warning(s):
275
+ `));
276
+ for (const f of warnings) {
277
+ const location = formatLocation(f.filePath, f.line);
278
+ console.log(chalk.yellow(" \u26A0"), f.message, location ? chalk.dim(location) : "");
279
+ printRuleContext(f);
280
+ }
281
+ }
282
+ if (errors.length === 0 && warnings.length === 0) {
283
+ console.log(chalk.green("\n\u2714 Validation passed \u2014 no issues found"));
284
+ } else {
285
+ console.log("");
286
+ const parts = [];
287
+ if (errors.length > 0) parts.push(chalk.red(`${errors.length} error(s)`));
288
+ if (warnings.length > 0) parts.push(chalk.yellow(`${warnings.length} warning(s)`));
289
+ console.log(`Summary: ${parts.join(", ")}`);
103
290
  }
104
- return join(currentDir, "..", "..", "templates");
105
291
  }
106
- async function rmDir(dirPath) {
107
- await rm(dirPath, { recursive: true, force: true });
292
+ function formatLocation(filePath, line) {
293
+ if (!filePath) return "";
294
+ return line ? `(${filePath}:${line})` : `(${filePath})`;
108
295
  }
109
- async function deleteFile(filePath) {
110
- await rm(filePath, { force: true });
296
+ function printRuleContext(f) {
297
+ if (!f.ruleSource && !f.rule) return;
298
+ const parts = [];
299
+ if (f.ruleSource) parts.push(f.ruleSource);
300
+ if (f.rule) parts.push(f.rule);
301
+ console.log(chalk.dim(` rule: ${parts.join(" \u2014 ")}`));
111
302
  }
112
303
 
113
- // src/utils/logger.ts
114
- import chalk from "chalk";
115
- var Logger = class {
116
- info(message) {
117
- console.log(chalk.blue("\u2139"), message);
304
+ // src/core/check/rule-loader.ts
305
+ import { readFile as readFile2 } from "fs/promises";
306
+ import { join } from "path";
307
+ import { parse as parseYaml } from "yaml";
308
+ async function loadRules(schemaDir) {
309
+ const pattern = join(schemaDir, "*.schema.yaml");
310
+ const files = await collectFiles([pattern], []);
311
+ const results = [];
312
+ for (const filePath of files) {
313
+ const ruleSet = await loadRuleFile(filePath);
314
+ if (ruleSet) {
315
+ results.push(ruleSet);
316
+ }
118
317
  }
119
- success(message) {
120
- console.log(chalk.green("\u2714"), message);
318
+ return results;
319
+ }
320
+ function matchesTargetGlob(filePath, targetGlob) {
321
+ return matchSimpleGlob(filePath, targetGlob);
322
+ }
323
+ async function loadRuleFile(filePath) {
324
+ let content;
325
+ try {
326
+ content = await readFile2(filePath, "utf-8");
327
+ } catch {
328
+ return null;
121
329
  }
122
- warn(message) {
123
- console.warn(chalk.yellow("\u26A0"), message);
330
+ const parsed = parseYaml(content);
331
+ if (!parsed || typeof parsed !== "object") {
332
+ throw new RuleValidationError(`Rule file is not a valid YAML object: ${filePath}`);
124
333
  }
125
- error(message) {
126
- console.error(chalk.red("\u2716"), message);
334
+ const raw = parsed;
335
+ const ruleFile = validateRuleFile(raw, filePath);
336
+ return {
337
+ ruleFile,
338
+ sourcePath: filePath,
339
+ targetGlob: ruleFile["target-files"]
340
+ };
341
+ }
342
+ function validateRuleFile(raw, filePath) {
343
+ if (typeof raw["target-files"] !== "string" || raw["target-files"].length === 0) {
344
+ throw new RuleValidationError(`Missing or empty 'target-files' in ${filePath}`);
127
345
  }
128
- fileAction(action) {
129
- const { type, outputPath } = action;
130
- switch (type) {
131
- case "create":
132
- console.log(chalk.green(" + "), chalk.dim(outputPath));
133
- break;
134
- case "overwrite":
135
- console.log(chalk.yellow(" ~ "), chalk.dim(outputPath));
136
- break;
137
- case "skip-user":
138
- console.log(chalk.blue(" - "), chalk.dim(outputPath), chalk.dim("(skipped)"));
139
- break;
140
- case "skip-empty":
141
- console.log(chalk.dim(" \xB7 "), chalk.dim(outputPath), chalk.dim("(empty)"));
142
- break;
143
- case "skip-equal":
144
- console.log(chalk.dim(" = "), chalk.dim(outputPath), chalk.dim("(unchanged)"));
145
- break;
146
- case "delete":
147
- console.log(chalk.red(" \u2716 "), chalk.dim(outputPath), chalk.red("(deleted)"));
148
- break;
346
+ let sections;
347
+ if (raw.sections !== void 0) {
348
+ if (!Array.isArray(raw.sections) || raw.sections.length === 0) {
349
+ throw new RuleValidationError(
350
+ `'sections' must be a non-empty array if present in ${filePath}`
351
+ );
149
352
  }
353
+ sections = raw.sections.map(
354
+ (s, i) => validateSectionRule(s, `sections[${i}]`, filePath)
355
+ );
150
356
  }
151
- // @awa-impl: GEN-9_AC-1, GEN-9_AC-2, GEN-9_AC-3, GEN-9_AC-4, GEN-9_AC-5, GEN-9_AC-6
152
- summary(result) {
153
- console.log("");
154
- console.log(chalk.bold("Summary:"));
155
- if (result.created === 0 && result.overwritten === 0 && result.deleted === 0) {
156
- console.log(chalk.yellow(" \u26A0 No files were created, overwritten, or deleted"));
357
+ let sectionsProhibited;
358
+ if (raw["sections-prohibited"] !== void 0) {
359
+ if (!Array.isArray(raw["sections-prohibited"]) || !raw["sections-prohibited"].every((v) => typeof v === "string")) {
360
+ throw new RuleValidationError(`'sections-prohibited' must be a string array in ${filePath}`);
157
361
  }
158
- if (result.created > 0) {
159
- console.log(chalk.green(` Created: ${result.created}`));
362
+ sectionsProhibited = raw["sections-prohibited"];
363
+ }
364
+ return {
365
+ "target-files": raw["target-files"],
366
+ ...typeof raw.description === "string" ? { description: raw.description } : {},
367
+ ...typeof raw["line-limit"] === "number" && raw["line-limit"] > 0 ? { "line-limit": raw["line-limit"] } : {},
368
+ sections: sections ?? [],
369
+ ...sectionsProhibited ? { "sections-prohibited": sectionsProhibited } : {},
370
+ ...typeof raw.example === "string" ? { example: raw.example } : {}
371
+ };
372
+ }
373
+ function validateSectionRule(raw, path, filePath) {
374
+ if (!raw || typeof raw !== "object") {
375
+ throw new RuleValidationError(`${path} must be an object in ${filePath}`);
376
+ }
377
+ const section = raw;
378
+ if (typeof section.heading !== "string" || section.heading.length === 0) {
379
+ throw new RuleValidationError(`${path}.heading must be a non-empty string in ${filePath}`);
380
+ }
381
+ if (typeof section.level !== "number" || section.level < 1 || section.level > 6) {
382
+ throw new RuleValidationError(`${path}.level must be 1-6 in ${filePath}`);
383
+ }
384
+ validatePattern(section.heading, `${path}.heading`, filePath);
385
+ let contains;
386
+ if (section.contains !== void 0) {
387
+ if (!Array.isArray(section.contains)) {
388
+ throw new RuleValidationError(`${path}.contains must be an array in ${filePath}`);
160
389
  }
161
- if (result.overwritten > 0) {
162
- console.log(chalk.yellow(` Overwritten: ${result.overwritten}`));
390
+ contains = section.contains.map(
391
+ (c, i) => validateContainsRule(c, `${path}.contains[${i}]`, filePath)
392
+ );
393
+ }
394
+ let children;
395
+ if (section.children !== void 0) {
396
+ if (!Array.isArray(section.children)) {
397
+ throw new RuleValidationError(`${path}.children must be an array in ${filePath}`);
163
398
  }
164
- if (result.deleted > 0) {
165
- console.log(chalk.red(` Deleted: ${result.deleted}`));
399
+ children = section.children.map(
400
+ (c, i) => validateSectionRule(c, `${path}.children[${i}]`, filePath)
401
+ );
402
+ }
403
+ return {
404
+ heading: section.heading,
405
+ level: section.level,
406
+ ...typeof section.required === "boolean" ? { required: section.required } : {},
407
+ ...typeof section.repeatable === "boolean" ? { repeatable: section.repeatable } : {},
408
+ ...typeof section.description === "string" ? { description: section.description } : {},
409
+ ...contains ? { contains } : {},
410
+ ...children ? { children } : {}
411
+ };
412
+ }
413
+ function validateContainsRule(raw, path, filePath) {
414
+ if (!raw || typeof raw !== "object") {
415
+ throw new RuleValidationError(`${path} must be an object in ${filePath}`);
416
+ }
417
+ const rule = raw;
418
+ const when = rule.when !== void 0 ? validateWhenCondition(rule.when, `${path}.when`, filePath) : void 0;
419
+ if (typeof rule.pattern === "string") {
420
+ validatePattern(rule.pattern, `${path}.pattern`, filePath);
421
+ return {
422
+ pattern: rule.pattern,
423
+ ...typeof rule.label === "string" ? { label: rule.label } : {},
424
+ ...typeof rule.description === "string" ? { description: rule.description } : {},
425
+ ...typeof rule.required === "boolean" ? { required: rule.required } : {},
426
+ ...typeof rule.prohibited === "boolean" ? { prohibited: rule.prohibited } : {},
427
+ ...when ? { when } : {}
428
+ };
429
+ }
430
+ if (rule.list && typeof rule.list === "object") {
431
+ const list = rule.list;
432
+ if (typeof list.pattern !== "string") {
433
+ throw new RuleValidationError(`${path}.list.pattern must be a string in ${filePath}`);
166
434
  }
167
- if (result.skippedEqual > 0) {
168
- console.log(chalk.dim(` Skipped (equal): ${result.skippedEqual}`));
435
+ validatePattern(list.pattern, `${path}.list.pattern`, filePath);
436
+ return {
437
+ list: {
438
+ pattern: list.pattern,
439
+ ...typeof list.min === "number" ? { min: list.min } : {},
440
+ ...typeof list.label === "string" ? { label: list.label } : {}
441
+ },
442
+ ...typeof rule.description === "string" ? { description: rule.description } : {},
443
+ ...when ? { when } : {}
444
+ };
445
+ }
446
+ if (rule.table && typeof rule.table === "object") {
447
+ const table = rule.table;
448
+ if (!Array.isArray(table.columns) || !table.columns.every((c) => typeof c === "string")) {
449
+ throw new RuleValidationError(`${path}.table.columns must be a string array in ${filePath}`);
169
450
  }
170
- if (result.skippedUser > 0) {
171
- console.log(chalk.blue(` Skipped (user): ${result.skippedUser}`));
451
+ return {
452
+ table: {
453
+ ...typeof table.heading === "string" ? { heading: table.heading } : {},
454
+ columns: table.columns,
455
+ ...typeof table["min-rows"] === "number" ? { "min-rows": table["min-rows"] } : {}
456
+ },
457
+ ...typeof rule.description === "string" ? { description: rule.description } : {},
458
+ ...when ? { when } : {}
459
+ };
460
+ }
461
+ if (rule["code-block"] === true) {
462
+ return {
463
+ "code-block": true,
464
+ ...typeof rule.label === "string" ? { label: rule.label } : {},
465
+ ...typeof rule.description === "string" ? { description: rule.description } : {},
466
+ ...when ? { when } : {}
467
+ };
468
+ }
469
+ if (typeof rule["heading-or-text"] === "string") {
470
+ return {
471
+ "heading-or-text": rule["heading-or-text"],
472
+ ...typeof rule.required === "boolean" ? { required: rule.required } : {},
473
+ ...typeof rule.description === "string" ? { description: rule.description } : {},
474
+ ...when ? { when } : {}
475
+ };
476
+ }
477
+ throw new RuleValidationError(`${path} has no recognized rule type in ${filePath}`);
478
+ }
479
+ function validateWhenCondition(raw, path, filePath) {
480
+ if (!raw || typeof raw !== "object") {
481
+ throw new RuleValidationError(`${path} must be an object in ${filePath}`);
482
+ }
483
+ const condition = raw;
484
+ const result = {};
485
+ if (typeof condition["heading-matches"] === "string") {
486
+ validatePattern(condition["heading-matches"], `${path}.heading-matches`, filePath);
487
+ result["heading-matches"] = condition["heading-matches"];
488
+ }
489
+ if (typeof condition["heading-not-matches"] === "string") {
490
+ validatePattern(condition["heading-not-matches"], `${path}.heading-not-matches`, filePath);
491
+ result["heading-not-matches"] = condition["heading-not-matches"];
492
+ }
493
+ if (!result["heading-matches"] && !result["heading-not-matches"]) {
494
+ throw new RuleValidationError(
495
+ `${path} must have 'heading-matches' or 'heading-not-matches' in ${filePath}`
496
+ );
497
+ }
498
+ return result;
499
+ }
500
+ function validatePattern(pattern, path, filePath) {
501
+ if (/[.+*?^${}()|[\]\\]/.test(pattern)) {
502
+ try {
503
+ new RegExp(pattern);
504
+ } catch (e) {
505
+ const msg = e instanceof Error ? e.message : String(e);
506
+ throw new RuleValidationError(`Invalid regex in ${path}: ${msg} (${filePath})`);
172
507
  }
173
- if (result.skippedEmpty > 0) {
174
- console.log(chalk.dim(` Skipped (empty): ${result.skippedEmpty}`));
508
+ }
509
+ }
510
+ var RuleValidationError = class extends Error {
511
+ constructor(message) {
512
+ super(message);
513
+ this.name = "RuleValidationError";
514
+ }
515
+ };
516
+
517
+ // src/core/check/schema-checker.ts
518
+ import { readFile as readFile3 } from "fs/promises";
519
+ import remarkGfm from "remark-gfm";
520
+ import remarkParse from "remark-parse";
521
+ import { unified } from "unified";
522
+ function formatSectionRule(rule) {
523
+ const parts = [`section: heading="${rule.heading}" level=${rule.level}`];
524
+ if (rule.required) parts.push("required");
525
+ if (rule.repeatable) parts.push("repeatable");
526
+ return parts.join(" ");
527
+ }
528
+ function formatContainsRule(rule) {
529
+ if ("pattern" in rule && typeof rule.pattern === "string") {
530
+ const r = rule;
531
+ if (r.prohibited) return `contains: prohibited pattern="${r.pattern}"`;
532
+ return `contains: pattern="${r.pattern}"${r.required === false ? "" : " required"}`;
533
+ }
534
+ if ("list" in rule) {
535
+ const r = rule;
536
+ return `contains: list pattern="${r.list.pattern}"${r.list.min !== void 0 ? ` min=${r.list.min}` : ""}`;
537
+ }
538
+ if ("table" in rule) {
539
+ const r = rule;
540
+ return `contains: table columns=[${r.table.columns.join(", ")}]${r.table["min-rows"] !== void 0 ? ` min-rows=${r.table["min-rows"]}` : ""}`;
541
+ }
542
+ if ("code-block" in rule) {
543
+ return "contains: code-block";
544
+ }
545
+ if ("heading-or-text" in rule) {
546
+ const r = rule;
547
+ return `contains: heading-or-text="${r["heading-or-text"]}"`;
548
+ }
549
+ return "contains: (unknown rule)";
550
+ }
551
+ function formatLineLimitRule(limit) {
552
+ return `line-limit: ${limit}`;
553
+ }
554
+ function formatProhibitedRule(pattern) {
555
+ return `sections-prohibited: "${pattern}"`;
556
+ }
557
+ async function checkSchemasAsync(specFiles, ruleSets) {
558
+ const findings = [];
559
+ const parser = unified().use(remarkParse).use(remarkGfm);
560
+ for (const spec of specFiles) {
561
+ const matchingRules = ruleSets.filter((rs) => matchesTargetGlob(spec.filePath, rs.targetGlob));
562
+ if (matchingRules.length === 0) continue;
563
+ let content;
564
+ try {
565
+ content = await readFile3(spec.filePath, "utf-8");
566
+ } catch {
567
+ continue;
568
+ }
569
+ const tree = parser.parse(content);
570
+ const sectionTree = buildSectionTree(tree);
571
+ const allSections = flattenSections(sectionTree);
572
+ for (const ruleSet of matchingRules) {
573
+ const ruleSource = ruleSet.sourcePath;
574
+ if (ruleSet.ruleFile["line-limit"] !== void 0) {
575
+ const lineCount = content.split("\n").length;
576
+ if (lineCount > ruleSet.ruleFile["line-limit"]) {
577
+ findings.push({
578
+ severity: "warning",
579
+ code: "schema-line-limit",
580
+ message: `File has ${lineCount} lines, exceeds limit of ${ruleSet.ruleFile["line-limit"]}`,
581
+ filePath: spec.filePath,
582
+ ruleSource,
583
+ rule: formatLineLimitRule(ruleSet.ruleFile["line-limit"])
584
+ });
585
+ }
586
+ }
587
+ findings.push(
588
+ ...checkRulesAgainstSections(
589
+ allSections,
590
+ ruleSet.ruleFile.sections,
591
+ spec.filePath,
592
+ ruleSource
593
+ )
594
+ );
595
+ if (ruleSet.ruleFile["sections-prohibited"]) {
596
+ findings.push(
597
+ ...checkProhibited(
598
+ content,
599
+ ruleSet.ruleFile["sections-prohibited"],
600
+ spec.filePath,
601
+ ruleSource
602
+ )
603
+ );
604
+ }
175
605
  }
176
- console.log("");
177
606
  }
178
- // @awa-impl: DIFF-4_AC-3
179
- diffLine(line, type) {
180
- switch (type) {
181
- case "add":
182
- console.log(chalk.green(line));
183
- break;
184
- case "remove":
185
- console.log(chalk.red(line));
186
- break;
187
- case "context":
188
- console.log(chalk.dim(line));
189
- break;
607
+ return { findings };
608
+ }
609
+ function buildSectionTree(tree) {
610
+ return buildSectionsFromNodes(tree.children, 0, 0).sections;
611
+ }
612
+ function buildSectionsFromNodes(nodes, start, parentLevel) {
613
+ const sections = [];
614
+ let i = start;
615
+ while (i < nodes.length) {
616
+ const node = nodes[i];
617
+ if (!node) break;
618
+ if (node.type === "heading") {
619
+ const h = node;
620
+ if (parentLevel > 0 && h.depth <= parentLevel) break;
621
+ const headingText = extractText(h.children);
622
+ const contentNodes = [];
623
+ i++;
624
+ while (i < nodes.length) {
625
+ const next = nodes[i];
626
+ if (!next || next.type === "heading") break;
627
+ contentNodes.push(next);
628
+ i++;
629
+ }
630
+ const childResult = buildSectionsFromNodes(nodes, i, h.depth);
631
+ i = childResult.nextIndex;
632
+ sections.push({
633
+ heading: h,
634
+ headingText,
635
+ level: h.depth,
636
+ children: childResult.sections,
637
+ contentNodes
638
+ });
639
+ } else {
640
+ i++;
190
641
  }
191
642
  }
192
- // @awa-impl: DIFF-4_AC-4, DIFF-4_AC-5
193
- diffSummary(result) {
194
- console.log("");
195
- const filesCompared = result.identical + result.modified + result.newFiles + result.extraFiles + result.binaryDiffers + result.deleteListed;
196
- const differences = result.modified + result.newFiles + result.extraFiles + result.binaryDiffers + result.deleteListed;
197
- console.log(chalk.bold(`${filesCompared} files compared, ${differences} differences`));
198
- if (!result.hasDifferences) {
199
- console.log(chalk.green("\u2714 No differences found"));
643
+ return { sections, nextIndex: i };
644
+ }
645
+ function flattenSections(sections) {
646
+ const result = [];
647
+ for (const s of sections) {
648
+ result.push(s);
649
+ result.push(...flattenSections(s.children));
650
+ }
651
+ return result;
652
+ }
653
+ function extractText(children) {
654
+ return children.map((c) => {
655
+ if ("value" in c) return c.value;
656
+ if ("children" in c) return extractText(c.children);
657
+ return "";
658
+ }).join("");
659
+ }
660
+ function checkRulesAgainstSections(allSections, rules, filePath, ruleSource) {
661
+ const findings = [];
662
+ for (const rule of rules) {
663
+ const matches = findMatchingSections(allSections, rule);
664
+ if (matches.length === 0 && rule.required) {
665
+ findings.push({
666
+ severity: "error",
667
+ code: "schema-missing-section",
668
+ message: `Missing required section: '${rule.heading}' (level ${rule.level})`,
669
+ filePath,
670
+ ruleSource,
671
+ rule: formatSectionRule(rule)
672
+ });
673
+ continue;
200
674
  }
201
- console.log(chalk.bold("Summary:"));
202
- console.log(chalk.dim(` Identical: ${result.identical}`));
203
- if (result.modified > 0) {
204
- console.log(chalk.yellow(` Modified: ${result.modified}`));
675
+ for (const match of matches) {
676
+ if (match.level !== rule.level) {
677
+ findings.push({
678
+ severity: "warning",
679
+ code: "schema-wrong-level",
680
+ message: `Section '${match.headingText}' is level ${match.level}, expected ${rule.level}`,
681
+ filePath,
682
+ line: match.heading.position?.start.line,
683
+ ruleSource,
684
+ rule: formatSectionRule(rule)
685
+ });
686
+ }
687
+ if (rule.contains) {
688
+ for (const cr of rule.contains) {
689
+ findings.push(...checkContainsRule(match, cr, filePath, ruleSource));
690
+ }
691
+ }
692
+ if (rule.children) {
693
+ const childFlat = flattenSections(match.children);
694
+ findings.push(...checkRulesAgainstSections(childFlat, rule.children, filePath, ruleSource));
695
+ }
205
696
  }
206
- if (result.newFiles > 0) {
207
- console.log(chalk.green(` New: ${result.newFiles}`));
697
+ }
698
+ return findings;
699
+ }
700
+ function findMatchingSections(allSections, rule) {
701
+ const regex = createHeadingRegex(rule.heading);
702
+ const matches = allSections.filter((s) => s.level === rule.level && regex.test(s.headingText));
703
+ if (!rule.repeatable && matches.length > 1) {
704
+ return [matches[0]];
705
+ }
706
+ return matches;
707
+ }
708
+ function createHeadingRegex(pattern) {
709
+ if (/[.+*?^${}()|[\]\\]/.test(pattern)) {
710
+ try {
711
+ return new RegExp(`^${pattern}$`);
712
+ } catch {
713
+ return new RegExp(`^${escapeRegex(pattern)}$`);
208
714
  }
209
- if (result.extraFiles > 0) {
210
- console.log(chalk.red(` Extra: ${result.extraFiles}`));
715
+ }
716
+ return new RegExp(`^${escapeRegex(pattern)}$`, "i");
717
+ }
718
+ function escapeRegex(str) {
719
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
720
+ }
721
+ function checkContainsRule(section, rule, filePath, ruleSource) {
722
+ const when = "when" in rule ? rule.when : void 0;
723
+ if (when && !evaluateWhenCondition(when, section.headingText)) {
724
+ return [];
725
+ }
726
+ const formattedRule = formatContainsRule(rule);
727
+ if ("pattern" in rule && typeof rule.pattern === "string") {
728
+ return checkPatternContains(
729
+ section,
730
+ rule,
731
+ filePath,
732
+ ruleSource,
733
+ formattedRule
734
+ );
735
+ }
736
+ if ("list" in rule) {
737
+ return checkListContains(
738
+ section,
739
+ rule,
740
+ filePath,
741
+ ruleSource,
742
+ formattedRule
743
+ );
744
+ }
745
+ if ("table" in rule) {
746
+ return checkTableContains(
747
+ section,
748
+ rule,
749
+ filePath,
750
+ ruleSource,
751
+ formattedRule
752
+ );
753
+ }
754
+ if ("code-block" in rule) {
755
+ return checkCodeBlockContains(
756
+ section,
757
+ rule,
758
+ filePath,
759
+ ruleSource,
760
+ formattedRule
761
+ );
762
+ }
763
+ if ("heading-or-text" in rule) {
764
+ return checkHeadingOrText(
765
+ section,
766
+ rule,
767
+ filePath,
768
+ ruleSource,
769
+ formattedRule
770
+ );
771
+ }
772
+ return [];
773
+ }
774
+ function checkPatternContains(section, rule, filePath, ruleSource, formattedRule) {
775
+ const text = getFullSectionText(section);
776
+ const found = new RegExp(rule.pattern, "m").test(text);
777
+ if (rule.prohibited) {
778
+ if (found) {
779
+ return [
780
+ {
781
+ severity: "warning",
782
+ code: "schema-prohibited",
783
+ message: `Section '${section.headingText}' contains prohibited content: ${rule.label ?? rule.pattern}`,
784
+ filePath,
785
+ line: section.heading.position?.start.line,
786
+ ruleSource,
787
+ rule: formattedRule
788
+ }
789
+ ];
211
790
  }
212
- if (result.binaryDiffers > 0) {
213
- console.log(chalk.red(` Binary differs: ${result.binaryDiffers}`));
791
+ return [];
792
+ }
793
+ if (found) return [];
794
+ if (rule.required !== false) {
795
+ return [
796
+ {
797
+ severity: "error",
798
+ code: "schema-missing-content",
799
+ message: `Section '${section.headingText}' missing required content: ${rule.label ?? rule.pattern}`,
800
+ filePath,
801
+ line: section.heading.position?.start.line,
802
+ ruleSource,
803
+ rule: formattedRule
804
+ }
805
+ ];
806
+ }
807
+ return [];
808
+ }
809
+ function checkListContains(section, rule, filePath, ruleSource, formattedRule) {
810
+ const items = collectAllListItems(section);
811
+ const regex = new RegExp(rule.list.pattern);
812
+ const count = items.filter((item) => regex.test(item)).length;
813
+ if (rule.list.min !== void 0 && count < rule.list.min) {
814
+ return [
815
+ {
816
+ severity: "error",
817
+ code: "schema-missing-content",
818
+ message: `Section '${section.headingText}' has ${count} matching ${rule.list.label ?? "list items"}, expected at least ${rule.list.min}`,
819
+ filePath,
820
+ line: section.heading.position?.start.line,
821
+ ruleSource,
822
+ rule: formattedRule
823
+ }
824
+ ];
825
+ }
826
+ return [];
827
+ }
828
+ function checkTableContains(section, rule, filePath, ruleSource, formattedRule) {
829
+ const tables = collectAllTables(section);
830
+ if (tables.length === 0) {
831
+ return [
832
+ {
833
+ severity: "error",
834
+ code: "schema-missing-content",
835
+ message: `Section '${section.headingText}' missing required table${rule.table.heading ? ` (${rule.table.heading})` : ""}`,
836
+ filePath,
837
+ line: section.heading.position?.start.line,
838
+ ruleSource,
839
+ rule: formattedRule
840
+ }
841
+ ];
842
+ }
843
+ let matched;
844
+ let firstMismatch;
845
+ for (const table of tables) {
846
+ const headerRow = table.children[0];
847
+ if (!headerRow) continue;
848
+ const headers = headerRow.children.map(
849
+ (cell) => extractText(cell.children).trim()
850
+ );
851
+ if (rule.table.columns.every((col) => headers.includes(col))) {
852
+ matched = table;
853
+ break;
214
854
  }
215
- if (result.deleteListed > 0) {
216
- console.log(chalk.red(` Delete listed: ${result.deleteListed}`));
855
+ if (!firstMismatch) {
856
+ firstMismatch = { table, headers };
217
857
  }
218
- console.log("");
219
858
  }
220
- };
221
- var logger = new Logger();
222
-
223
- // src/core/config.ts
224
- var DEFAULT_CONFIG_PATH = ".awa.toml";
225
- var ConfigLoader = class {
226
- // @awa-impl: CFG-1_AC-1, CFG-1_AC-2, CFG-1_AC-3, CFG-1_AC-4
227
- async load(configPath) {
228
- const pathToLoad = configPath ?? DEFAULT_CONFIG_PATH;
229
- const exists = await pathExists(pathToLoad);
230
- if (configPath && !exists) {
231
- throw new ConfigError(
232
- `Configuration file not found: ${configPath}`,
233
- "FILE_NOT_FOUND",
234
- configPath
235
- );
236
- }
237
- if (!configPath && !exists) {
238
- return null;
859
+ if (!matched) {
860
+ const mm = firstMismatch;
861
+ return [
862
+ {
863
+ severity: "error",
864
+ code: "schema-table-columns",
865
+ message: `No table in '${section.headingText}' has columns [${rule.table.columns.join(", ")}]${mm ? `, found [${mm.headers.join(", ")}]` : ""}`,
866
+ filePath,
867
+ line: mm?.table.position?.start.line ?? section.heading.position?.start.line,
868
+ ruleSource,
869
+ rule: formattedRule
870
+ }
871
+ ];
872
+ }
873
+ const findings = [];
874
+ const dataRows = matched.children.length - 1;
875
+ if (rule.table["min-rows"] !== void 0 && dataRows < rule.table["min-rows"]) {
876
+ findings.push({
877
+ severity: "error",
878
+ code: "schema-missing-content",
879
+ message: `Table in '${section.headingText}' has ${dataRows} data rows, expected at least ${rule.table["min-rows"]}`,
880
+ filePath,
881
+ line: matched.position?.start.line,
882
+ ruleSource,
883
+ rule: formattedRule
884
+ });
885
+ }
886
+ return findings;
887
+ }
888
+ function checkCodeBlockContains(section, rule, filePath, ruleSource, formattedRule) {
889
+ if (collectAllCodeBlocks(section).length > 0) return [];
890
+ return [
891
+ {
892
+ severity: "error",
893
+ code: "schema-missing-content",
894
+ message: `Section '${section.headingText}' missing required ${rule.label ?? "code block"}`,
895
+ filePath,
896
+ line: section.heading.position?.start.line,
897
+ ruleSource,
898
+ rule: formattedRule
239
899
  }
240
- try {
241
- const content = await readTextFile(pathToLoad);
242
- const parsed = parse(content);
243
- const config = {};
244
- if (parsed.output !== void 0) {
245
- if (typeof parsed.output !== "string") {
246
- throw new ConfigError(
247
- `Invalid type for 'output': expected string, got ${typeof parsed.output}`,
248
- "INVALID_TYPE",
249
- pathToLoad
250
- );
251
- }
252
- config.output = parsed.output;
900
+ ];
901
+ }
902
+ function checkHeadingOrText(section, rule, filePath, ruleSource, formattedRule) {
903
+ const needle = rule["heading-or-text"].toUpperCase();
904
+ if (section.children.some((c) => c.headingText.toUpperCase().includes(needle))) return [];
905
+ if (getFullSectionText(section).toUpperCase().includes(needle)) return [];
906
+ if (rule.required !== false) {
907
+ return [
908
+ {
909
+ severity: "error",
910
+ code: "schema-missing-content",
911
+ message: `Section '${section.headingText}' missing required heading or text: '${rule["heading-or-text"]}'`,
912
+ filePath,
913
+ line: section.heading.position?.start.line,
914
+ ruleSource,
915
+ rule: formattedRule
253
916
  }
254
- if (parsed.template !== void 0) {
255
- if (typeof parsed.template !== "string") {
256
- throw new ConfigError(
257
- `Invalid type for 'template': expected string, got ${typeof parsed.template}`,
258
- "INVALID_TYPE",
259
- pathToLoad
260
- );
261
- }
262
- config.template = parsed.template;
917
+ ];
918
+ }
919
+ return [];
920
+ }
921
+ function evaluateWhenCondition(when, headingText) {
922
+ if (when["heading-matches"]) {
923
+ if (!new RegExp(when["heading-matches"]).test(headingText)) return false;
924
+ }
925
+ if (when["heading-not-matches"]) {
926
+ if (new RegExp(when["heading-not-matches"]).test(headingText)) return false;
927
+ }
928
+ return true;
929
+ }
930
+ function checkProhibited(content, prohibited, filePath, ruleSource) {
931
+ const findings = [];
932
+ const lines = content.split("\n");
933
+ for (const pattern of prohibited) {
934
+ const regex = new RegExp(escapeRegex(pattern));
935
+ let inCodeBlock = false;
936
+ for (const [i, line] of lines.entries()) {
937
+ if (line.startsWith("```")) {
938
+ inCodeBlock = !inCodeBlock;
939
+ continue;
263
940
  }
264
- if (parsed.features !== void 0) {
265
- if (!Array.isArray(parsed.features) || !parsed.features.every((f) => typeof f === "string")) {
266
- throw new ConfigError(
267
- `Invalid type for 'features': expected array of strings`,
268
- "INVALID_TYPE",
269
- pathToLoad
270
- );
271
- }
272
- config.features = parsed.features;
941
+ if (inCodeBlock) continue;
942
+ if (regex.test(line)) {
943
+ findings.push({
944
+ severity: "warning",
945
+ code: "schema-prohibited",
946
+ message: `Prohibited formatting '${pattern}' found`,
947
+ filePath,
948
+ line: i + 1,
949
+ ruleSource,
950
+ rule: formatProhibitedRule(pattern)
951
+ });
952
+ break;
273
953
  }
274
- if (parsed.preset !== void 0) {
275
- if (!Array.isArray(parsed.preset) || !parsed.preset.every((p) => typeof p === "string")) {
276
- throw new ConfigError(
277
- `Invalid type for 'preset': expected array of strings`,
278
- "INVALID_TYPE",
279
- pathToLoad
280
- );
954
+ }
955
+ }
956
+ return findings;
957
+ }
958
+ function getFullSectionText(section) {
959
+ let text = section.contentNodes.map(nodeToText).join("\n");
960
+ for (const child of section.children) {
961
+ text += `
962
+ ${child.headingText}
963
+ ${getFullSectionText(child)}`;
964
+ }
965
+ return text;
966
+ }
967
+ function nodeToText(node) {
968
+ if ("value" in node) return node.value;
969
+ if ("children" in node) {
970
+ return node.children.map((c) => nodeToText(c)).join("");
971
+ }
972
+ return "";
973
+ }
974
+ function extractListItems(nodes) {
975
+ const items = [];
976
+ for (const node of nodes) {
977
+ if (node.type === "list") {
978
+ for (const item of node.children) {
979
+ const raw = nodeToText(item);
980
+ const li = item;
981
+ if (li.checked === true) {
982
+ items.push(`[x] ${raw}`);
983
+ } else if (li.checked === false) {
984
+ items.push(`[ ] ${raw}`);
985
+ } else {
986
+ items.push(raw);
281
987
  }
282
- config.preset = parsed.preset;
283
988
  }
284
- if (parsed["remove-features"] !== void 0) {
285
- if (!Array.isArray(parsed["remove-features"]) || !parsed["remove-features"].every((f) => typeof f === "string")) {
286
- throw new ConfigError(
287
- `Invalid type for 'remove-features': expected array of strings`,
288
- "INVALID_TYPE",
289
- pathToLoad
290
- );
291
- }
292
- config["remove-features"] = parsed["remove-features"];
989
+ }
990
+ }
991
+ return items;
992
+ }
993
+ function collectAllListItems(section) {
994
+ const items = extractListItems(section.contentNodes);
995
+ for (const child of section.children) items.push(...collectAllListItems(child));
996
+ return items;
997
+ }
998
+ function collectAllTables(section) {
999
+ const tables = section.contentNodes.filter((n) => n.type === "table");
1000
+ for (const child of section.children) tables.push(...collectAllTables(child));
1001
+ return tables;
1002
+ }
1003
+ function collectAllCodeBlocks(section) {
1004
+ const blocks = section.contentNodes.filter((n) => n.type === "code");
1005
+ for (const child of section.children) blocks.push(...collectAllCodeBlocks(child));
1006
+ return blocks;
1007
+ }
1008
+
1009
+ // src/core/check/spec-parser.ts
1010
+ import { readFile as readFile4 } from "fs/promises";
1011
+ import { basename } from "path";
1012
+ async function parseSpecs(config) {
1013
+ const files = await collectSpecFiles(config.specGlobs, config.specIgnore);
1014
+ const specFiles = [];
1015
+ const requirementIds = /* @__PURE__ */ new Set();
1016
+ const acIds = /* @__PURE__ */ new Set();
1017
+ const propertyIds = /* @__PURE__ */ new Set();
1018
+ const componentNames = /* @__PURE__ */ new Set();
1019
+ const idLocations = /* @__PURE__ */ new Map();
1020
+ for (const filePath of files) {
1021
+ const specFile = await parseSpecFile(filePath, config.crossRefPatterns);
1022
+ if (specFile) {
1023
+ specFiles.push(specFile);
1024
+ for (const id of specFile.requirementIds) requirementIds.add(id);
1025
+ for (const id of specFile.acIds) acIds.add(id);
1026
+ for (const id of specFile.propertyIds) propertyIds.add(id);
1027
+ for (const name of specFile.componentNames) componentNames.add(name);
1028
+ for (const [id, loc] of specFile.idLocations ?? []) {
1029
+ idLocations.set(id, loc);
293
1030
  }
294
- if (parsed.force !== void 0) {
295
- if (typeof parsed.force !== "boolean") {
296
- throw new ConfigError(
297
- `Invalid type for 'force': expected boolean, got ${typeof parsed.force}`,
298
- "INVALID_TYPE",
299
- pathToLoad
300
- );
301
- }
302
- config.force = parsed.force;
1031
+ }
1032
+ }
1033
+ const allIds = /* @__PURE__ */ new Set([...requirementIds, ...acIds, ...propertyIds, ...componentNames]);
1034
+ return { requirementIds, acIds, propertyIds, componentNames, allIds, specFiles, idLocations };
1035
+ }
1036
+ async function parseSpecFile(filePath, crossRefPatterns) {
1037
+ let content;
1038
+ try {
1039
+ content = await readFile4(filePath, "utf-8");
1040
+ } catch {
1041
+ return null;
1042
+ }
1043
+ const code = extractCodePrefix(filePath);
1044
+ const lines = content.split("\n");
1045
+ const requirementIds = [];
1046
+ const acIds = [];
1047
+ const propertyIds = [];
1048
+ const componentNames = [];
1049
+ const crossRefs = [];
1050
+ const idLocations = /* @__PURE__ */ new Map();
1051
+ const reqIdRegex = /^###\s+([A-Z][A-Z0-9]*-\d+(?:\.\d+)?)\s*:/;
1052
+ const acIdRegex = /^-\s+\[[ x]\]\s+([A-Z][A-Z0-9]*-\d+(?:\.\d+)?_AC-\d+)\s/;
1053
+ const propIdRegex = /^-\s+([A-Z][A-Z0-9]*_P-\d+)\s/;
1054
+ const componentRegex = /^###\s+([A-Z][A-Z0-9]*-[A-Za-z][A-Za-z0-9]*(?:[A-Z][a-z0-9]*)*)\s*$/;
1055
+ for (const [i, line] of lines.entries()) {
1056
+ const lineNum = i + 1;
1057
+ const reqMatch = reqIdRegex.exec(line);
1058
+ if (reqMatch?.[1]) {
1059
+ requirementIds.push(reqMatch[1]);
1060
+ idLocations.set(reqMatch[1], { filePath, line: lineNum });
1061
+ }
1062
+ const acMatch = acIdRegex.exec(line);
1063
+ if (acMatch?.[1]) {
1064
+ acIds.push(acMatch[1]);
1065
+ idLocations.set(acMatch[1], { filePath, line: lineNum });
1066
+ }
1067
+ const propMatch = propIdRegex.exec(line);
1068
+ if (propMatch?.[1]) {
1069
+ propertyIds.push(propMatch[1]);
1070
+ idLocations.set(propMatch[1], { filePath, line: lineNum });
1071
+ }
1072
+ const compMatch = componentRegex.exec(line);
1073
+ if (compMatch?.[1]) {
1074
+ if (!reqIdRegex.test(line)) {
1075
+ componentNames.push(compMatch[1]);
1076
+ idLocations.set(compMatch[1], { filePath, line: lineNum });
303
1077
  }
304
- if (parsed["dry-run"] !== void 0) {
305
- if (typeof parsed["dry-run"] !== "boolean") {
306
- throw new ConfigError(
307
- `Invalid type for 'dry-run': expected boolean, got ${typeof parsed["dry-run"]}`,
308
- "INVALID_TYPE",
309
- pathToLoad
310
- );
1078
+ }
1079
+ for (const pattern of crossRefPatterns) {
1080
+ const patIdx = line.indexOf(pattern);
1081
+ if (patIdx !== -1) {
1082
+ const afterPattern = line.slice(patIdx + pattern.length);
1083
+ const ids = extractIdsFromText(afterPattern);
1084
+ if (ids.length > 0) {
1085
+ const type = pattern.toLowerCase().includes("implements") ? "implements" : "validates";
1086
+ crossRefs.push({ type, ids, filePath, line: i + 1 });
311
1087
  }
312
- config["dry-run"] = parsed["dry-run"];
313
1088
  }
314
- if (parsed.delete !== void 0) {
315
- if (typeof parsed.delete !== "boolean") {
316
- throw new ConfigError(
317
- `Invalid type for 'delete': expected boolean, got ${typeof parsed.delete}`,
318
- "INVALID_TYPE",
319
- pathToLoad
320
- );
1089
+ }
1090
+ }
1091
+ return {
1092
+ filePath,
1093
+ code,
1094
+ requirementIds,
1095
+ acIds,
1096
+ propertyIds,
1097
+ componentNames,
1098
+ crossRefs,
1099
+ idLocations
1100
+ };
1101
+ }
1102
+ function extractCodePrefix(filePath) {
1103
+ const name = basename(filePath, ".md");
1104
+ const match = /^(?:REQ|DESIGN|FEAT|EXAMPLES|API)-([A-Z][A-Z0-9]*)-/.exec(name);
1105
+ if (match?.[1]) return match[1];
1106
+ return "";
1107
+ }
1108
+ function extractIdsFromText(text) {
1109
+ const idRegex = /[A-Z][A-Z0-9]*-\d+(?:\.\d+)?(?:_AC-\d+)?|[A-Z][A-Z0-9]*_P-\d+/g;
1110
+ const ids = [];
1111
+ let match = idRegex.exec(text);
1112
+ while (match !== null) {
1113
+ ids.push(match[0]);
1114
+ match = idRegex.exec(text);
1115
+ }
1116
+ return ids;
1117
+ }
1118
+ async function collectSpecFiles(specGlobs, ignore) {
1119
+ return collectFiles(specGlobs, ignore);
1120
+ }
1121
+
1122
+ // src/core/check/spec-spec-checker.ts
1123
+ function checkSpecAgainstSpec(specs, markers, config) {
1124
+ const findings = [];
1125
+ for (const specFile of specs.specFiles) {
1126
+ for (const crossRef of specFile.crossRefs) {
1127
+ for (const refId of crossRef.ids) {
1128
+ if (!specs.allIds.has(refId)) {
1129
+ findings.push({
1130
+ severity: "error",
1131
+ code: "broken-cross-ref",
1132
+ message: `Cross-reference '${refId}' (${crossRef.type}) not found in any spec file`,
1133
+ filePath: crossRef.filePath,
1134
+ line: crossRef.line,
1135
+ id: refId
1136
+ });
321
1137
  }
322
- config.delete = parsed.delete;
323
1138
  }
324
- if (parsed.refresh !== void 0) {
325
- if (typeof parsed.refresh !== "boolean") {
326
- throw new ConfigError(
327
- `Invalid type for 'refresh': expected boolean, got ${typeof parsed.refresh}`,
328
- "INVALID_TYPE",
329
- pathToLoad
330
- );
331
- }
332
- config.refresh = parsed.refresh;
1139
+ }
1140
+ }
1141
+ if (!config.specOnly) {
1142
+ const referencedCodes = /* @__PURE__ */ new Set();
1143
+ for (const marker of markers.markers) {
1144
+ const codeMatch = /^([A-Z][A-Z0-9]*)[-_]/.exec(marker.id);
1145
+ if (codeMatch?.[1]) {
1146
+ referencedCodes.add(codeMatch[1]);
333
1147
  }
334
- if (parsed.presets !== void 0) {
335
- if (parsed.presets === null || typeof parsed.presets !== "object" || Array.isArray(parsed.presets)) {
336
- throw new ConfigError(
337
- `Invalid type for 'presets': expected table of string arrays`,
338
- "INVALID_PRESET",
339
- pathToLoad
340
- );
341
- }
342
- const defs = {};
343
- for (const [presetName, value] of Object.entries(
344
- parsed.presets
345
- )) {
346
- if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
347
- throw new ConfigError(
348
- `Invalid preset '${presetName}': expected array of strings`,
349
- "INVALID_PRESET",
350
- pathToLoad
351
- );
1148
+ }
1149
+ for (const specFile of specs.specFiles) {
1150
+ for (const crossRef of specFile.crossRefs) {
1151
+ for (const refId of crossRef.ids) {
1152
+ const codeMatch = /^([A-Z][A-Z0-9]*)[-_]/.exec(refId);
1153
+ if (codeMatch?.[1]) {
1154
+ referencedCodes.add(codeMatch[1]);
352
1155
  }
353
- defs[presetName] = value;
354
- }
355
- config.presets = defs;
356
- }
357
- if (parsed["list-unknown"] !== void 0) {
358
- if (typeof parsed["list-unknown"] !== "boolean") {
359
- throw new ConfigError(
360
- `Invalid type for 'list-unknown': expected boolean, got ${typeof parsed["list-unknown"]}`,
361
- "INVALID_TYPE",
362
- pathToLoad
363
- );
364
- }
365
- config["list-unknown"] = parsed["list-unknown"];
366
- }
367
- const knownKeys = /* @__PURE__ */ new Set([
368
- "output",
369
- "template",
370
- "features",
371
- "preset",
372
- "remove-features",
373
- "presets",
374
- "force",
375
- "dry-run",
376
- "delete",
377
- "refresh",
378
- "list-unknown"
379
- ]);
380
- for (const key of Object.keys(parsed)) {
381
- if (!knownKeys.has(key)) {
382
- logger.warn(`Unknown configuration option: '${key}'`);
383
1156
  }
384
1157
  }
385
- return config;
386
- } catch (error) {
387
- if (error instanceof ConfigError) {
388
- throw error;
1158
+ }
1159
+ for (const specFile of specs.specFiles) {
1160
+ if (!specFile.code) continue;
1161
+ if (!referencedCodes.has(specFile.code)) {
1162
+ findings.push({
1163
+ severity: "warning",
1164
+ code: "orphaned-spec",
1165
+ message: `Spec file code '${specFile.code}' is not referenced by any other spec or code marker`,
1166
+ filePath: specFile.filePath,
1167
+ id: specFile.code
1168
+ });
389
1169
  }
1170
+ }
1171
+ }
1172
+ return { findings };
1173
+ }
1174
+
1175
+ // src/core/check/types.ts
1176
+ var DEFAULT_CHECK_CONFIG = {
1177
+ specGlobs: [
1178
+ ".awa/specs/ARCHITECTURE.md",
1179
+ ".awa/specs/FEAT-*.md",
1180
+ ".awa/specs/REQ-*.md",
1181
+ ".awa/specs/DESIGN-*.md",
1182
+ ".awa/specs/EXAMPLES-*.md",
1183
+ ".awa/specs/API-*.tsp",
1184
+ ".awa/tasks/TASK-*.md",
1185
+ ".awa/plans/PLAN-*.md",
1186
+ ".awa/align/ALIGN-*.md"
1187
+ ],
1188
+ codeGlobs: [
1189
+ "**/*.{ts,js,tsx,jsx,mts,mjs,cjs,py,go,rs,java,kt,kts,cs,c,h,cpp,cc,cxx,hpp,hxx,swift,rb,php,scala,ex,exs,dart,lua,zig}"
1190
+ ],
1191
+ specIgnore: [],
1192
+ codeIgnore: [
1193
+ "node_modules/**",
1194
+ "dist/**",
1195
+ "vendor/**",
1196
+ "target/**",
1197
+ "build/**",
1198
+ "out/**",
1199
+ ".awa/**"
1200
+ ],
1201
+ ignoreMarkers: [],
1202
+ markers: ["@awa-impl", "@awa-test", "@awa-component"],
1203
+ idPattern: "([A-Z][A-Z0-9]*-\\d+(?:\\.\\d+)?(?:_AC-\\d+)?|[A-Z][A-Z0-9]*_P-\\d+)",
1204
+ crossRefPatterns: ["IMPLEMENTS:", "VALIDATES:"],
1205
+ format: "text",
1206
+ schemaDir: ".awa/.agent/schemas",
1207
+ schemaEnabled: true,
1208
+ allowWarnings: false,
1209
+ specOnly: false
1210
+ };
1211
+
1212
+ // src/commands/check.ts
1213
+ async function checkCommand(cliOptions) {
1214
+ try {
1215
+ const fileConfig = await configLoader.load(cliOptions.config ?? null);
1216
+ const config = buildCheckConfig(fileConfig, cliOptions);
1217
+ const emptyMarkers = {
1218
+ markers: [],
1219
+ findings: []
1220
+ };
1221
+ const [markers, specs, ruleSets] = await Promise.all([
1222
+ config.specOnly ? Promise.resolve(emptyMarkers) : scanMarkers(config),
1223
+ parseSpecs(config),
1224
+ config.schemaEnabled ? loadRules(config.schemaDir) : Promise.resolve([])
1225
+ ]);
1226
+ const codeSpecResult = config.specOnly ? { findings: [] } : checkCodeAgainstSpec(markers, specs, config);
1227
+ const specSpecResult = checkSpecAgainstSpec(specs, markers, config);
1228
+ const schemaResult = config.schemaEnabled && ruleSets.length > 0 ? await checkSchemasAsync(specs.specFiles, ruleSets) : { findings: [] };
1229
+ const combinedFindings = [
1230
+ ...markers.findings,
1231
+ ...codeSpecResult.findings,
1232
+ ...specSpecResult.findings,
1233
+ ...schemaResult.findings
1234
+ ];
1235
+ const allFindings = config.allowWarnings ? combinedFindings : combinedFindings.map(
1236
+ (f) => f.severity === "warning" ? { ...f, severity: "error" } : f
1237
+ );
1238
+ report(allFindings, config.format);
1239
+ const hasErrors = allFindings.some((f) => f.severity === "error");
1240
+ return hasErrors ? 1 : 0;
1241
+ } catch (error) {
1242
+ if (error instanceof Error) {
1243
+ logger.error(error.message);
1244
+ } else {
1245
+ logger.error(String(error));
1246
+ }
1247
+ return 2;
1248
+ }
1249
+ }
1250
+ function buildCheckConfig(fileConfig, cliOptions) {
1251
+ const section = fileConfig?.check;
1252
+ const specGlobs = toStringArray(section?.["spec-globs"]) ?? [...DEFAULT_CHECK_CONFIG.specGlobs];
1253
+ const codeGlobs = toStringArray(section?.["code-globs"]) ?? [...DEFAULT_CHECK_CONFIG.codeGlobs];
1254
+ const markers = toStringArray(section?.markers) ?? [...DEFAULT_CHECK_CONFIG.markers];
1255
+ const crossRefPatterns = toStringArray(section?.["cross-ref-patterns"]) ?? [
1256
+ ...DEFAULT_CHECK_CONFIG.crossRefPatterns
1257
+ ];
1258
+ const idPattern = typeof section?.["id-pattern"] === "string" ? section["id-pattern"] : DEFAULT_CHECK_CONFIG.idPattern;
1259
+ const configSpecIgnore = toStringArray(section?.["spec-ignore"]) ?? [
1260
+ ...DEFAULT_CHECK_CONFIG.specIgnore
1261
+ ];
1262
+ const configCodeIgnore = toStringArray(section?.["code-ignore"]) ?? [
1263
+ ...DEFAULT_CHECK_CONFIG.codeIgnore
1264
+ ];
1265
+ const specIgnore = [...configSpecIgnore, ...cliOptions.specIgnore ?? []];
1266
+ const codeIgnore = [...configCodeIgnore, ...cliOptions.codeIgnore ?? []];
1267
+ const ignoreMarkers = toStringArray(section?.["ignore-markers"]) ?? [
1268
+ ...DEFAULT_CHECK_CONFIG.ignoreMarkers
1269
+ ];
1270
+ const format = cliOptions.format === "json" ? "json" : section?.format === "json" ? "json" : DEFAULT_CHECK_CONFIG.format;
1271
+ const schemaDir = typeof section?.["schema-dir"] === "string" ? section["schema-dir"] : DEFAULT_CHECK_CONFIG.schemaDir;
1272
+ const schemaEnabled = typeof section?.["schema-enabled"] === "boolean" ? section["schema-enabled"] : DEFAULT_CHECK_CONFIG.schemaEnabled;
1273
+ const allowWarnings = cliOptions.allowWarnings === true ? true : typeof section?.["allow-warnings"] === "boolean" ? section["allow-warnings"] : DEFAULT_CHECK_CONFIG.allowWarnings;
1274
+ const specOnly = cliOptions.specOnly === true ? true : typeof section?.["spec-only"] === "boolean" ? section["spec-only"] : DEFAULT_CHECK_CONFIG.specOnly;
1275
+ return {
1276
+ specGlobs,
1277
+ codeGlobs,
1278
+ specIgnore,
1279
+ codeIgnore,
1280
+ ignoreMarkers,
1281
+ markers,
1282
+ idPattern,
1283
+ crossRefPatterns,
1284
+ format,
1285
+ schemaDir,
1286
+ schemaEnabled,
1287
+ allowWarnings,
1288
+ specOnly
1289
+ };
1290
+ }
1291
+ function toStringArray(value) {
1292
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
1293
+ return value;
1294
+ }
1295
+ return null;
1296
+ }
1297
+
1298
+ // src/commands/diff.ts
1299
+ import { intro, outro } from "@clack/prompts";
1300
+
1301
+ // src/core/batch-runner.ts
1302
+ var BatchRunner = class {
1303
+ // Resolve all targets or a single named target from config
1304
+ resolveTargets(cli, fileConfig, mode, targetName) {
1305
+ if (!fileConfig) {
390
1306
  throw new ConfigError(
391
- `Failed to parse TOML configuration: ${error instanceof Error ? error.message : String(error)}`,
392
- "PARSE_ERROR",
393
- pathToLoad
1307
+ "No configuration file found. --all and --target require a config file with [targets.*] sections.",
1308
+ "NO_TARGETS",
1309
+ null
394
1310
  );
395
1311
  }
396
- }
397
- // @awa-impl: CFG-4_AC-1, CFG-4_AC-2, CFG-4_AC-3, CFG-4_AC-4
398
- // @awa-impl: CLI-2_AC-2, CLI-2_AC-3, CLI-2_AC-4
399
- merge(cli, file) {
400
- const output = cli.output ?? file?.output;
401
- if (!output) {
1312
+ const targetNames = configLoader.getTargetNames(fileConfig);
1313
+ if (targetNames.length === 0) {
402
1314
  throw new ConfigError(
403
- "Output directory is required. Provide it as a positional argument or in the config file.",
404
- "MISSING_OUTPUT",
1315
+ "No targets defined in configuration. Add [targets.<name>] sections to .awa.toml.",
1316
+ "NO_TARGETS",
405
1317
  null
406
1318
  );
407
1319
  }
408
- const template = cli.template ?? file?.template ?? null;
409
- const features = cli.features ?? file?.features ?? [];
410
- const preset = cli.preset ?? file?.preset ?? [];
411
- const removeFeatures = cli.removeFeatures ?? file?.["remove-features"] ?? [];
412
- const presets = file?.presets ?? {};
413
- const force = cli.force ?? file?.force ?? false;
414
- const dryRun = cli.dryRun ?? file?.["dry-run"] ?? false;
415
- const enableDelete = cli.delete ?? file?.delete ?? false;
416
- const refresh = cli.refresh ?? file?.refresh ?? false;
417
- const listUnknown = cli.listUnknown ?? file?.["list-unknown"] ?? false;
418
- return {
419
- output,
420
- template,
421
- features,
422
- preset,
423
- removeFeatures,
424
- force,
425
- dryRun,
426
- delete: enableDelete,
427
- refresh,
428
- presets,
429
- listUnknown
430
- };
1320
+ const namesToProcess = mode === "all" ? targetNames : [targetName];
1321
+ const results = [];
1322
+ for (const name of namesToProcess) {
1323
+ const resolved = configLoader.resolveTarget(name, fileConfig);
1324
+ const targetCli = {
1325
+ ...cli,
1326
+ output: mode === "all" ? void 0 : cli.output
1327
+ };
1328
+ let options;
1329
+ try {
1330
+ options = configLoader.merge(targetCli, resolved);
1331
+ } catch (error) {
1332
+ if (error instanceof ConfigError && error.code === "MISSING_OUTPUT") {
1333
+ throw new ConfigError(
1334
+ `Target '${name}' has no output directory. Specify 'output' in [targets.${name}] or in the root config.`,
1335
+ "MISSING_OUTPUT",
1336
+ null
1337
+ );
1338
+ }
1339
+ throw error;
1340
+ }
1341
+ results.push({ targetName: name, options });
1342
+ }
1343
+ return results;
1344
+ }
1345
+ // Log a message prefixed with target name
1346
+ logForTarget(targetName, message) {
1347
+ logger.info(`[${targetName}] ${message}`);
1348
+ }
1349
+ warnForTarget(targetName, message) {
1350
+ logger.warn(`[${targetName}] ${message}`);
1351
+ }
1352
+ errorForTarget(targetName, message) {
1353
+ logger.error(`[${targetName}] ${message}`);
431
1354
  }
432
1355
  };
433
- var configLoader = new ConfigLoader();
1356
+ var batchRunner = new BatchRunner();
434
1357
 
435
1358
  // src/core/differ.ts
436
1359
  import { tmpdir } from "os";
@@ -661,7 +1584,8 @@ var TemplateEngine = class {
661
1584
  try {
662
1585
  const templateContent = await readTextFile(templatePath);
663
1586
  const rendered = await this.eta.renderStringAsync(templateContent, {
664
- features: context.features
1587
+ features: context.features,
1588
+ version: context.version ?? ""
665
1589
  });
666
1590
  const trimmed = rendered.trim();
667
1591
  const isEmpty = trimmed.length === 0;
@@ -703,7 +1627,10 @@ var FileGenerator = class {
703
1627
  try {
704
1628
  for await (const templateFile of this.walkTemplates(templatePath)) {
705
1629
  const outputFile = this.computeOutputPath(templateFile, templatePath, outputPath);
706
- const result = await templateEngine.render(templateFile, { features });
1630
+ const result = await templateEngine.render(templateFile, {
1631
+ features,
1632
+ version: PACKAGE_INFO.version
1633
+ });
707
1634
  if (result.isEmpty && !result.isEmptyFileMarker) {
708
1635
  actions.push({
709
1636
  type: "skip-empty",
@@ -1073,9 +2000,62 @@ var FeatureResolver = class {
1073
2000
  };
1074
2001
  var featureResolver = new FeatureResolver();
1075
2002
 
2003
+ // src/core/json-output.ts
2004
+ function serializeGenerationResult(result) {
2005
+ const actions = result.actions.map((action) => ({
2006
+ type: action.type,
2007
+ path: action.outputPath
2008
+ }));
2009
+ return {
2010
+ actions,
2011
+ counts: {
2012
+ created: result.created,
2013
+ overwritten: result.overwritten,
2014
+ skipped: result.skipped,
2015
+ deleted: result.deleted
2016
+ }
2017
+ };
2018
+ }
2019
+ function serializeDiffResult(result) {
2020
+ const diffs = result.files.map((file) => {
2021
+ const entry = {
2022
+ path: file.relativePath,
2023
+ status: file.status
2024
+ };
2025
+ if (file.unifiedDiff) {
2026
+ entry.diff = file.unifiedDiff;
2027
+ }
2028
+ return entry;
2029
+ });
2030
+ return {
2031
+ diffs,
2032
+ counts: {
2033
+ changed: result.modified,
2034
+ new: result.newFiles,
2035
+ matching: result.identical,
2036
+ deleted: result.deleteListed
2037
+ }
2038
+ };
2039
+ }
2040
+ function formatGenerationSummary(result) {
2041
+ return `created: ${result.created}, overwritten: ${result.overwritten}, skipped: ${result.skipped}, deleted: ${result.deleted}`;
2042
+ }
2043
+ function formatDiffSummary(result) {
2044
+ return `changed: ${result.modified}, new: ${result.newFiles}, matching: ${result.identical}, deleted: ${result.deleteListed}`;
2045
+ }
2046
+ function writeJsonOutput(data) {
2047
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
2048
+ `);
2049
+ }
2050
+
2051
+ // src/core/overlay.ts
2052
+ import { cp } from "fs/promises";
2053
+ import { tmpdir as tmpdir2 } from "os";
2054
+ import { join as join6 } from "path";
2055
+
1076
2056
  // src/core/template-resolver.ts
1077
2057
  import { createHash } from "crypto";
1078
- import { rm as rm2 } from "fs/promises";
2058
+ import { rm } from "fs/promises";
1079
2059
  import { isAbsolute, join as join5, resolve } from "path";
1080
2060
  import degit from "degit";
1081
2061
  var TemplateResolver = class {
@@ -1119,7 +2099,7 @@ var TemplateResolver = class {
1119
2099
  try {
1120
2100
  if (cacheExists && refresh) {
1121
2101
  logger.info(`Refreshing template: ${source}`);
1122
- await rm2(cachePath, { recursive: true, force: true });
2102
+ await rm(cachePath, { recursive: true, force: true });
1123
2103
  } else {
1124
2104
  logger.info(`Fetching template: ${source}`);
1125
2105
  }
@@ -1145,7 +2125,7 @@ var TemplateResolver = class {
1145
2125
  source
1146
2126
  );
1147
2127
  }
1148
- // @awa-impl: TPL-2_AC-1 through TPL-2_AC-6
2128
+ // @awa-impl: TPL-2_AC-1, TPL-2_AC-2, TPL-2_AC-3, TPL-2_AC-4, TPL-2_AC-5, TPL-2_AC-6
1149
2129
  detectType(source) {
1150
2130
  if (source.startsWith(".") || source.startsWith("/") || source.startsWith("~")) {
1151
2131
  return "local";
@@ -1155,38 +2135,99 @@ var TemplateResolver = class {
1155
2135
  }
1156
2136
  return "git";
1157
2137
  }
1158
- // @awa-impl: TPL-3_AC-1
1159
- getCachePath(source) {
1160
- const hash = createHash("sha256").update(source).digest("hex").substring(0, 16);
1161
- const cacheDir = getCacheDir();
1162
- return join5(cacheDir, hash);
2138
+ // @awa-impl: TPL-3_AC-1
2139
+ getCachePath(source) {
2140
+ const hash = createHash("sha256").update(source).digest("hex").substring(0, 16);
2141
+ const cacheDir = getCacheDir();
2142
+ return join5(cacheDir, hash);
2143
+ }
2144
+ };
2145
+ var templateResolver = new TemplateResolver();
2146
+
2147
+ // src/core/overlay.ts
2148
+ async function resolveOverlays(overlays, refresh) {
2149
+ const dirs = [];
2150
+ for (const source of overlays) {
2151
+ const resolved = await templateResolver.resolve(source, refresh);
2152
+ dirs.push(resolved.localPath);
2153
+ }
2154
+ return dirs;
2155
+ }
2156
+ async function buildMergedDir(baseDir, overlayDirs) {
2157
+ const timestamp = Date.now();
2158
+ const random = Math.random().toString(36).substring(2, 8);
2159
+ const tempPath = join6(tmpdir2(), `awa-overlay-${timestamp}-${random}`);
2160
+ await ensureDir(tempPath);
2161
+ await cp(baseDir, tempPath, { recursive: true });
2162
+ for (const overlayDir of overlayDirs) {
2163
+ await cp(overlayDir, tempPath, { recursive: true });
2164
+ }
2165
+ return tempPath;
2166
+ }
2167
+
2168
+ // src/utils/file-watcher.ts
2169
+ import { watch } from "fs";
2170
+
2171
+ // src/utils/debouncer.ts
2172
+ var Debouncer = class {
2173
+ constructor(delayMs) {
2174
+ this.delayMs = delayMs;
2175
+ }
2176
+ timer = null;
2177
+ trigger(callback) {
2178
+ if (this.timer) {
2179
+ clearTimeout(this.timer);
2180
+ }
2181
+ this.timer = setTimeout(() => {
2182
+ this.timer = null;
2183
+ callback();
2184
+ }, this.delayMs);
2185
+ }
2186
+ cancel() {
2187
+ if (this.timer) {
2188
+ clearTimeout(this.timer);
2189
+ this.timer = null;
2190
+ }
2191
+ }
2192
+ };
2193
+
2194
+ // src/utils/file-watcher.ts
2195
+ var FileWatcher = class {
2196
+ watcher = null;
2197
+ debouncer;
2198
+ directory;
2199
+ onChange;
2200
+ constructor(options) {
2201
+ this.directory = options.directory;
2202
+ this.debouncer = new Debouncer(options.debounceMs ?? 300);
2203
+ this.onChange = options.onChange;
2204
+ }
2205
+ start() {
2206
+ this.watcher = watch(this.directory, { recursive: true }, () => {
2207
+ this.debouncer.trigger(this.onChange);
2208
+ });
2209
+ }
2210
+ stop() {
2211
+ this.debouncer.cancel();
2212
+ if (this.watcher) {
2213
+ this.watcher.close();
2214
+ this.watcher = null;
2215
+ }
1163
2216
  }
1164
2217
  };
1165
- var templateResolver = new TemplateResolver();
1166
2218
 
1167
2219
  // src/commands/diff.ts
1168
- async function diffCommand(cliOptions) {
2220
+ async function runDiff(diffOptions, options, mergedDir) {
1169
2221
  try {
1170
- intro("awa CLI - Template Diff");
1171
- const fileConfig = await configLoader.load(cliOptions.config ?? null);
1172
- const options = configLoader.merge(cliOptions, fileConfig);
1173
- if (!await pathExists(options.output)) {
1174
- throw new DiffError(`Target directory does not exist: ${options.output}`);
2222
+ const result = await diffEngine.diff(diffOptions);
2223
+ if (options.json) {
2224
+ writeJsonOutput(serializeDiffResult(result));
2225
+ return result.hasDifferences ? 1 : 0;
2226
+ }
2227
+ if (options.summary) {
2228
+ console.log(formatDiffSummary(result));
2229
+ return result.hasDifferences ? 1 : 0;
1175
2230
  }
1176
- const targetPath = options.output;
1177
- const template = await templateResolver.resolve(options.template, options.refresh);
1178
- const features = featureResolver.resolve({
1179
- baseFeatures: [...options.features],
1180
- presetNames: [...options.preset],
1181
- removeFeatures: [...options.removeFeatures],
1182
- presetDefinitions: options.presets
1183
- });
1184
- const result = await diffEngine.diff({
1185
- templatePath: template.localPath,
1186
- targetPath,
1187
- features,
1188
- listUnknown: options.listUnknown
1189
- });
1190
2231
  for (const file of result.files) {
1191
2232
  switch (file.status) {
1192
2233
  case "modified":
@@ -1225,8 +2266,106 @@ async function diffCommand(cliOptions) {
1225
2266
  }
1226
2267
  }
1227
2268
  logger.diffSummary(result);
1228
- outro("Diff complete!");
1229
2269
  return result.hasDifferences ? 1 : 0;
2270
+ } finally {
2271
+ if (mergedDir) {
2272
+ try {
2273
+ await rmDir(mergedDir);
2274
+ } catch {
2275
+ }
2276
+ }
2277
+ }
2278
+ }
2279
+ async function prepareDiff(options) {
2280
+ if (!await pathExists(options.output)) {
2281
+ throw new DiffError(`Target directory does not exist: ${options.output}`);
2282
+ }
2283
+ const targetPath = options.output;
2284
+ const template = await templateResolver.resolve(options.template, options.refresh);
2285
+ const features = featureResolver.resolve({
2286
+ baseFeatures: [...options.features],
2287
+ presetNames: [...options.preset],
2288
+ removeFeatures: [...options.removeFeatures],
2289
+ presetDefinitions: options.presets
2290
+ });
2291
+ let mergedDir = null;
2292
+ let templatePath = template.localPath;
2293
+ if (options.overlay.length > 0) {
2294
+ const overlayDirs = await resolveOverlays([...options.overlay], options.refresh);
2295
+ mergedDir = await buildMergedDir(template.localPath, overlayDirs);
2296
+ templatePath = mergedDir;
2297
+ }
2298
+ return {
2299
+ diffOptions: {
2300
+ templatePath,
2301
+ targetPath,
2302
+ features,
2303
+ listUnknown: options.listUnknown
2304
+ },
2305
+ template,
2306
+ mergedDir
2307
+ };
2308
+ }
2309
+ async function diffCommand(cliOptions) {
2310
+ try {
2311
+ const fileConfig = await configLoader.load(cliOptions.config ?? null);
2312
+ if (cliOptions.all || cliOptions.target) {
2313
+ const mode = cliOptions.all ? "all" : "single";
2314
+ const targets = batchRunner.resolveTargets(cliOptions, fileConfig, mode, cliOptions.target);
2315
+ let hasDifferences = false;
2316
+ for (const { targetName, options: options2 } of targets) {
2317
+ batchRunner.logForTarget(targetName, "Starting diff...");
2318
+ const { diffOptions: diffOptions2, mergedDir: mergedDir2 } = await prepareDiff(options2);
2319
+ const exitCode = await runDiff(diffOptions2, options2, mergedDir2);
2320
+ if (exitCode === 1) {
2321
+ hasDifferences = true;
2322
+ }
2323
+ batchRunner.logForTarget(targetName, "Diff complete.");
2324
+ }
2325
+ outro("All targets diffed!");
2326
+ return hasDifferences ? 1 : 0;
2327
+ }
2328
+ const options = configLoader.merge(cliOptions, fileConfig);
2329
+ const silent = options.json || options.summary;
2330
+ if (!silent) {
2331
+ intro("awa CLI - Template Diff");
2332
+ }
2333
+ const { diffOptions, template, mergedDir } = await prepareDiff(options);
2334
+ if (cliOptions.watch && template.type !== "local" && template.type !== "bundled") {
2335
+ throw new DiffError("--watch is only supported with local template sources");
2336
+ }
2337
+ const result = await runDiff(diffOptions, options, mergedDir);
2338
+ if (!cliOptions.watch) {
2339
+ if (!silent) {
2340
+ outro("Diff complete!");
2341
+ }
2342
+ return result;
2343
+ }
2344
+ logger.info(`Watching for changes in ${template.localPath}...`);
2345
+ return new Promise((resolve2) => {
2346
+ let running = false;
2347
+ const watcher = new FileWatcher({
2348
+ directory: template.localPath,
2349
+ onChange: async () => {
2350
+ if (running) return;
2351
+ running = true;
2352
+ try {
2353
+ console.clear();
2354
+ logger.info(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Change detected, re-running diff...`);
2355
+ logger.info("---");
2356
+ await runDiff(diffOptions, options, null);
2357
+ } finally {
2358
+ running = false;
2359
+ }
2360
+ }
2361
+ });
2362
+ watcher.start();
2363
+ process.once("SIGINT", () => {
2364
+ watcher.stop();
2365
+ logger.info("\nWatch mode stopped.");
2366
+ resolve2(0);
2367
+ });
2368
+ });
1230
2369
  } catch (error) {
1231
2370
  if (error instanceof Error) {
1232
2371
  logger.error(error.message);
@@ -1237,8 +2376,161 @@ async function diffCommand(cliOptions) {
1237
2376
  }
1238
2377
  }
1239
2378
 
2379
+ // src/commands/features.ts
2380
+ import { intro as intro2, outro as outro2 } from "@clack/prompts";
2381
+
2382
+ // src/core/features/reporter.ts
2383
+ import chalk3 from "chalk";
2384
+ var FeaturesReporter = class {
2385
+ // @awa-impl: DISC-6_AC-1, DISC-7_AC-1
2386
+ /** Render the features report to stdout. */
2387
+ report(options) {
2388
+ const { scanResult, json, presets } = options;
2389
+ if (json) {
2390
+ this.reportJson(scanResult, presets);
2391
+ } else {
2392
+ this.reportTable(scanResult, presets);
2393
+ }
2394
+ }
2395
+ // @awa-impl: DISC-6_AC-1
2396
+ /** Build the JSON output object (also used by tests). */
2397
+ buildJsonOutput(scanResult, presets) {
2398
+ const output = {
2399
+ features: scanResult.features.map((f) => ({
2400
+ name: f.name,
2401
+ files: f.files
2402
+ })),
2403
+ filesScanned: scanResult.filesScanned
2404
+ };
2405
+ if (presets && Object.keys(presets).length > 0) {
2406
+ output.presets = presets;
2407
+ }
2408
+ return output;
2409
+ }
2410
+ reportJson(scanResult, presets) {
2411
+ const output = this.buildJsonOutput(scanResult, presets);
2412
+ console.log(JSON.stringify(output, null, 2));
2413
+ }
2414
+ // @awa-impl: DISC-7_AC-1
2415
+ reportTable(scanResult, presets) {
2416
+ const { features, filesScanned } = scanResult;
2417
+ if (features.length === 0) {
2418
+ console.log(chalk3.yellow("No feature flags found."));
2419
+ console.log(chalk3.dim(`(${filesScanned} files scanned)`));
2420
+ return;
2421
+ }
2422
+ console.log(chalk3.bold(`Feature flags (${features.length} found):
2423
+ `));
2424
+ for (const feature of features) {
2425
+ console.log(` ${chalk3.cyan(feature.name)}`);
2426
+ for (const file of feature.files) {
2427
+ console.log(` ${chalk3.dim(file)}`);
2428
+ }
2429
+ }
2430
+ console.log("");
2431
+ console.log(chalk3.dim(`${filesScanned} files scanned`));
2432
+ if (presets && Object.keys(presets).length > 0) {
2433
+ console.log("");
2434
+ console.log(chalk3.bold("Presets (from .awa.toml):\n"));
2435
+ for (const [name, flags] of Object.entries(presets)) {
2436
+ console.log(` ${chalk3.green(name)}: ${flags.join(", ")}`);
2437
+ }
2438
+ }
2439
+ }
2440
+ };
2441
+ var featuresReporter = new FeaturesReporter();
2442
+
2443
+ // src/core/features/scanner.ts
2444
+ import { readdir, readFile as readFile5 } from "fs/promises";
2445
+ import { join as join7, relative as relative3 } from "path";
2446
+ var FEATURE_PATTERN = /it\.features\.(?:includes|indexOf)\(\s*['"]([^'"]+)['"]\s*\)/g;
2447
+ async function* walkAllFiles(dir) {
2448
+ const entries = await readdir(dir, { withFileTypes: true });
2449
+ for (const entry of entries) {
2450
+ const fullPath = join7(dir, entry.name);
2451
+ if (entry.isDirectory()) {
2452
+ yield* walkAllFiles(fullPath);
2453
+ } else if (entry.isFile()) {
2454
+ yield fullPath;
2455
+ }
2456
+ }
2457
+ }
2458
+ var FeatureScanner = class {
2459
+ // @awa-impl: DISC-1_AC-1, DISC-2_AC-1
2460
+ /** Extract feature flag names from a single file's content. */
2461
+ extractFlags(content) {
2462
+ const flags = /* @__PURE__ */ new Set();
2463
+ for (const match of content.matchAll(FEATURE_PATTERN)) {
2464
+ if (match[1]) {
2465
+ flags.add(match[1]);
2466
+ }
2467
+ }
2468
+ return [...flags];
2469
+ }
2470
+ // @awa-impl: DISC-1_AC-1, DISC-2_AC-1, DISC-3_AC-1
2471
+ /** Scan a template directory and return all discovered feature flags. */
2472
+ async scan(templatePath) {
2473
+ const flagToFiles = /* @__PURE__ */ new Map();
2474
+ let filesScanned = 0;
2475
+ for await (const filePath of walkAllFiles(templatePath)) {
2476
+ filesScanned++;
2477
+ try {
2478
+ const content = await readFile5(filePath, "utf-8");
2479
+ const flags = this.extractFlags(content);
2480
+ const relPath = relative3(templatePath, filePath);
2481
+ for (const flag of flags) {
2482
+ const existing = flagToFiles.get(flag);
2483
+ if (existing) {
2484
+ existing.add(relPath);
2485
+ } else {
2486
+ flagToFiles.set(flag, /* @__PURE__ */ new Set([relPath]));
2487
+ }
2488
+ }
2489
+ } catch {
2490
+ }
2491
+ }
2492
+ const features = [...flagToFiles.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, files]) => ({
2493
+ name,
2494
+ files: [...files].sort()
2495
+ }));
2496
+ return { features, filesScanned };
2497
+ }
2498
+ };
2499
+ var featureScanner = new FeatureScanner();
2500
+
2501
+ // src/commands/features.ts
2502
+ async function featuresCommand(cliOptions) {
2503
+ try {
2504
+ if (!cliOptions.json) {
2505
+ intro2("awa CLI - Feature Discovery");
2506
+ }
2507
+ const fileConfig = await configLoader.load(cliOptions.config ?? null);
2508
+ const templateSource = cliOptions.template ?? fileConfig?.template ?? null;
2509
+ const refresh = cliOptions.refresh ?? fileConfig?.refresh ?? false;
2510
+ const template = await templateResolver.resolve(templateSource, refresh);
2511
+ const scanResult = await featureScanner.scan(template.localPath);
2512
+ const presets = fileConfig?.presets;
2513
+ featuresReporter.report({
2514
+ scanResult,
2515
+ json: cliOptions.json ?? false,
2516
+ presets
2517
+ });
2518
+ if (!cliOptions.json) {
2519
+ outro2("Feature discovery complete!");
2520
+ }
2521
+ return 0;
2522
+ } catch (error) {
2523
+ if (error instanceof Error) {
2524
+ logger.error(error.message);
2525
+ } else {
2526
+ logger.error(String(error));
2527
+ }
2528
+ return 1;
2529
+ }
2530
+ }
2531
+
1240
2532
  // src/commands/generate.ts
1241
- import { intro as intro2, isCancel as isCancel2, multiselect as multiselect2, outro as outro2 } from "@clack/prompts";
2533
+ import { intro as intro3, isCancel as isCancel2, multiselect as multiselect2, outro as outro3 } from "@clack/prompts";
1242
2534
  var TOOL_FEATURES = [
1243
2535
  { value: "copilot", label: "GitHub Copilot" },
1244
2536
  { value: "claude", label: "Claude Code" },
@@ -1254,20 +2546,18 @@ var TOOL_FEATURES = [
1254
2546
  { value: "agents-md", label: "AGENTS.md (cross-tool)" }
1255
2547
  ];
1256
2548
  var TOOL_FEATURE_VALUES = new Set(TOOL_FEATURES.map((t) => t.value));
1257
- async function generateCommand(cliOptions) {
1258
- try {
1259
- intro2("awa CLI - Template Generator");
1260
- const fileConfig = await configLoader.load(cliOptions.config ?? null);
1261
- const options = configLoader.merge(cliOptions, fileConfig);
1262
- const template = await templateResolver.resolve(options.template, options.refresh);
1263
- const features = featureResolver.resolve({
1264
- baseFeatures: [...options.features],
1265
- presetNames: [...options.preset],
1266
- removeFeatures: [...options.removeFeatures],
1267
- presetDefinitions: options.presets
1268
- });
2549
+ async function runGenerate(options, batchMode) {
2550
+ const silent = options.json || options.summary;
2551
+ const template = await templateResolver.resolve(options.template, options.refresh);
2552
+ const features = featureResolver.resolve({
2553
+ baseFeatures: [...options.features],
2554
+ presetNames: [...options.preset],
2555
+ removeFeatures: [...options.removeFeatures],
2556
+ presetDefinitions: options.presets
2557
+ });
2558
+ if (!batchMode) {
1269
2559
  const hasToolFlag = features.some((f) => TOOL_FEATURE_VALUES.has(f));
1270
- if (!hasToolFlag) {
2560
+ if (!hasToolFlag && !silent) {
1271
2561
  const selected = await multiselect2({
1272
2562
  message: "Select AI tools to generate for (space to toggle, enter to confirm):",
1273
2563
  options: TOOL_FEATURES.map((t) => ({ value: t.value, label: t.label })),
@@ -1279,22 +2569,74 @@ async function generateCommand(cliOptions) {
1279
2569
  }
1280
2570
  features.push(...selected);
1281
2571
  }
1282
- if (options.dryRun) {
2572
+ }
2573
+ const effectiveDryRun = options.json || options.dryRun;
2574
+ if (!silent) {
2575
+ if (effectiveDryRun) {
1283
2576
  logger.info("Running in dry-run mode (no files will be modified)");
1284
2577
  }
1285
2578
  if (options.force) {
1286
2579
  logger.info("Force mode enabled (existing files will be overwritten)");
1287
2580
  }
2581
+ }
2582
+ let mergedDir = null;
2583
+ let templatePath = template.localPath;
2584
+ if (options.overlay.length > 0) {
2585
+ const overlayDirs = await resolveOverlays([...options.overlay], options.refresh);
2586
+ mergedDir = await buildMergedDir(template.localPath, overlayDirs);
2587
+ templatePath = mergedDir;
2588
+ }
2589
+ try {
1288
2590
  const result = await fileGenerator.generate({
1289
- templatePath: template.localPath,
2591
+ templatePath,
1290
2592
  outputPath: options.output,
1291
2593
  features,
1292
2594
  force: options.force,
1293
- dryRun: options.dryRun,
2595
+ dryRun: effectiveDryRun,
1294
2596
  delete: options.delete
1295
2597
  });
1296
- logger.summary(result);
1297
- outro2("Generation complete!");
2598
+ if (options.json) {
2599
+ writeJsonOutput(serializeGenerationResult(result));
2600
+ } else if (options.summary) {
2601
+ console.log(formatGenerationSummary(result));
2602
+ } else {
2603
+ logger.summary(result);
2604
+ }
2605
+ } finally {
2606
+ if (mergedDir) {
2607
+ try {
2608
+ await rmDir(mergedDir);
2609
+ } catch {
2610
+ }
2611
+ }
2612
+ }
2613
+ }
2614
+ async function generateCommand(cliOptions) {
2615
+ try {
2616
+ const fileConfig = await configLoader.load(cliOptions.config ?? null);
2617
+ if (!cliOptions.config && fileConfig === null) {
2618
+ logger.info("Tip: create .awa.toml to save your options for next time.");
2619
+ }
2620
+ if (cliOptions.all || cliOptions.target) {
2621
+ const mode = cliOptions.all ? "all" : "single";
2622
+ const targets = batchRunner.resolveTargets(cliOptions, fileConfig, mode, cliOptions.target);
2623
+ for (const { targetName, options: options2 } of targets) {
2624
+ batchRunner.logForTarget(targetName, "Starting generation...");
2625
+ await runGenerate(options2, true);
2626
+ batchRunner.logForTarget(targetName, "Generation complete.");
2627
+ }
2628
+ outro3("All targets generated!");
2629
+ return;
2630
+ }
2631
+ const options = configLoader.merge(cliOptions, fileConfig);
2632
+ const silent = options.json || options.summary;
2633
+ if (!silent) {
2634
+ intro3("awa CLI - Template Generator");
2635
+ }
2636
+ await runGenerate(options, false);
2637
+ if (!silent) {
2638
+ outro3("Generation complete!");
2639
+ }
1298
2640
  } catch (error) {
1299
2641
  if (error instanceof Error) {
1300
2642
  logger.error(error.message);
@@ -1305,49 +2647,483 @@ async function generateCommand(cliOptions) {
1305
2647
  }
1306
2648
  }
1307
2649
 
2650
+ // src/commands/test.ts
2651
+ import { intro as intro4, outro as outro4 } from "@clack/prompts";
2652
+
2653
+ // src/core/template-test/fixture-loader.ts
2654
+ import { readdir as readdir2 } from "fs/promises";
2655
+ import { basename as basename2, extname, join as join8 } from "path";
2656
+ import { parse } from "smol-toml";
2657
+ async function discoverFixtures(templatePath) {
2658
+ const testsDir = join8(templatePath, "_tests");
2659
+ let entries;
2660
+ try {
2661
+ const dirEntries = await readdir2(testsDir, { withFileTypes: true });
2662
+ entries = dirEntries.filter((e) => e.isFile() && extname(e.name) === ".toml").map((e) => e.name).sort();
2663
+ } catch {
2664
+ return [];
2665
+ }
2666
+ const fixtures = [];
2667
+ for (const filename of entries) {
2668
+ const filePath = join8(testsDir, filename);
2669
+ const fixture = await parseFixture(filePath);
2670
+ fixtures.push(fixture);
2671
+ }
2672
+ return fixtures;
2673
+ }
2674
+ async function parseFixture(filePath) {
2675
+ const content = await readTextFile(filePath);
2676
+ const parsed = parse(content);
2677
+ const name = basename2(filePath, extname(filePath));
2678
+ const features = toStringArray2(parsed.features) ?? [];
2679
+ const preset = toStringArray2(parsed.preset) ?? [];
2680
+ const removeFeatures = toStringArray2(parsed["remove-features"]) ?? [];
2681
+ const expectedFiles = toStringArray2(parsed["expected-files"]) ?? [];
2682
+ return {
2683
+ name,
2684
+ features,
2685
+ preset,
2686
+ removeFeatures,
2687
+ expectedFiles,
2688
+ filePath
2689
+ };
2690
+ }
2691
+ function toStringArray2(value) {
2692
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
2693
+ return value;
2694
+ }
2695
+ return null;
2696
+ }
2697
+
2698
+ // src/core/template-test/reporter.ts
2699
+ import chalk4 from "chalk";
2700
+ function report2(result) {
2701
+ console.log("");
2702
+ for (const fixture of result.results) {
2703
+ reportFixture(fixture);
2704
+ }
2705
+ console.log("");
2706
+ console.log(chalk4.bold("Test Summary:"));
2707
+ console.log(` Total: ${result.total}`);
2708
+ console.log(chalk4.green(` Passed: ${result.passed}`));
2709
+ if (result.failed > 0) {
2710
+ console.log(chalk4.red(` Failed: ${result.failed}`));
2711
+ }
2712
+ console.log("");
2713
+ }
2714
+ function reportFixture(fixture) {
2715
+ const icon = fixture.passed ? chalk4.green("\u2714") : chalk4.red("\u2716");
2716
+ console.log(`${icon} ${fixture.name}`);
2717
+ if (fixture.error) {
2718
+ console.log(chalk4.red(` Error: ${fixture.error}`));
2719
+ return;
2720
+ }
2721
+ const missingFiles = fixture.fileResults.filter((r) => !r.found);
2722
+ for (const missing of missingFiles) {
2723
+ console.log(chalk4.red(` Missing file: ${missing.path}`));
2724
+ }
2725
+ const snapshotFailures = fixture.snapshotResults.filter((r) => r.status !== "match");
2726
+ for (const failure of snapshotFailures) {
2727
+ switch (failure.status) {
2728
+ case "mismatch":
2729
+ console.log(chalk4.yellow(` Snapshot mismatch: ${failure.path}`));
2730
+ break;
2731
+ case "missing-snapshot":
2732
+ console.log(chalk4.yellow(` Missing snapshot: ${failure.path}`));
2733
+ break;
2734
+ case "extra-snapshot":
2735
+ console.log(chalk4.yellow(` Extra snapshot (not in output): ${failure.path}`));
2736
+ break;
2737
+ }
2738
+ }
2739
+ }
2740
+
2741
+ // src/core/template-test/runner.ts
2742
+ import { mkdir as mkdir2, rm as rm3 } from "fs/promises";
2743
+ import { tmpdir as tmpdir3 } from "os";
2744
+ import { join as join10 } from "path";
2745
+
2746
+ // src/core/template-test/snapshot.ts
2747
+ import { mkdir, readdir as readdir3, rm as rm2 } from "fs/promises";
2748
+ import { join as join9, relative as relative4 } from "path";
2749
+ async function walkRelative(dir, base) {
2750
+ const results = [];
2751
+ const entries = await readdir3(dir, { withFileTypes: true });
2752
+ for (const entry of entries) {
2753
+ const fullPath = join9(dir, entry.name);
2754
+ if (entry.isDirectory()) {
2755
+ const sub = await walkRelative(fullPath, base);
2756
+ results.push(...sub);
2757
+ } else if (entry.isFile()) {
2758
+ results.push(relative4(base, fullPath));
2759
+ }
2760
+ }
2761
+ return results;
2762
+ }
2763
+ async function compareSnapshots(renderedDir, snapshotDir) {
2764
+ const results = [];
2765
+ const renderedFiles = await walkRelative(renderedDir, renderedDir);
2766
+ const snapshotFiles = await walkRelative(snapshotDir, snapshotDir);
2767
+ const snapshotSet = new Set(snapshotFiles);
2768
+ const renderedSet = new Set(renderedFiles);
2769
+ for (const file of renderedFiles) {
2770
+ const renderedPath = join9(renderedDir, file);
2771
+ const snapshotPath = join9(snapshotDir, file);
2772
+ if (!snapshotSet.has(file)) {
2773
+ results.push({ path: file, status: "missing-snapshot" });
2774
+ continue;
2775
+ }
2776
+ const renderedContent = await readTextFile(renderedPath);
2777
+ const snapshotContent = await readTextFile(snapshotPath);
2778
+ results.push({
2779
+ path: file,
2780
+ status: renderedContent === snapshotContent ? "match" : "mismatch"
2781
+ });
2782
+ }
2783
+ for (const file of snapshotFiles) {
2784
+ if (!renderedSet.has(file)) {
2785
+ results.push({ path: file, status: "extra-snapshot" });
2786
+ }
2787
+ }
2788
+ return results;
2789
+ }
2790
+ async function updateSnapshots(renderedDir, snapshotDir) {
2791
+ if (await pathExists(snapshotDir)) {
2792
+ await rm2(snapshotDir, { recursive: true, force: true });
2793
+ }
2794
+ await mkdir(snapshotDir, { recursive: true });
2795
+ const files = await walkRelative(renderedDir, renderedDir);
2796
+ for (const file of files) {
2797
+ const srcPath = join9(renderedDir, file);
2798
+ const destPath = join9(snapshotDir, file);
2799
+ const content = await readTextFile(srcPath);
2800
+ await writeTextFile(destPath, content);
2801
+ }
2802
+ }
2803
+
2804
+ // src/core/template-test/runner.ts
2805
+ async function runFixture(fixture, templatePath, options, presetDefinitions = {}) {
2806
+ const tempDir = join10(tmpdir3(), `awa-test-${fixture.name}-${Date.now()}`);
2807
+ try {
2808
+ await mkdir2(tempDir, { recursive: true });
2809
+ const features = featureResolver.resolve({
2810
+ baseFeatures: [...fixture.features],
2811
+ presetNames: [...fixture.preset],
2812
+ removeFeatures: [...fixture.removeFeatures],
2813
+ presetDefinitions
2814
+ });
2815
+ await fileGenerator.generate({
2816
+ templatePath,
2817
+ outputPath: tempDir,
2818
+ features,
2819
+ force: true,
2820
+ dryRun: false,
2821
+ delete: false
2822
+ });
2823
+ const fileResults = [];
2824
+ for (const expectedFile of fixture.expectedFiles) {
2825
+ const fullPath = join10(tempDir, expectedFile);
2826
+ const found = await pathExists(fullPath);
2827
+ fileResults.push({ path: expectedFile, found });
2828
+ }
2829
+ const missingFiles = fileResults.filter((r) => !r.found);
2830
+ const snapshotDir = join10(templatePath, "_tests", fixture.name);
2831
+ let snapshotResults = [];
2832
+ if (options.updateSnapshots) {
2833
+ await updateSnapshots(tempDir, snapshotDir);
2834
+ } else if (await pathExists(snapshotDir)) {
2835
+ snapshotResults = await compareSnapshots(tempDir, snapshotDir);
2836
+ }
2837
+ const snapshotFailures = snapshotResults.filter((r) => r.status !== "match");
2838
+ const passed = missingFiles.length === 0 && snapshotFailures.length === 0;
2839
+ return {
2840
+ name: fixture.name,
2841
+ passed,
2842
+ fileResults,
2843
+ snapshotResults
2844
+ };
2845
+ } catch (error) {
2846
+ return {
2847
+ name: fixture.name,
2848
+ passed: false,
2849
+ fileResults: [],
2850
+ snapshotResults: [],
2851
+ error: error instanceof Error ? error.message : String(error)
2852
+ };
2853
+ } finally {
2854
+ await rm3(tempDir, { recursive: true, force: true }).catch(() => {
2855
+ });
2856
+ }
2857
+ }
2858
+ async function runAll(fixtures, templatePath, options, presetDefinitions = {}) {
2859
+ const results = [];
2860
+ for (const fixture of fixtures) {
2861
+ const result = await runFixture(fixture, templatePath, options, presetDefinitions);
2862
+ results.push(result);
2863
+ }
2864
+ const passed = results.filter((r) => r.passed).length;
2865
+ const failed = results.filter((r) => !r.passed).length;
2866
+ return {
2867
+ results,
2868
+ total: results.length,
2869
+ passed,
2870
+ failed
2871
+ };
2872
+ }
2873
+
2874
+ // src/commands/test.ts
2875
+ async function testCommand(options) {
2876
+ try {
2877
+ intro4("awa CLI - Template Test");
2878
+ const fileConfig = await configLoader.load(options.config ?? null);
2879
+ const templateSource = options.template ?? fileConfig?.template ?? null;
2880
+ const template = await templateResolver.resolve(templateSource, false);
2881
+ const fixtures = await discoverFixtures(template.localPath);
2882
+ if (fixtures.length === 0) {
2883
+ logger.warn("No test fixtures found in _tests/ directory");
2884
+ outro4("No tests to run.");
2885
+ return 0;
2886
+ }
2887
+ logger.info(`Found ${fixtures.length} fixture(s)`);
2888
+ const presetDefinitions = fileConfig?.presets ?? {};
2889
+ const result = await runAll(
2890
+ fixtures,
2891
+ template.localPath,
2892
+ { updateSnapshots: options.updateSnapshots },
2893
+ presetDefinitions
2894
+ );
2895
+ report2(result);
2896
+ if (result.failed > 0) {
2897
+ outro4(`${result.failed} fixture(s) failed.`);
2898
+ return 1;
2899
+ }
2900
+ outro4("All tests passed!");
2901
+ return 0;
2902
+ } catch (error) {
2903
+ if (error instanceof Error) {
2904
+ logger.error(error.message);
2905
+ } else {
2906
+ logger.error(String(error));
2907
+ }
2908
+ return 2;
2909
+ }
2910
+ }
2911
+
2912
+ // src/utils/update-check.ts
2913
+ import chalk5 from "chalk";
2914
+ function compareSemver(a, b) {
2915
+ const partsA = a.split(".").map((s) => Number.parseInt(s, 10));
2916
+ const partsB = b.split(".").map((s) => Number.parseInt(s, 10));
2917
+ for (let i = 0; i < 3; i++) {
2918
+ const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);
2919
+ if (diff !== 0) return diff;
2920
+ }
2921
+ return 0;
2922
+ }
2923
+ function isMajorVersionBump(current, latest) {
2924
+ const currentMajor = Number.parseInt(current.split(".")[0] ?? "0", 10);
2925
+ const latestMajor = Number.parseInt(latest.split(".")[0] ?? "0", 10);
2926
+ return latestMajor > currentMajor;
2927
+ }
2928
+ async function checkForUpdate() {
2929
+ try {
2930
+ const response = await fetch("https://registry.npmjs.org/@ncoderz/awa/latest", {
2931
+ signal: AbortSignal.timeout(5e3)
2932
+ });
2933
+ if (!response.ok) return null;
2934
+ const data = await response.json();
2935
+ const latest = data.version;
2936
+ if (!latest || typeof latest !== "string") return null;
2937
+ const current = PACKAGE_INFO.version;
2938
+ const isOutdated = compareSemver(current, latest) < 0;
2939
+ return {
2940
+ current,
2941
+ latest,
2942
+ isOutdated,
2943
+ isMajorBump: isOutdated && isMajorVersionBump(current, latest)
2944
+ };
2945
+ } catch {
2946
+ return null;
2947
+ }
2948
+ }
2949
+ function printUpdateWarning(log, result) {
2950
+ if (!result.isOutdated) return;
2951
+ console.log("");
2952
+ if (result.isMajorBump) {
2953
+ log.warn(
2954
+ chalk5.yellow(
2955
+ `New major version available: ${result.current} \u2192 ${result.latest} (breaking changes)`
2956
+ )
2957
+ );
2958
+ log.warn(chalk5.dim(" See https://github.com/ncoderz/awa/releases for details"));
2959
+ } else {
2960
+ log.warn(chalk5.yellow(`Update available: ${result.current} \u2192 ${result.latest}`));
2961
+ }
2962
+ log.warn(chalk5.dim(" Run `npm install -g @ncoderz/awa` to update"));
2963
+ console.log("");
2964
+ }
2965
+
2966
+ // src/utils/update-check-cache.ts
2967
+ import { mkdir as mkdir3, readFile as readFile6, writeFile } from "fs/promises";
2968
+ import { homedir } from "os";
2969
+ import { dirname, join as join11 } from "path";
2970
+ var CACHE_DIR = join11(homedir(), ".cache", "awa");
2971
+ var CACHE_FILE = join11(CACHE_DIR, "update-check.json");
2972
+ var DEFAULT_INTERVAL_MS = 864e5;
2973
+ async function shouldCheck(intervalMs = DEFAULT_INTERVAL_MS) {
2974
+ try {
2975
+ const raw = await readFile6(CACHE_FILE, "utf-8");
2976
+ const data = JSON.parse(raw);
2977
+ if (typeof data.timestamp !== "number" || typeof data.latestVersion !== "string") {
2978
+ return true;
2979
+ }
2980
+ return Date.now() - data.timestamp >= intervalMs;
2981
+ } catch {
2982
+ return true;
2983
+ }
2984
+ }
2985
+ async function writeCache(latestVersion) {
2986
+ try {
2987
+ await mkdir3(dirname(CACHE_FILE), { recursive: true });
2988
+ const data = {
2989
+ timestamp: Date.now(),
2990
+ latestVersion
2991
+ };
2992
+ await writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
2993
+ } catch {
2994
+ }
2995
+ }
2996
+
1308
2997
  // src/cli/index.ts
1309
2998
  var version = PACKAGE_INFO.version;
1310
2999
  var program = new Command();
1311
3000
  program.name("awa").description("awa - tool for generating AI coding agent configuration files").version(version, "-v, --version", "Display version number");
1312
- program.command("generate").description("Generate AI agent configuration files from templates").argument("[output]", "Output directory (optional if specified in config)").option("-t, --template <source>", "Template source (local path or Git repository)").option("-f, --features <flag...>", "Feature flags (can be specified multiple times)").option("--preset <name...>", "Preset names to enable (can be specified multiple times)").option(
3001
+ program.command("generate").alias("init").description("Generate AI agent configuration files from templates").argument("[output]", "Output directory (optional if specified in config)").option("-t, --template <source>", "Template source (local path or Git repository)").option("-f, --features <flag...>", "Feature flags (can be specified multiple times)").option("--preset <name...>", "Preset names to enable (can be specified multiple times)").option(
1313
3002
  "--remove-features <flag...>",
1314
3003
  "Feature flags to remove (can be specified multiple times)"
1315
3004
  ).option("--force", "Force overwrite existing files without prompting", false).option("--dry-run", "Preview changes without modifying files", false).option(
1316
3005
  "--delete",
1317
3006
  "Enable deletion of files listed in the delete list (default: warn only)",
1318
3007
  false
1319
- ).option("-c, --config <path>", "Path to configuration file").option("--refresh", "Force refresh of cached Git templates", false).action(async (output, options) => {
3008
+ ).option("-c, --config <path>", "Path to configuration file").option("--refresh", "Force refresh of cached Git templates", false).option("--all", "Process all named targets from config", false).option("--target <name>", "Process a specific named target from config").option("--overlay <path...>", "Overlay directory paths applied over base template (repeatable)").option("--json", "Output results as JSON (implies --dry-run)", false).option("--summary", "Output compact one-line summary", false).action(async (output, options) => {
1320
3009
  const cliOptions = {
1321
3010
  output,
1322
3011
  template: options.template,
1323
- features: options.features || [],
1324
- preset: options.preset || [],
1325
- removeFeatures: options.removeFeatures || [],
3012
+ features: options.features,
3013
+ preset: options.preset,
3014
+ removeFeatures: options.removeFeatures,
1326
3015
  force: options.force,
1327
3016
  dryRun: options.dryRun,
1328
3017
  delete: options.delete,
1329
3018
  config: options.config,
1330
- refresh: options.refresh
3019
+ refresh: options.refresh,
3020
+ all: options.all,
3021
+ target: options.target,
3022
+ overlay: options.overlay || [],
3023
+ json: options.json,
3024
+ summary: options.summary
1331
3025
  };
1332
3026
  await generateCommand(cliOptions);
1333
3027
  });
1334
3028
  program.command("diff").description("Compare template output with existing target directory").argument("[target]", "Target directory to compare against (optional if specified in config)").option("-t, --template <source>", "Template source (local path or Git repository)").option("-f, --features <flag...>", "Feature flags (can be specified multiple times)").option("--preset <name...>", "Preset names to enable (can be specified multiple times)").option(
1335
3029
  "--remove-features <flag...>",
1336
3030
  "Feature flags to remove (can be specified multiple times)"
1337
- ).option("-c, --config <path>", "Path to configuration file").option("--refresh", "Force refresh of cached Git templates", false).option("--list-unknown", "Include target-only files in diff results", false).action(async (target, options) => {
3031
+ ).option("-c, --config <path>", "Path to configuration file").option("--refresh", "Force refresh of cached Git templates", false).option("--list-unknown", "Include target-only files in diff results", false).option("--all", "Process all named targets from config", false).option("--target <name>", "Process a specific named target from config").option("-w, --watch", "Watch template directory for changes and re-run diff", false).option("--overlay <path...>", "Overlay directory paths applied over base template (repeatable)").option("--json", "Output results as JSON", false).option("--summary", "Output compact one-line summary", false).action(async (target, options) => {
1338
3032
  const cliOptions = {
1339
3033
  output: target,
1340
3034
  // Use target as output for consistency
1341
3035
  template: options.template,
1342
- features: options.features || [],
1343
- preset: options.preset || [],
1344
- removeFeatures: options.removeFeatures || [],
3036
+ features: options.features,
3037
+ preset: options.preset,
3038
+ removeFeatures: options.removeFeatures,
1345
3039
  config: options.config,
1346
3040
  refresh: options.refresh,
1347
- listUnknown: options.listUnknown
3041
+ listUnknown: options.listUnknown,
3042
+ all: options.all,
3043
+ target: options.target,
3044
+ watch: options.watch,
3045
+ overlay: options.overlay || [],
3046
+ json: options.json,
3047
+ summary: options.summary
1348
3048
  };
1349
3049
  const exitCode = await diffCommand(cliOptions);
1350
3050
  process.exit(exitCode);
1351
3051
  });
1352
- program.parse();
3052
+ program.command("check").description(
3053
+ "Validate spec files against schemas and check traceability between code markers and specs"
3054
+ ).option("-c, --config <path>", "Path to configuration file").option("--spec-ignore <pattern...>", "Glob patterns to exclude from spec file scanning").option("--code-ignore <pattern...>", "Glob patterns to exclude from code file scanning").option("--format <format>", "Output format (text or json)", "text").option(
3055
+ "--allow-warnings",
3056
+ "Allow warnings without failing (default: warnings are errors)",
3057
+ false
3058
+ ).option(
3059
+ "--spec-only",
3060
+ "Run only spec-level checks (schema and cross-refs); skip code-to-spec traceability",
3061
+ false
3062
+ ).action(async (options) => {
3063
+ const cliOptions = {
3064
+ config: options.config,
3065
+ specIgnore: options.specIgnore,
3066
+ codeIgnore: options.codeIgnore,
3067
+ format: options.format,
3068
+ allowWarnings: options.allowWarnings,
3069
+ specOnly: options.specOnly
3070
+ };
3071
+ const exitCode = await checkCommand(cliOptions);
3072
+ process.exit(exitCode);
3073
+ });
3074
+ program.command("features").description("Discover feature flags available in a template").option("-t, --template <source>", "Template source (local path or Git repository)").option("-c, --config <path>", "Path to configuration file").option("--refresh", "Force refresh of cached Git templates", false).option("--json", "Output results as JSON", false).action(async (options) => {
3075
+ const exitCode = await featuresCommand({
3076
+ template: options.template,
3077
+ config: options.config,
3078
+ refresh: options.refresh,
3079
+ json: options.json
3080
+ });
3081
+ process.exit(exitCode);
3082
+ });
3083
+ program.command("test").description("Run template test fixtures to verify expected output").option("-t, --template <source>", "Template source (local path or Git repository)").option("-c, --config <path>", "Path to configuration file").option("--update-snapshots", "Update stored snapshots with current rendered output", false).action(async (options) => {
3084
+ const testOptions = {
3085
+ template: options.template,
3086
+ config: options.config,
3087
+ updateSnapshots: options.updateSnapshots
3088
+ };
3089
+ const exitCode = await testCommand(testOptions);
3090
+ process.exit(exitCode);
3091
+ });
3092
+ var updateCheckPromise = null;
3093
+ var isJsonOrSummary = process.argv.includes("--json") || process.argv.includes("--summary");
3094
+ var isTTY = process.stdout.isTTY === true;
3095
+ var isDisabledByEnv = !!process.env.NO_UPDATE_NOTIFIER;
3096
+ if (!isJsonOrSummary && isTTY && !isDisabledByEnv) {
3097
+ updateCheckPromise = (async () => {
3098
+ try {
3099
+ const { configLoader: configLoader2 } = await import("./config-2TOQATI3.js");
3100
+ const configPath = process.argv.indexOf("-c") !== -1 ? process.argv[process.argv.indexOf("-c") + 1] : process.argv.indexOf("--config") !== -1 ? process.argv[process.argv.indexOf("--config") + 1] : void 0;
3101
+ const fileConfig = await configLoader2.load(configPath ?? null);
3102
+ const updateCheckConfig = fileConfig?.["update-check"];
3103
+ if (updateCheckConfig?.enabled === false) return null;
3104
+ const intervalSeconds = updateCheckConfig?.interval ?? 86400;
3105
+ const intervalMs = intervalSeconds * 1e3;
3106
+ const needsCheck = await shouldCheck(intervalMs);
3107
+ if (!needsCheck) return null;
3108
+ const result = await checkForUpdate();
3109
+ if (result) {
3110
+ await writeCache(result.latest);
3111
+ }
3112
+ return result;
3113
+ } catch {
3114
+ return null;
3115
+ }
3116
+ })();
3117
+ }
3118
+ program.hook("postAction", async () => {
3119
+ if (!updateCheckPromise) return;
3120
+ try {
3121
+ const result = await updateCheckPromise;
3122
+ if (result?.isOutdated) {
3123
+ printUpdateWarning(logger, result);
3124
+ }
3125
+ } catch {
3126
+ }
3127
+ });
3128
+ program.parseAsync();
1353
3129
  //# sourceMappingURL=index.js.map