speckeeper 0.1.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.
package/dist/cli.js ADDED
@@ -0,0 +1,1110 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk4 from 'chalk';
4
+ import { dirname, join, relative, resolve, extname, basename } from 'path';
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
6
+ import { parse } from 'yaml';
7
+ import 'crypto';
8
+ import 'glob';
9
+ import { z } from 'zod';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ var defaultConfig = {
13
+ srcDir: "src",
14
+ designDir: "design",
15
+ // Directory for requirements/design TS models
16
+ docsDir: "docs",
17
+ specsDir: "specs"
18
+ };
19
+ var CONFIG_FILES = [
20
+ "speckeeper.config.yaml",
21
+ "speckeeper.config.yml",
22
+ "speckeeper.config.json",
23
+ "speckeeper.config.ts",
24
+ "speckeeper.config.js"
25
+ ];
26
+ function findConfigFile(startDir = process.cwd()) {
27
+ let currentDir = startDir;
28
+ while (currentDir !== dirname(currentDir)) {
29
+ for (const configFile of CONFIG_FILES) {
30
+ const configPath = resolve(currentDir, configFile);
31
+ if (existsSync(configPath)) {
32
+ return configPath;
33
+ }
34
+ }
35
+ currentDir = dirname(currentDir);
36
+ }
37
+ return null;
38
+ }
39
+ async function loadConfig(configPath, cwd = process.cwd()) {
40
+ const resolvedPath = configPath ?? findConfigFile(cwd);
41
+ if (!resolvedPath) {
42
+ return { ...defaultConfig };
43
+ }
44
+ const ext = resolvedPath.split(".").pop()?.toLowerCase();
45
+ try {
46
+ if (ext === "yaml" || ext === "yml") {
47
+ const content = readFileSync(resolvedPath, "utf-8");
48
+ const parsed = parse(content);
49
+ return { ...defaultConfig, ...parsed };
50
+ }
51
+ if (ext === "json") {
52
+ const content = readFileSync(resolvedPath, "utf-8");
53
+ const parsed = JSON.parse(content);
54
+ return { ...defaultConfig, ...parsed };
55
+ }
56
+ if (ext === "ts" || ext === "js") {
57
+ const module = await import(resolvedPath);
58
+ const parsed = module.default ?? module;
59
+ return { ...defaultConfig, ...parsed };
60
+ }
61
+ return { ...defaultConfig };
62
+ } catch (error) {
63
+ console.error(`Failed to load config from ${resolvedPath}:`, error);
64
+ return { ...defaultConfig };
65
+ }
66
+ }
67
+ function ensureDir(dirPath) {
68
+ if (!existsSync(dirPath)) {
69
+ mkdirSync(dirPath, { recursive: true });
70
+ }
71
+ }
72
+ function writeFile(filePath, content) {
73
+ const dir = dirname(filePath);
74
+ ensureDir(dir);
75
+ const existed = existsSync(filePath);
76
+ let unchanged = false;
77
+ if (existed) {
78
+ const existingContent = readFileSync(filePath, "utf-8");
79
+ unchanged = existingContent === content;
80
+ }
81
+ if (!unchanged) {
82
+ writeFileSync(filePath, content, "utf-8");
83
+ }
84
+ return {
85
+ path: filePath,
86
+ created: !existed,
87
+ updated: existed && !unchanged,
88
+ unchanged
89
+ };
90
+ }
91
+ function batchWriteFiles(files) {
92
+ const results = [];
93
+ for (const { path, content } of files) {
94
+ results.push(writeFile(path, content));
95
+ }
96
+ return {
97
+ results,
98
+ created: results.filter((r) => r.created).length,
99
+ updated: results.filter((r) => r.updated).length,
100
+ unchanged: results.filter((r) => r.unchanged).length,
101
+ total: results.length
102
+ };
103
+ }
104
+ var RELATION_TYPES = [
105
+ "dependsOn",
106
+ "uses",
107
+ "includes",
108
+ "implements",
109
+ "refines",
110
+ "verifies",
111
+ "satisfies",
112
+ "traces",
113
+ "relatedTo"
114
+ ];
115
+ var RelationSchema = z.object({
116
+ /** Relation type */
117
+ type: z.enum(RELATION_TYPES),
118
+ /** Target ID */
119
+ target: z.string(),
120
+ /** Description (optional) */
121
+ description: z.string().optional()
122
+ });
123
+ z.array(RelationSchema).optional();
124
+
125
+ // src/core/model.ts
126
+ var modelRegistry = /* @__PURE__ */ new Map();
127
+ function registerModel(model) {
128
+ modelRegistry.set(model.id, model);
129
+ }
130
+ function getAllModels() {
131
+ return Array.from(modelRegistry.values());
132
+ }
133
+
134
+ // src/cli/build.ts
135
+ async function buildCommand(options) {
136
+ console.log(chalk4.blue("speckeeper build"));
137
+ console.log("");
138
+ const cwd = process.cwd();
139
+ const config = await loadConfig(options.config);
140
+ console.log(chalk4.gray(` Design: ${config.designDir || "design"}/`));
141
+ console.log(chalk4.gray(` Docs: ${config.docsDir}/`));
142
+ console.log(chalk4.gray(` Specs: ${config.specsDir}/`));
143
+ console.log("");
144
+ try {
145
+ ensureDir(join(cwd, config.docsDir));
146
+ ensureDir(join(cwd, config.specsDir));
147
+ const files = [];
148
+ const models = getAllModels();
149
+ if (models.length === 0) {
150
+ console.log(chalk4.yellow(" No models registered. Run model registration first."));
151
+ return;
152
+ }
153
+ console.log(chalk4.blue(` Processing ${models.length} model types...`));
154
+ for (const model of models) {
155
+ const exporters = model.getExporters();
156
+ if (exporters.length === 0) {
157
+ if (options.verbose) {
158
+ console.log(chalk4.gray(` ${model.name}: no exporters defined`));
159
+ }
160
+ continue;
161
+ }
162
+ const specs = [];
163
+ if (specs.length === 0) {
164
+ if (options.verbose) {
165
+ console.log(chalk4.gray(` ${model.name}: no specs found`));
166
+ }
167
+ continue;
168
+ }
169
+ for (const exporter of exporters) {
170
+ const outputDir = exporter.outputDir ? join(cwd, config.docsDir, exporter.outputDir) : join(cwd, config.docsDir);
171
+ ensureDir(outputDir);
172
+ if (exporter.single) {
173
+ for (const spec of specs) {
174
+ const content = exporter.single(spec);
175
+ const filename = model.getFilename(spec, exporter.format) || spec.id;
176
+ files.push({
177
+ path: join(outputDir, `${filename}.md`),
178
+ content
179
+ });
180
+ }
181
+ }
182
+ if (exporter.index) {
183
+ const indexContent = exporter.index(specs);
184
+ files.push({
185
+ path: join(outputDir, "index.md"),
186
+ content: indexContent
187
+ });
188
+ }
189
+ }
190
+ console.log(chalk4.green(` \u2713 ${model.name}`));
191
+ }
192
+ if (files.length > 0) {
193
+ console.log("");
194
+ console.log(chalk4.blue(` Writing ${files.length} files...`));
195
+ const result = await batchWriteFiles(files);
196
+ console.log("");
197
+ console.log(chalk4.green(` \u2713 Build complete`));
198
+ console.log(chalk4.gray(` Created: ${result.created}`));
199
+ console.log(chalk4.gray(` Updated: ${result.updated}`));
200
+ console.log(chalk4.gray(` Unchanged: ${result.unchanged}`));
201
+ } else {
202
+ console.log(chalk4.yellow(" No files to generate."));
203
+ }
204
+ } catch (error) {
205
+ console.error(chalk4.red("Build failed:"), error);
206
+ process.exit(1);
207
+ }
208
+ }
209
+ function createEmptyRegistry() {
210
+ return {
211
+ requirements: /* @__PURE__ */ new Map(),
212
+ useCases: /* @__PURE__ */ new Map(),
213
+ actors: /* @__PURE__ */ new Map(),
214
+ components: /* @__PURE__ */ new Map(),
215
+ entities: /* @__PURE__ */ new Map(),
216
+ relations: /* @__PURE__ */ new Map(),
217
+ rules: /* @__PURE__ */ new Map(),
218
+ screens: /* @__PURE__ */ new Map(),
219
+ transitions: /* @__PURE__ */ new Map(),
220
+ forms: /* @__PURE__ */ new Map(),
221
+ processFlows: /* @__PURE__ */ new Map(),
222
+ glossaryTerms: /* @__PURE__ */ new Map(),
223
+ apiRefs: /* @__PURE__ */ new Map(),
224
+ tableRefs: /* @__PURE__ */ new Map(),
225
+ testRefs: /* @__PURE__ */ new Map(),
226
+ layers: /* @__PURE__ */ new Map(),
227
+ boundaries: /* @__PURE__ */ new Map(),
228
+ artifacts: /* @__PURE__ */ new Map()
229
+ };
230
+ }
231
+ var globalRegistry = createEmptyRegistry();
232
+ function getGlobalRegistry() {
233
+ return globalRegistry;
234
+ }
235
+ function resetGlobalRegistry() {
236
+ globalRegistry = createEmptyRegistry();
237
+ }
238
+ function isModelInstance(value) {
239
+ if (!value || typeof value !== "object") return false;
240
+ const obj = value;
241
+ return typeof obj.id === "string" && typeof obj.name === "string" && typeof obj.idPrefix === "string" && obj.schema !== void 0 && typeof obj.validate === "function" && typeof obj.lint === "function";
242
+ }
243
+ function registerExportedModels(module) {
244
+ for (const [exportName, exportValue] of Object.entries(module)) {
245
+ if (!exportValue || typeof exportValue !== "object") continue;
246
+ if (isModelInstance(exportValue)) {
247
+ registerModel(exportValue);
248
+ continue;
249
+ }
250
+ if (Array.isArray(exportValue)) {
251
+ registerArrayItems(exportName, exportValue);
252
+ continue;
253
+ }
254
+ const value = exportValue;
255
+ if (value.components && value.layers && value.boundaries && value.relations) {
256
+ const arch = value;
257
+ for (const comp of arch.components) {
258
+ if (comp.id) globalRegistry.components.set(comp.id, comp);
259
+ }
260
+ for (const layer of arch.layers) {
261
+ if (layer.id) globalRegistry.layers.set(layer.id, layer);
262
+ }
263
+ for (const boundary of arch.boundaries) {
264
+ if (boundary.id) globalRegistry.boundaries.set(boundary.id, boundary);
265
+ }
266
+ for (const rel of arch.relations) {
267
+ if (rel.id) globalRegistry.relations.set(rel.id, rel);
268
+ }
269
+ continue;
270
+ }
271
+ if (value.entities && value.relations && Array.isArray(value.entities)) {
272
+ const concept = value;
273
+ for (const entity of concept.entities) {
274
+ if (entity.id) globalRegistry.entities.set(entity.id, entity);
275
+ }
276
+ for (const rel of concept.relations) {
277
+ if (rel.id) globalRegistry.relations.set(rel.id, rel);
278
+ }
279
+ for (const rule of concept.rules || []) {
280
+ if (rule.id) globalRegistry.rules.set(rule.id, rule);
281
+ }
282
+ continue;
283
+ }
284
+ if (value.useCases && value.actors) {
285
+ const ucModel = value;
286
+ for (const uc of ucModel.useCases) {
287
+ if (uc.id) globalRegistry.useCases.set(uc.id, uc);
288
+ }
289
+ for (const actor of ucModel.actors) {
290
+ if (actor.id) globalRegistry.actors.set(actor.id, actor);
291
+ }
292
+ continue;
293
+ }
294
+ if (value.terms && Array.isArray(value.terms)) {
295
+ const glossary = value;
296
+ for (const term of glossary.terms) {
297
+ if (term.id) globalRegistry.glossaryTerms.set(term.id, term);
298
+ }
299
+ continue;
300
+ }
301
+ if (value.id && value.title && (value.kind || value.type || value.acceptance)) {
302
+ globalRegistry.requirements.set(value.id, value);
303
+ continue;
304
+ }
305
+ if (value.id && value.name && value.attributes && Array.isArray(value.attributes)) {
306
+ globalRegistry.entities.set(value.id, value);
307
+ continue;
308
+ }
309
+ if (value.id && value.name && value.actor && value.mainFlow) {
310
+ globalRegistry.useCases.set(value.id, value);
311
+ continue;
312
+ }
313
+ if (value.id && value.name && value.type && ["human", "system", "external"].includes(value.type)) {
314
+ globalRegistry.actors.set(value.id, value);
315
+ continue;
316
+ }
317
+ if (value.id && (value.term || value.abbreviation) && value.definition) {
318
+ globalRegistry.glossaryTerms.set(value.id, value);
319
+ continue;
320
+ }
321
+ }
322
+ }
323
+ function registerArrayItems(exportName, items) {
324
+ for (const item of items) {
325
+ if (!item || typeof item !== "object") continue;
326
+ const obj = item;
327
+ if (!obj.id || typeof obj.id !== "string") continue;
328
+ if (exportName.toLowerCase().includes("requirement") || exportName.toLowerCase().includes("constraints") || obj.type && obj.acceptanceCriteria) {
329
+ globalRegistry.requirements.set(obj.id, item);
330
+ continue;
331
+ }
332
+ if (exportName.toLowerCase().includes("usecase") || obj.actor && obj.mainFlow) {
333
+ globalRegistry.useCases.set(obj.id, item);
334
+ continue;
335
+ }
336
+ if (exportName.toLowerCase().includes("actor") || obj.type && ["human", "system", "external"].includes(obj.type) && !obj.mainFlow) {
337
+ globalRegistry.actors.set(obj.id, item);
338
+ continue;
339
+ }
340
+ if (exportName.toLowerCase().includes("component") || exportName.toLowerCase().includes("container") || obj.type && ["person", "system", "container", "component"].includes(obj.type)) {
341
+ globalRegistry.components.set(obj.id, item);
342
+ continue;
343
+ }
344
+ if (exportName.toLowerCase().includes("entit") || obj.name && obj.attributes && Array.isArray(obj.attributes)) {
345
+ globalRegistry.entities.set(obj.id, item);
346
+ continue;
347
+ }
348
+ if (exportName.toLowerCase().includes("term") || exportName.toLowerCase().includes("acronym") || exportName.toLowerCase().includes("glossary") || obj.term && obj.definition) {
349
+ globalRegistry.glossaryTerms.set(obj.id, item);
350
+ continue;
351
+ }
352
+ if (exportName.toLowerCase().includes("relation") || obj.from && obj.to && obj.label) {
353
+ globalRegistry.relations.set(obj.id, item);
354
+ continue;
355
+ }
356
+ if (exportName.toLowerCase().includes("layer") || obj.name && typeof obj.order === "number") {
357
+ globalRegistry.layers.set(obj.id, item);
358
+ continue;
359
+ }
360
+ if (exportName.toLowerCase().includes("boundar") || obj.type && ["system", "context", "container", "deployment"].includes(obj.type) && obj.description) {
361
+ globalRegistry.boundaries.set(obj.id, item);
362
+ continue;
363
+ }
364
+ if (exportName.toLowerCase().includes("screen") || obj.type && ["page", "modal", "drawer", "panel", "wizard"].includes(obj.type) && obj.auth) {
365
+ globalRegistry.screens.set(obj.id, item);
366
+ continue;
367
+ }
368
+ if (exportName.toLowerCase().includes("testref") || exportName.toLowerCase().includes("test-ref") || obj.source && obj.verifiesRequirements && Array.isArray(obj.verifiesRequirements)) {
369
+ globalRegistry.testRefs.set(obj.id, item);
370
+ continue;
371
+ }
372
+ if (exportName.toLowerCase().includes("artifact") || obj.category && obj.location && obj.purpose && ["ssot", "human-readable", "machine-readable", "implementation"].includes(obj.category)) {
373
+ globalRegistry.artifacts.set(obj.id, item);
374
+ continue;
375
+ }
376
+ }
377
+ }
378
+ async function loadModelsFromDirectory(dirPath, options = {}) {
379
+ const loadedFiles = [];
380
+ if (!existsSync(dirPath)) {
381
+ return loadedFiles;
382
+ }
383
+ const entries = readdirSync(dirPath);
384
+ for (const entry of entries) {
385
+ const fullPath = join(dirPath, entry);
386
+ const stat = statSync(fullPath);
387
+ if (stat.isDirectory() && options.recursive) {
388
+ const subFiles = await loadModelsFromDirectory(fullPath, options);
389
+ loadedFiles.push(...subFiles);
390
+ } else if (stat.isFile()) {
391
+ const ext = extname(entry);
392
+ if ([".ts", ".js", ".mts", ".mjs"].includes(ext) && !entry.endsWith(".d.ts")) {
393
+ if (basename(entry, ext) === "index") {
394
+ continue;
395
+ }
396
+ try {
397
+ const module = await import(fullPath);
398
+ loadedFiles.push(fullPath);
399
+ registerExportedModels(module);
400
+ } catch (error) {
401
+ console.error(`Failed to load model from ${fullPath}:`, error);
402
+ }
403
+ }
404
+ }
405
+ }
406
+ return loadedFiles;
407
+ }
408
+ async function loadAllModels(config, cwd = process.cwd()) {
409
+ resetGlobalRegistry();
410
+ const loadedFiles = [];
411
+ const errors = [];
412
+ const designDir = resolve(cwd, config.designDir || "design");
413
+ const modelDirs = [
414
+ { path: join(designDir, "requirements") },
415
+ { path: join(designDir, "usecases") },
416
+ { path: join(designDir, "architecture") },
417
+ { path: join(designDir, "data-model") },
418
+ { path: join(designDir, "screens") },
419
+ { path: join(designDir, "flows") },
420
+ { path: join(designDir, "glossary") },
421
+ // Also load files directly under design/ by default (index.ts, etc.)
422
+ { path: designDir }
423
+ ];
424
+ for (const { path: dirPath } of modelDirs) {
425
+ if (!existsSync(dirPath)) {
426
+ continue;
427
+ }
428
+ try {
429
+ const files = await loadModelsFromDirectory(dirPath, { recursive: true });
430
+ loadedFiles.push(...files);
431
+ } catch (error) {
432
+ errors.push({ file: dirPath, error });
433
+ }
434
+ }
435
+ return {
436
+ registry: getGlobalRegistry(),
437
+ loadedFiles,
438
+ errors
439
+ };
440
+ }
441
+
442
+ // src/cli/lint.ts
443
+ async function lintCommand(options) {
444
+ console.log(chalk4.blue("speckeeper lint"));
445
+ console.log("");
446
+ const cwd = process.cwd();
447
+ const config = await loadConfig(options.config);
448
+ console.log(chalk4.gray(` Design: ${config.designDir || "design"}/`));
449
+ if (options.phase) {
450
+ console.log(chalk4.gray(` Phase: ${options.phase}`));
451
+ }
452
+ console.log("");
453
+ try {
454
+ console.log(chalk4.blue(" Loading models..."));
455
+ const { registry, loadedFiles, errors: loadErrors } = await loadAllModels(config, cwd);
456
+ if (loadErrors.length > 0) {
457
+ console.log(chalk4.yellow(` \u26A0 ${loadErrors.length} file(s) failed to load`));
458
+ for (const { file, error } of loadErrors) {
459
+ console.log(chalk4.yellow(` - ${relative(cwd, file)}: ${error}`));
460
+ }
461
+ }
462
+ console.log(chalk4.gray(` Loaded: ${loadedFiles.length} files`));
463
+ console.log("");
464
+ console.log(chalk4.blue(" Running lint checks..."));
465
+ const result = runModelLint(registry, options);
466
+ console.log("");
467
+ outputLintResults(result, options);
468
+ if (result.errors > 0) {
469
+ process.exit(1);
470
+ }
471
+ } catch (error) {
472
+ console.error(chalk4.red("Lint failed:"), error);
473
+ process.exit(1);
474
+ }
475
+ }
476
+ function runModelLint(registry, options) {
477
+ const issues = [];
478
+ const models = getAllModels();
479
+ for (const model of models) {
480
+ const specs = getSpecsFromRegistry(registry, model.id);
481
+ if (specs.length === 0) continue;
482
+ const lintResults = model.lintAll(specs);
483
+ for (const result of lintResults) {
484
+ issues.push({
485
+ rule: result.ruleId,
486
+ severity: result.severity,
487
+ message: result.message,
488
+ specId: result.specId,
489
+ modelType: model.name
490
+ });
491
+ }
492
+ }
493
+ const errors = issues.filter((i) => i.severity === "error").length;
494
+ const warnings = issues.filter((i) => i.severity === "warning").length;
495
+ const infos = issues.filter((i) => i.severity === "info").length;
496
+ if (!options.strict) {
497
+ return {
498
+ issues: issues.filter((i) => i.severity !== "info"),
499
+ errors,
500
+ warnings,
501
+ infos
502
+ };
503
+ }
504
+ return { issues, errors, warnings, infos };
505
+ }
506
+ function getSpecsFromRegistry(registry, modelId) {
507
+ const registryMap = {
508
+ "requirement": "requirements",
509
+ "usecase": "useCases",
510
+ "actor": "actors",
511
+ "term": "glossaryTerms",
512
+ "entity": "entities",
513
+ "screen": "screens",
514
+ "process-flow": "processFlows",
515
+ "component": "components",
516
+ "boundary": "boundaries",
517
+ "layer": "layers",
518
+ "relation": "relations"
519
+ };
520
+ const key = registryMap[modelId];
521
+ if (!key || !registry[key]) return [];
522
+ return Array.from(registry[key].values());
523
+ }
524
+ function outputLintResults(result, options) {
525
+ if (options.format === "json") {
526
+ console.log(JSON.stringify(result, null, 2));
527
+ return;
528
+ }
529
+ if (options.format === "github") {
530
+ for (const issue of result.issues) {
531
+ const level = issue.severity === "error" ? "error" : issue.severity === "warning" ? "warning" : "notice";
532
+ console.log(`::${level}::${issue.message} [${issue.rule}]${issue.specId ? ` (${issue.specId})` : ""}`);
533
+ }
534
+ return;
535
+ }
536
+ if (result.issues.length === 0) {
537
+ console.log(chalk4.green(" \u2713 No issues found"));
538
+ return;
539
+ }
540
+ for (const issue of result.issues) {
541
+ const icon = issue.severity === "error" ? "\u2717" : issue.severity === "warning" ? "\u26A0" : "\u2139";
542
+ const color = issue.severity === "error" ? chalk4.red : issue.severity === "warning" ? chalk4.yellow : chalk4.blue;
543
+ const prefix = issue.specId ? `[${issue.specId}] ` : "";
544
+ console.log(color(` ${icon} ${prefix}${issue.message}`));
545
+ console.log(chalk4.gray(` Rule: ${issue.rule}`));
546
+ }
547
+ console.log("");
548
+ console.log(chalk4.gray(` Summary: ${result.errors} errors, ${result.warnings} warnings, ${result.infos} infos`));
549
+ }
550
+ async function driftCommand(options) {
551
+ console.log(chalk4.blue("speckeeper drift"));
552
+ console.log("");
553
+ const cwd = process.cwd();
554
+ const config = await loadConfig(options.config);
555
+ console.log(chalk4.gray(` Design: ${config.designDir || "design"}/`));
556
+ console.log(chalk4.gray(` Docs: ${config.docsDir}/`));
557
+ console.log("");
558
+ try {
559
+ console.log(chalk4.blue(" Loading models..."));
560
+ const { registry } = await loadAllModels(config, cwd);
561
+ const results = [];
562
+ const models = getAllModels();
563
+ for (const model of models) {
564
+ const specs = getSpecsFromRegistry2(registry, model.id);
565
+ if (specs.length === 0) continue;
566
+ for (const exporter of model.getExporters()) {
567
+ if (exporter.format !== "markdown") continue;
568
+ const outputDir = exporter.outputDir ? join(cwd, config.docsDir, exporter.outputDir) : join(cwd, config.docsDir);
569
+ if (exporter.single) {
570
+ for (const spec of specs) {
571
+ const filename = model.getFilename(spec, "markdown") || spec.id;
572
+ const filePath = join(outputDir, `${filename}.md`);
573
+ if (!existsSync(filePath)) {
574
+ results.push({ file: filePath, status: "missing" });
575
+ continue;
576
+ }
577
+ const expected = exporter.single(spec);
578
+ const actual = readFileSync(filePath, "utf-8");
579
+ if (normalizeContent(expected) !== normalizeContent(actual)) {
580
+ results.push({ file: filePath, status: "drifted" });
581
+ } else {
582
+ results.push({ file: filePath, status: "ok" });
583
+ }
584
+ }
585
+ }
586
+ if (exporter.index) {
587
+ const indexPath = join(outputDir, "index.md");
588
+ if (!existsSync(indexPath)) {
589
+ results.push({ file: indexPath, status: "missing" });
590
+ } else {
591
+ const expected = exporter.index(specs);
592
+ const actual = readFileSync(indexPath, "utf-8");
593
+ if (normalizeContent(expected) !== normalizeContent(actual)) {
594
+ results.push({ file: indexPath, status: "drifted" });
595
+ } else {
596
+ results.push({ file: indexPath, status: "ok" });
597
+ }
598
+ }
599
+ }
600
+ }
601
+ }
602
+ outputDriftResults(results, options);
603
+ const hasDrift = results.some((r) => r.status === "drifted" || r.status === "missing");
604
+ if (hasDrift && options.failOnDrift) {
605
+ process.exit(1);
606
+ }
607
+ } catch (error) {
608
+ console.error(chalk4.red("Drift check failed:"), error);
609
+ process.exit(1);
610
+ }
611
+ }
612
+ function getSpecsFromRegistry2(registry, modelId) {
613
+ const registryMap = {
614
+ "requirement": "requirements",
615
+ "usecase": "useCases",
616
+ "actor": "actors",
617
+ "term": "glossaryTerms",
618
+ "entity": "entities",
619
+ "screen": "screens",
620
+ "process-flow": "processFlows",
621
+ "component": "components"
622
+ };
623
+ const key = registryMap[modelId];
624
+ if (!key || !registry[key]) return [];
625
+ return Array.from(registry[key].values());
626
+ }
627
+ function normalizeContent(content) {
628
+ return content.trim().replace(/\r\n/g, "\n");
629
+ }
630
+ function outputDriftResults(results, _options) {
631
+ console.log("");
632
+ const ok = results.filter((r) => r.status === "ok");
633
+ const drifted = results.filter((r) => r.status === "drifted");
634
+ const missing = results.filter((r) => r.status === "missing");
635
+ if (drifted.length === 0 && missing.length === 0) {
636
+ console.log(chalk4.green(" \u2713 No drift detected"));
637
+ console.log(chalk4.gray(` Checked: ${results.length} files`));
638
+ return;
639
+ }
640
+ if (drifted.length > 0) {
641
+ console.log(chalk4.yellow(` \u26A0 ${drifted.length} file(s) have drifted:`));
642
+ for (const result of drifted) {
643
+ console.log(chalk4.yellow(` - ${result.file}`));
644
+ }
645
+ }
646
+ if (missing.length > 0) {
647
+ console.log(chalk4.red(` \u2717 ${missing.length} file(s) are missing:`));
648
+ for (const result of missing) {
649
+ console.log(chalk4.red(` - ${result.file}`));
650
+ }
651
+ }
652
+ console.log("");
653
+ console.log(chalk4.gray(` Summary: ${ok.length} ok, ${drifted.length} drifted, ${missing.length} missing`));
654
+ }
655
+ async function checkCommand(type, options) {
656
+ console.log(chalk4.blue("speckeeper check"));
657
+ console.log("");
658
+ const cwd = process.cwd();
659
+ const config = await loadConfig(options.config);
660
+ const checkType = type || options.type || "all";
661
+ console.log(chalk4.gray(` Design: ${config.designDir || "design"}/`));
662
+ console.log(chalk4.gray(` Type: ${checkType}`));
663
+ console.log("");
664
+ try {
665
+ console.log(chalk4.blue(" Loading models..."));
666
+ const { registry } = await loadAllModels(config, cwd);
667
+ const results = [];
668
+ const models = getAllModels();
669
+ if (options.verbose) {
670
+ console.log(chalk4.gray(` Registered models: ${models.map((m) => m.id).join(", ")}`));
671
+ }
672
+ for (const model of models) {
673
+ const specs = getSpecsFromRegistry3(registry, model.id);
674
+ if (specs.length === 0) continue;
675
+ for (const spec of specs) {
676
+ const sourcePath = model.getExternalSourcePath(spec);
677
+ if (!sourcePath) continue;
678
+ const fullPath = join(cwd, sourcePath);
679
+ if (!existsSync(fullPath)) {
680
+ results.push({
681
+ type: model.id,
682
+ success: false,
683
+ issues: [{
684
+ severity: "error",
685
+ message: `External source not found: ${sourcePath}`,
686
+ specId: spec.id
687
+ }]
688
+ });
689
+ continue;
690
+ }
691
+ const externalData = loadExternalData(fullPath);
692
+ const checkResult = model.check(spec, externalData);
693
+ if (!checkResult.success || checkResult.errors.length > 0 || checkResult.warnings.length > 0) {
694
+ results.push({
695
+ type: model.id,
696
+ success: checkResult.success,
697
+ issues: [
698
+ ...checkResult.errors.map((e) => ({ severity: "error", message: e.message, specId: e.specId, field: e.field })),
699
+ ...checkResult.warnings.map((w) => ({ severity: "warning", message: w.message, specId: w.specId, field: w.field }))
700
+ ]
701
+ });
702
+ }
703
+ }
704
+ }
705
+ outputCheckResults(results);
706
+ if (options.coverage) {
707
+ console.log("");
708
+ console.log(chalk4.blue(" Coverage checks..."));
709
+ const coverageResults = runAllCoverageChecks(registry);
710
+ if (coverageResults.length > 0) {
711
+ outputAllCoverageResults(coverageResults);
712
+ for (const check of coverageResults) {
713
+ if (check.result.uncoveredItems.length > 0) {
714
+ results.push({
715
+ type: `coverage-${check.modelId}`,
716
+ success: check.result.coveragePercent >= 80,
717
+ // fail if below 80%
718
+ issues: check.result.uncoveredItems.slice(0, 10).map((item) => ({
719
+ severity: "warning",
720
+ message: `[${check.modelName}\u2192${check.targetModel}] '${item.id}'${item.sourceId ? ` (${item.sourceId})` : ""} not covered`,
721
+ specId: item.id
722
+ }))
723
+ });
724
+ }
725
+ }
726
+ console.log("");
727
+ console.log(chalk4.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
728
+ const allPassed = coverageResults.every((c) => c.result.coveragePercent >= 80);
729
+ if (allPassed) {
730
+ console.log(chalk4.green(" \u2713 All coverage checks passed (\u226580%)"));
731
+ } else {
732
+ const failed = coverageResults.filter((c) => c.result.coveragePercent < 80);
733
+ console.log(chalk4.yellow(` \u26A0 ${failed.length} coverage check(s) below 80%`));
734
+ }
735
+ } else {
736
+ console.log(chalk4.gray(" No coverage checker found"));
737
+ }
738
+ }
739
+ const hasErrors = results.some((r) => !r.success);
740
+ if (hasErrors) {
741
+ process.exit(1);
742
+ }
743
+ } catch (error) {
744
+ console.error(chalk4.red("Check failed:"), error);
745
+ process.exit(1);
746
+ }
747
+ }
748
+ function getSpecsFromRegistry3(registry, modelId) {
749
+ const registryMap = {
750
+ "requirement": "requirements",
751
+ "usecase": "useCases",
752
+ "actor": "actors",
753
+ "term": "glossaryTerms",
754
+ "entity": "entities",
755
+ "screen": "screens",
756
+ "process-flow": "processFlows",
757
+ "component": "components",
758
+ "api-ref": "apiRefs",
759
+ "table-ref": "tableRefs",
760
+ "test-ref": "testRefs",
761
+ "artifact": "artifacts"
762
+ };
763
+ const key = registryMap[modelId];
764
+ if (!key || !registry[key]) return [];
765
+ return Array.from(registry[key].values());
766
+ }
767
+ function loadExternalData(filePath) {
768
+ const content = readFileSync(filePath, "utf-8");
769
+ if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
770
+ return parse(content);
771
+ }
772
+ if (filePath.endsWith(".json")) {
773
+ return JSON.parse(content);
774
+ }
775
+ return content;
776
+ }
777
+ function outputCheckResults(results) {
778
+ console.log("");
779
+ if (results.length === 0) {
780
+ console.log(chalk4.green(" \u2713 All checks passed"));
781
+ return;
782
+ }
783
+ let totalErrors = 0;
784
+ let totalWarnings = 0;
785
+ for (const result of results) {
786
+ for (const issue of result.issues) {
787
+ const icon = issue.severity === "error" ? "\u2717" : "\u26A0";
788
+ const color = issue.severity === "error" ? chalk4.red : chalk4.yellow;
789
+ if (issue.severity === "error") totalErrors++;
790
+ else totalWarnings++;
791
+ const prefix = issue.specId ? `[${issue.specId}] ` : "";
792
+ console.log(color(` ${icon} ${prefix}${issue.message}`));
793
+ }
794
+ }
795
+ console.log("");
796
+ console.log(chalk4.gray(` Summary: ${totalErrors} errors, ${totalWarnings} warnings`));
797
+ }
798
+ function runAllCoverageChecks(registry) {
799
+ const models = getAllModels();
800
+ const results = [];
801
+ const registryRecord = {};
802
+ for (const [key, value] of Object.entries(registry)) {
803
+ if (value instanceof Map) {
804
+ registryRecord[key] = value;
805
+ }
806
+ }
807
+ for (const model of models) {
808
+ const checker = model.getCoverageChecker();
809
+ if (checker) {
810
+ const specs = getSpecsFromRegistry3(registry, model.id);
811
+ const result = model.checkCoverage(specs, registryRecord);
812
+ if (result) {
813
+ results.push({
814
+ modelId: model.id,
815
+ modelName: model.name,
816
+ description: checker.description,
817
+ targetModel: checker.targetModel,
818
+ result
819
+ });
820
+ }
821
+ }
822
+ }
823
+ return results;
824
+ }
825
+ function outputAllCoverageResults(checks) {
826
+ for (const check of checks) {
827
+ console.log("");
828
+ console.log(chalk4.blue(` Coverage: ${check.modelName} \u2192 ${check.targetModel}`));
829
+ console.log(chalk4.gray(` ${check.description}`));
830
+ console.log(chalk4.gray(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
831
+ console.log(chalk4.gray(` Total: ${check.result.total}`));
832
+ console.log(chalk4.green(` Covered: ${check.result.covered}`));
833
+ console.log(chalk4.yellow(` Uncovered: ${check.result.uncovered}`));
834
+ const color = check.result.coveragePercent >= 80 ? chalk4.green : check.result.coveragePercent >= 50 ? chalk4.yellow : chalk4.red;
835
+ console.log(color(` Coverage: ${check.result.coveragePercent}%`));
836
+ if (check.result.uncoveredItems.length > 0 && check.result.uncoveredItems.length <= 10) {
837
+ console.log("");
838
+ console.log(chalk4.yellow(" Uncovered items:"));
839
+ for (const item of check.result.uncoveredItems) {
840
+ const desc = item.description ? `: ${item.description.substring(0, 40)}` : "";
841
+ const source = item.sourceId ? ` (${item.sourceId})` : "";
842
+ console.log(chalk4.yellow(` - ${item.id}${source}${desc}`));
843
+ }
844
+ } else if (check.result.uncoveredItems.length > 10) {
845
+ console.log("");
846
+ console.log(chalk4.yellow(` (${check.result.uncoveredItems.length} uncovered items - showing first 5)`));
847
+ for (const item of check.result.uncoveredItems.slice(0, 5)) {
848
+ const desc = item.description ? `: ${item.description.substring(0, 40)}` : "";
849
+ console.log(chalk4.yellow(` - ${item.id}${desc}`));
850
+ }
851
+ }
852
+ }
853
+ }
854
+ async function newCommand(options) {
855
+ console.log(chalk4.blue("speckeeper new"));
856
+ console.log("");
857
+ const cwd = process.cwd();
858
+ const config = await loadConfig(options.config);
859
+ if (!options.type) {
860
+ console.log(chalk4.gray(" Available model types:"));
861
+ const models2 = getAllModels();
862
+ for (const model2 of models2) {
863
+ console.log(chalk4.gray(` - ${model2.id} (${model2.name})`));
864
+ }
865
+ console.log("");
866
+ console.log(chalk4.gray(" Usage: speckeeper new --type <type> [--id <id>]"));
867
+ return;
868
+ }
869
+ const models = getAllModels();
870
+ const model = models.find((m) => m.id === options.type);
871
+ if (!model) {
872
+ console.error(chalk4.red(` Error: Unknown model type '${options.type}'`));
873
+ console.log(chalk4.gray(" Run `speckeeper new` to see available types"));
874
+ process.exit(1);
875
+ }
876
+ const id = options.id || `${model.idPrefix}-${String(Date.now()).slice(-6)}`;
877
+ const template = generateTemplate(model, id);
878
+ if (options.dryRun) {
879
+ console.log(chalk4.gray(" Template (dry run):"));
880
+ console.log("");
881
+ console.log(template);
882
+ return;
883
+ }
884
+ const outputDir = options.output || join(cwd, config.designDir || "design");
885
+ const outputFile = join(outputDir, `${model.id}-${id}.ts`);
886
+ if (existsSync(outputFile)) {
887
+ console.error(chalk4.red(` Error: File already exists: ${outputFile}`));
888
+ process.exit(1);
889
+ }
890
+ writeFileSync(outputFile, template);
891
+ console.log(chalk4.green(` \u2713 Created: ${outputFile}`));
892
+ }
893
+ function generateTemplate(model, id) {
894
+ const typeName = model.name;
895
+ const lines = [];
896
+ lines.push(`/**`);
897
+ lines.push(` * ${typeName}: ${id}`);
898
+ lines.push(` * `);
899
+ lines.push(` * Generated by speckeeper new`);
900
+ lines.push(` */`);
901
+ lines.push(`import type { ${typeName} } from './_models/${model.id}';`);
902
+ lines.push("");
903
+ lines.push(`export const ${camelCase(id)}: ${typeName} = {`);
904
+ lines.push(` id: '${id}',`);
905
+ lines.push(` // TODO: Fill in the required fields`);
906
+ lines.push(`};`);
907
+ lines.push("");
908
+ return lines.join("\n");
909
+ }
910
+ function camelCase(str) {
911
+ return str.toLowerCase().replace(/[-_](.)/g, (_, c) => c.toUpperCase());
912
+ }
913
+ async function impactCommand(targetId, options) {
914
+ console.log(chalk4.blue("speckeeper impact"));
915
+ console.log("");
916
+ if (!targetId) {
917
+ console.error(chalk4.red("Error: ID is required"));
918
+ console.log(chalk4.gray(" Usage: speckeeper impact <id>"));
919
+ process.exit(1);
920
+ }
921
+ const cwd = process.cwd();
922
+ const config = await loadConfig(options.config);
923
+ const maxDepth = options.depth ? parseInt(options.depth, 10) : 3;
924
+ console.log(chalk4.gray(` Target: ${targetId}`));
925
+ console.log(chalk4.gray(` Depth: ${maxDepth}`));
926
+ console.log("");
927
+ try {
928
+ console.log(chalk4.blue(" Loading models..."));
929
+ const { registry } = await loadAllModels(config, cwd);
930
+ const target = findById(registry, targetId);
931
+ if (!target) {
932
+ console.error(chalk4.red(` Error: Target '${targetId}' not found`));
933
+ process.exit(1);
934
+ }
935
+ console.log(chalk4.blue(" Analyzing impact..."));
936
+ const result = analyzeImpact(registry, targetId, target.type, maxDepth);
937
+ outputImpactResults(result, options);
938
+ } catch (error) {
939
+ console.error(chalk4.red("Impact analysis failed:"), error);
940
+ process.exit(1);
941
+ }
942
+ }
943
+ function findById(registry, id) {
944
+ for (const [type, map] of Object.entries(registry)) {
945
+ if (map instanceof Map && map.has(id)) {
946
+ return { type, data: map.get(id) };
947
+ }
948
+ }
949
+ return null;
950
+ }
951
+ function analyzeImpact(registry, targetId, targetType, maxDepth) {
952
+ const impactedNodes = [];
953
+ const visited = /* @__PURE__ */ new Set([targetId]);
954
+ function findReferences(id, depth) {
955
+ if (depth > maxDepth) return;
956
+ for (const [type, map] of Object.entries(registry)) {
957
+ if (!(map instanceof Map)) continue;
958
+ for (const [itemId, item] of map) {
959
+ if (visited.has(itemId)) continue;
960
+ const itemStr = JSON.stringify(item);
961
+ if (itemStr.includes(`"${id}"`)) {
962
+ visited.add(itemId);
963
+ impactedNodes.push({
964
+ id: itemId,
965
+ type,
966
+ depth,
967
+ impactType: depth === 1 ? "direct" : "indirect"
968
+ });
969
+ findReferences(itemId, depth + 1);
970
+ }
971
+ }
972
+ }
973
+ }
974
+ findReferences(targetId, 1);
975
+ return {
976
+ target: targetId,
977
+ targetType,
978
+ impactedNodes
979
+ };
980
+ }
981
+ function outputImpactResults(result, options) {
982
+ console.log("");
983
+ if (options.format === "json") {
984
+ console.log(JSON.stringify(result, null, 2));
985
+ return;
986
+ }
987
+ if (options.format === "mermaid") {
988
+ console.log("```mermaid");
989
+ console.log("graph TD");
990
+ console.log(` ${result.target}[${result.target}]`);
991
+ for (const node of result.impactedNodes) {
992
+ const style = node.impactType === "direct" ? "-->|direct|" : "-.->|indirect|";
993
+ console.log(` ${result.target} ${style} ${node.id}[${node.id}]`);
994
+ }
995
+ console.log("```");
996
+ return;
997
+ }
998
+ if (result.impactedNodes.length === 0) {
999
+ console.log(chalk4.green(" No impacted elements found"));
1000
+ return;
1001
+ }
1002
+ const direct = result.impactedNodes.filter((n) => n.impactType === "direct");
1003
+ const indirect = result.impactedNodes.filter((n) => n.impactType === "indirect");
1004
+ if (direct.length > 0) {
1005
+ console.log(chalk4.yellow(` Direct impact (${direct.length}):`));
1006
+ for (const node of direct) {
1007
+ console.log(chalk4.yellow(` \u2192 ${node.id} (${node.type})`));
1008
+ }
1009
+ }
1010
+ if (indirect.length > 0) {
1011
+ console.log(chalk4.gray(` Indirect impact (${indirect.length}):`));
1012
+ for (const node of indirect) {
1013
+ console.log(chalk4.gray(` \u2933 ${node.id} (${node.type}, depth: ${node.depth})`));
1014
+ }
1015
+ }
1016
+ console.log("");
1017
+ console.log(chalk4.gray(` Total: ${result.impactedNodes.length} impacted elements`));
1018
+ }
1019
+ var __filename$1 = fileURLToPath(import.meta.url);
1020
+ var __dirname$1 = dirname(__filename$1);
1021
+ async function runInit(options = {}) {
1022
+ const cwd = process.cwd();
1023
+ console.log(chalk4.cyan("speckeeper init"));
1024
+ console.log("");
1025
+ const designDir = join(cwd, "design");
1026
+ const configFile = join(cwd, "speckeeper.config.ts");
1027
+ if (!options.force && (existsSync(designDir) || existsSync(configFile))) {
1028
+ console.log(chalk4.yellow(" Project already initialized."));
1029
+ console.log(chalk4.gray(" Use --force to overwrite existing files."));
1030
+ return;
1031
+ }
1032
+ const projectName = cwd.split("/").pop() || "my-project";
1033
+ const templatesDir = join(__dirname$1, "templates", "init");
1034
+ if (!existsSync(templatesDir)) {
1035
+ console.log(chalk4.red(` Error: Templates directory not found at ${templatesDir}`));
1036
+ console.log(chalk4.gray(" Please ensure the package is properly installed."));
1037
+ return;
1038
+ }
1039
+ let packageJsonCreated = false;
1040
+ copyTemplateDir(templatesDir, cwd, projectName, options.force || false, (relativePath, created) => {
1041
+ if (created) {
1042
+ console.log(chalk4.green(` Created: ${relativePath}`));
1043
+ if (relativePath === "package.json") {
1044
+ packageJsonCreated = true;
1045
+ }
1046
+ } else {
1047
+ console.log(chalk4.yellow(` Skipped: ${relativePath} (already exists)`));
1048
+ }
1049
+ });
1050
+ console.log("");
1051
+ console.log(chalk4.cyan(" Project initialized successfully!"));
1052
+ console.log("");
1053
+ console.log(" Next steps:");
1054
+ if (packageJsonCreated) {
1055
+ console.log(chalk4.gray(" 1. Run `npm install` to install dependencies"));
1056
+ console.log(chalk4.gray(" 2. Edit design/_models/ to customize your models"));
1057
+ console.log(chalk4.gray(" 3. Add specifications in design/"));
1058
+ console.log(chalk4.gray(" 4. Run `npx speckeeper lint` to validate"));
1059
+ } else {
1060
+ console.log(chalk4.gray(" 1. Add speckeeper and zod to your package.json dependencies"));
1061
+ console.log(chalk4.gray(' 2. Ensure "type": "module" is set in package.json'));
1062
+ console.log(chalk4.gray(" 3. Edit design/_models/ to customize your models"));
1063
+ console.log(chalk4.gray(" 4. Add specifications in design/"));
1064
+ console.log(chalk4.gray(" 5. Run `npx speckeeper lint` to validate"));
1065
+ }
1066
+ }
1067
+ function copyTemplateDir(srcDir, destDir, projectName, force, onFile, relativePath = "") {
1068
+ const entries = readdirSync(srcDir);
1069
+ for (const entry of entries) {
1070
+ const srcPath = join(srcDir, entry);
1071
+ const stat = statSync(srcPath);
1072
+ const destEntry = entry.endsWith(".template") ? entry.slice(0, -9) : entry;
1073
+ const destPath = join(destDir, destEntry);
1074
+ const relPath = relativePath ? join(relativePath, destEntry) : destEntry;
1075
+ if (stat.isDirectory()) {
1076
+ if (!existsSync(destPath)) {
1077
+ mkdirSync(destPath, { recursive: true });
1078
+ onFile(relPath + "/", true);
1079
+ }
1080
+ copyTemplateDir(srcPath, destPath, projectName, force, onFile, relPath);
1081
+ } else {
1082
+ if (!force && existsSync(destPath)) {
1083
+ onFile(relPath, false);
1084
+ continue;
1085
+ }
1086
+ let content = readFileSync(srcPath, "utf-8");
1087
+ content = content.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
1088
+ const dir = dirname(destPath);
1089
+ if (!existsSync(dir)) {
1090
+ mkdirSync(dir, { recursive: true });
1091
+ }
1092
+ writeFileSync(destPath, content);
1093
+ onFile(relPath, true);
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ // src/cli/index.ts
1099
+ var program = new Command();
1100
+ program.name("speckeeper").description("Requirements and design management framework with TypeScript DSL").version("0.1.0");
1101
+ program.command("build").description("Generate docs/ and specs/ from TypeScript models").option("-c, --config <path>", "Path to config file").option("-o, --output <path>", "Base output directory path", ".").option("-f, --format <format>", "Output format: markdown, json, both", "both").option("-w, --watch", "Watch for changes and auto-regenerate").option("-v, --verbose", "Show detailed output").action(buildCommand);
1102
+ program.command("lint").description("Check design integrity (ID duplicates, references, layer violations, etc.)").option("-c, --config <path>", "Path to config file").option("-p, --phase <phase>", "Phase gate to check against (REQ, HLD, LLD, OPS)").option("-s, --strict", "Treat warnings as errors").option("--fix", "Attempt to fix auto-fixable issues").option("-f, --format <format>", "Output format: text, json, github", "text").action(lintCommand);
1103
+ program.command("drift").description("Check if generated files have been manually edited").option("-c, --config <path>", "Path to config file").option("-u, --update", "Auto-update if differences are found").option("-f, --format <format>", "Output format: text, json, diff", "text").action(driftCommand);
1104
+ program.command("check").description("Check external SSOT conformance (including custom models)").argument("[type]", "Type of check: external-ssot, openapi, ddl, iac, custom, all, test").option("-c, --config <path>", "Path to config file").option("--strict", "Treat warnings as errors").option("-v, --verbose", "Show detailed output").option("--coverage", "Check if all testable acceptance criteria are covered by TestRefs").action(checkCommand);
1105
+ program.command("new").description("Create a new element with auto-generated ID").argument("<type>", "Type: requirement, usecase, entity, component, screen, flow, error-case, term").option("-k, --kind <kind>", "Sub-kind (e.g., functional, non-functional for requirements)").option("-n, --name <name>", "Name of the element").option("-o, --output <path>", "Output directory path").option("-t, --template <path>", "Path to template file").action(newCommand);
1106
+ program.command("impact").description("Analyze impact of changes to an ID").argument("<id>", "ID to analyze (e.g., REQ-001, ENT-ORDER)").option("-c, --config <path>", "Path to config file").option("-d, --depth <depth>", "Analysis depth (reference tracking level)", "3").option("--direction <direction>", "Analysis direction: upstream, downstream, both", "both").option("-f, --format <format>", "Output format: text, json, mermaid", "text").action(impactCommand);
1107
+ program.command("init").description("Initialize a new speckeeper project with starter templates").option("-f, --force", "Overwrite existing files").action(runInit);
1108
+ program.parse();
1109
+ //# sourceMappingURL=cli.js.map
1110
+ //# sourceMappingURL=cli.js.map