speckeeper 0.2.0 → 0.2.1
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 +36 -139
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +25 -62
- package/dist/index.js +35 -129
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import chalk4 from 'chalk';
|
|
4
|
-
import { dirname,
|
|
4
|
+
import { dirname, join, resolve } from 'path';
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
6
6
|
import { parse } from 'yaml';
|
|
7
7
|
import 'crypto';
|
|
@@ -127,9 +127,6 @@ var specStore = /* @__PURE__ */ new Map();
|
|
|
127
127
|
function getSpecStore() {
|
|
128
128
|
return specStore;
|
|
129
129
|
}
|
|
130
|
-
function resetSpecStore() {
|
|
131
|
-
specStore.clear();
|
|
132
|
-
}
|
|
133
130
|
var modelRegistry = /* @__PURE__ */ new Map();
|
|
134
131
|
function registerModel(model) {
|
|
135
132
|
modelRegistry.set(model.id, model);
|
|
@@ -137,103 +134,24 @@ function registerModel(model) {
|
|
|
137
134
|
function getAllModels() {
|
|
138
135
|
return Array.from(modelRegistry.values());
|
|
139
136
|
}
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!value || typeof value !== "object") return false;
|
|
145
|
-
const obj = value;
|
|
146
|
-
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";
|
|
147
|
-
}
|
|
148
|
-
function registerExportedModelInstances(module) {
|
|
149
|
-
for (const [, exportValue] of Object.entries(module)) {
|
|
150
|
-
if (!exportValue || typeof exportValue !== "object") continue;
|
|
151
|
-
if (isModelInstance(exportValue)) {
|
|
152
|
-
registerModel(exportValue);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
async function loadModelsFromDirectory(dirPath, options = {}) {
|
|
157
|
-
const loadedFiles = [];
|
|
158
|
-
if (!existsSync(dirPath)) {
|
|
159
|
-
return loadedFiles;
|
|
160
|
-
}
|
|
161
|
-
const entries = readdirSync(dirPath);
|
|
162
|
-
for (const entry of entries) {
|
|
163
|
-
const fullPath = join(dirPath, entry);
|
|
164
|
-
const stat = statSync(fullPath);
|
|
165
|
-
if (stat.isDirectory() && options.recursive) {
|
|
166
|
-
const subFiles = await loadModelsFromDirectory(fullPath, options);
|
|
167
|
-
loadedFiles.push(...subFiles);
|
|
168
|
-
} else if (stat.isFile()) {
|
|
169
|
-
const ext = extname(entry);
|
|
170
|
-
if ([".ts", ".js", ".mts", ".mjs"].includes(ext) && !entry.endsWith(".d.ts")) {
|
|
171
|
-
if (basename(entry, ext) === "index") {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
try {
|
|
175
|
-
const module = await import(fullPath);
|
|
176
|
-
loadedFiles.push(fullPath);
|
|
177
|
-
registerExportedModelInstances(module);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
console.error(`Failed to load from ${fullPath}:`, error);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return loadedFiles;
|
|
185
|
-
}
|
|
186
|
-
async function loadAllModels(config, cwd = process.cwd()) {
|
|
187
|
-
resetSpecStore();
|
|
188
|
-
clearModelRegistry();
|
|
189
|
-
const loadedFiles = [];
|
|
190
|
-
const errors = [];
|
|
191
|
-
if (config.models && Array.isArray(config.models)) {
|
|
192
|
-
for (const model of config.models) {
|
|
193
|
-
if (isModelInstance(model)) {
|
|
194
|
-
registerModel(model);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const designDir = resolve(cwd, config.designDir || "design");
|
|
199
|
-
const modelDirs = [
|
|
200
|
-
{ path: join(designDir, "_models") },
|
|
201
|
-
{ path: join(designDir, "requirements") },
|
|
202
|
-
{ path: join(designDir, "usecases") },
|
|
203
|
-
{ path: join(designDir, "architecture") },
|
|
204
|
-
{ path: join(designDir, "data-model") },
|
|
205
|
-
{ path: join(designDir, "screens") },
|
|
206
|
-
{ path: join(designDir, "flows") },
|
|
207
|
-
{ path: join(designDir, "glossary") },
|
|
208
|
-
{ path: designDir }
|
|
209
|
-
];
|
|
210
|
-
for (const { path: dirPath } of modelDirs) {
|
|
211
|
-
if (!existsSync(dirPath)) {
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
try {
|
|
215
|
-
const files = await loadModelsFromDirectory(dirPath, { recursive: true });
|
|
216
|
-
loadedFiles.push(...files);
|
|
217
|
-
} catch (error) {
|
|
218
|
-
errors.push({ file: dirPath, error });
|
|
137
|
+
function registerModelsFromConfig(models) {
|
|
138
|
+
for (const model of models) {
|
|
139
|
+
if (model && typeof model === "object" && "id" in model && "schema" in model) {
|
|
140
|
+
registerModel(model);
|
|
219
141
|
}
|
|
220
142
|
}
|
|
221
|
-
const store = getSpecStore();
|
|
222
|
-
const registry = {};
|
|
223
|
-
for (const [modelId, specMap] of store) {
|
|
224
|
-
registry[modelId] = specMap;
|
|
225
|
-
}
|
|
226
|
-
return {
|
|
227
|
-
registry,
|
|
228
|
-
loadedFiles,
|
|
229
|
-
errors
|
|
230
|
-
};
|
|
231
143
|
}
|
|
232
|
-
function
|
|
233
|
-
const map =
|
|
144
|
+
function getSpecs(modelId) {
|
|
145
|
+
const map = specStore.get(modelId);
|
|
234
146
|
if (!map) return [];
|
|
235
147
|
return Array.from(map.values());
|
|
236
148
|
}
|
|
149
|
+
function findModelTypeBySpecId(specId) {
|
|
150
|
+
for (const [modelId, map] of specStore) {
|
|
151
|
+
if (map.has(specId)) return modelId;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
237
155
|
|
|
238
156
|
// src/cli/build.ts
|
|
239
157
|
async function buildCommand(options) {
|
|
@@ -247,14 +165,7 @@ async function buildCommand(options) {
|
|
|
247
165
|
console.log("");
|
|
248
166
|
try {
|
|
249
167
|
console.log(chalk4.blue(" Loading models..."));
|
|
250
|
-
|
|
251
|
-
if (loadErrors.length > 0) {
|
|
252
|
-
console.log(chalk4.yellow(` \u26A0 ${loadErrors.length} file(s) failed to load`));
|
|
253
|
-
for (const { file, error } of loadErrors) {
|
|
254
|
-
console.log(chalk4.yellow(` - ${relative(cwd, file)}: ${error}`));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
console.log(chalk4.gray(` Loaded: ${loadedFiles.length} files`));
|
|
168
|
+
registerModelsFromConfig(config.models || []);
|
|
258
169
|
console.log("");
|
|
259
170
|
ensureDir(join(cwd, config.docsDir));
|
|
260
171
|
ensureDir(join(cwd, config.specsDir));
|
|
@@ -273,7 +184,7 @@ async function buildCommand(options) {
|
|
|
273
184
|
}
|
|
274
185
|
continue;
|
|
275
186
|
}
|
|
276
|
-
const specs =
|
|
187
|
+
const specs = getSpecs(model.id);
|
|
277
188
|
if (specs.length === 0) {
|
|
278
189
|
if (options.verbose) {
|
|
279
190
|
console.log(chalk4.gray(` ${model.name}: no specs found`));
|
|
@@ -323,7 +234,6 @@ async function buildCommand(options) {
|
|
|
323
234
|
async function lintCommand(options) {
|
|
324
235
|
console.log(chalk4.blue("speckeeper lint"));
|
|
325
236
|
console.log("");
|
|
326
|
-
const cwd = process.cwd();
|
|
327
237
|
const config = await loadConfig(options.config);
|
|
328
238
|
console.log(chalk4.gray(` Design: ${config.designDir || "design"}/`));
|
|
329
239
|
if (options.phase) {
|
|
@@ -332,17 +242,12 @@ async function lintCommand(options) {
|
|
|
332
242
|
console.log("");
|
|
333
243
|
try {
|
|
334
244
|
console.log(chalk4.blue(" Loading models..."));
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
for (const { file, error } of loadErrors) {
|
|
339
|
-
console.log(chalk4.yellow(` - ${relative(cwd, file)}: ${error}`));
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
console.log(chalk4.gray(` Loaded: ${loadedFiles.length} files`));
|
|
245
|
+
registerModelsFromConfig(config.models || []);
|
|
246
|
+
const models = getAllModels();
|
|
247
|
+
console.log(chalk4.gray(` Loaded: ${models.length} models`));
|
|
343
248
|
console.log("");
|
|
344
249
|
console.log(chalk4.blue(" Running lint checks..."));
|
|
345
|
-
const result = runModelLint(
|
|
250
|
+
const result = runModelLint(options);
|
|
346
251
|
console.log("");
|
|
347
252
|
outputLintResults(result, options);
|
|
348
253
|
if (result.errors > 0) {
|
|
@@ -353,11 +258,11 @@ async function lintCommand(options) {
|
|
|
353
258
|
process.exit(1);
|
|
354
259
|
}
|
|
355
260
|
}
|
|
356
|
-
function runModelLint(
|
|
261
|
+
function runModelLint(options) {
|
|
357
262
|
const issues = [];
|
|
358
263
|
const models = getAllModels();
|
|
359
264
|
for (const model of models) {
|
|
360
|
-
const specs =
|
|
265
|
+
const specs = getSpecs(model.id);
|
|
361
266
|
if (specs.length === 0) continue;
|
|
362
267
|
const lintResults = model.lintAll(specs);
|
|
363
268
|
for (const result of lintResults) {
|
|
@@ -419,11 +324,11 @@ async function driftCommand(options) {
|
|
|
419
324
|
console.log("");
|
|
420
325
|
try {
|
|
421
326
|
console.log(chalk4.blue(" Loading models..."));
|
|
422
|
-
|
|
327
|
+
registerModelsFromConfig(config.models || []);
|
|
423
328
|
const results = [];
|
|
424
329
|
const models = getAllModels();
|
|
425
330
|
for (const model of models) {
|
|
426
|
-
const specs =
|
|
331
|
+
const specs = getSpecs(model.id);
|
|
427
332
|
if (specs.length === 0) continue;
|
|
428
333
|
for (const exporter of model.getExporters()) {
|
|
429
334
|
if (exporter.format !== "markdown") continue;
|
|
@@ -510,14 +415,14 @@ async function checkCommand(type, options) {
|
|
|
510
415
|
console.log("");
|
|
511
416
|
try {
|
|
512
417
|
console.log(chalk4.blue(" Loading models..."));
|
|
513
|
-
|
|
418
|
+
registerModelsFromConfig(config.models || []);
|
|
514
419
|
const results = [];
|
|
515
420
|
const models = getAllModels();
|
|
516
421
|
if (options.verbose) {
|
|
517
422
|
console.log(chalk4.gray(` Registered models: ${models.map((m) => m.id).join(", ")}`));
|
|
518
423
|
}
|
|
519
424
|
for (const model of models) {
|
|
520
|
-
const specs =
|
|
425
|
+
const specs = getSpecs(model.id);
|
|
521
426
|
if (specs.length === 0) continue;
|
|
522
427
|
for (const spec of specs) {
|
|
523
428
|
const sourcePath = model.getExternalSourcePath(spec);
|
|
@@ -553,7 +458,7 @@ async function checkCommand(type, options) {
|
|
|
553
458
|
if (options.coverage) {
|
|
554
459
|
console.log("");
|
|
555
460
|
console.log(chalk4.blue(" Coverage checks..."));
|
|
556
|
-
const coverageResults = runAllCoverageChecks(
|
|
461
|
+
const coverageResults = runAllCoverageChecks();
|
|
557
462
|
if (coverageResults.length > 0) {
|
|
558
463
|
outputAllCoverageResults(coverageResults);
|
|
559
464
|
for (const check of coverageResults) {
|
|
@@ -623,13 +528,14 @@ function outputCheckResults(results) {
|
|
|
623
528
|
console.log("");
|
|
624
529
|
console.log(chalk4.gray(` Summary: ${totalErrors} errors, ${totalWarnings} warnings`));
|
|
625
530
|
}
|
|
626
|
-
function runAllCoverageChecks(
|
|
531
|
+
function runAllCoverageChecks() {
|
|
627
532
|
const models = getAllModels();
|
|
628
533
|
const results = [];
|
|
534
|
+
const registry = Object.fromEntries(getSpecStore());
|
|
629
535
|
for (const model of models) {
|
|
630
536
|
const checker = model.getCoverageChecker();
|
|
631
537
|
if (checker) {
|
|
632
|
-
const specs =
|
|
538
|
+
const specs = getSpecs(model.id);
|
|
633
539
|
const result = model.checkCoverage(specs, registry);
|
|
634
540
|
if (result) {
|
|
635
541
|
results.push({
|
|
@@ -740,7 +646,6 @@ async function impactCommand(targetId, options) {
|
|
|
740
646
|
console.log(chalk4.gray(" Usage: speckeeper impact <id>"));
|
|
741
647
|
process.exit(1);
|
|
742
648
|
}
|
|
743
|
-
const cwd = process.cwd();
|
|
744
649
|
const config = await loadConfig(options.config);
|
|
745
650
|
const maxDepth = options.depth ? parseInt(options.depth, 10) : 3;
|
|
746
651
|
console.log(chalk4.gray(` Target: ${targetId}`));
|
|
@@ -748,35 +653,27 @@ async function impactCommand(targetId, options) {
|
|
|
748
653
|
console.log("");
|
|
749
654
|
try {
|
|
750
655
|
console.log(chalk4.blue(" Loading models..."));
|
|
751
|
-
|
|
752
|
-
const
|
|
753
|
-
|
|
656
|
+
registerModelsFromConfig(config.models || []);
|
|
657
|
+
const store = getSpecStore();
|
|
658
|
+
const targetType = findModelTypeBySpecId(targetId);
|
|
659
|
+
if (!targetType) {
|
|
754
660
|
console.error(chalk4.red(` Error: Target '${targetId}' not found`));
|
|
755
661
|
process.exit(1);
|
|
756
662
|
}
|
|
757
663
|
console.log(chalk4.blue(" Analyzing impact..."));
|
|
758
|
-
const result = analyzeImpact(
|
|
664
|
+
const result = analyzeImpact(store, targetId, targetType, maxDepth);
|
|
759
665
|
outputImpactResults(result, options);
|
|
760
666
|
} catch (error) {
|
|
761
667
|
console.error(chalk4.red("Impact analysis failed:"), error);
|
|
762
668
|
process.exit(1);
|
|
763
669
|
}
|
|
764
670
|
}
|
|
765
|
-
function
|
|
766
|
-
for (const [type, map] of Object.entries(registry)) {
|
|
767
|
-
if (map instanceof Map && map.has(id)) {
|
|
768
|
-
return { type, data: map.get(id) };
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
return null;
|
|
772
|
-
}
|
|
773
|
-
function analyzeImpact(registry, targetId, targetType, maxDepth) {
|
|
671
|
+
function analyzeImpact(store, targetId, targetType, maxDepth) {
|
|
774
672
|
const impactedNodes = [];
|
|
775
673
|
const visited = /* @__PURE__ */ new Set([targetId]);
|
|
776
674
|
function findReferences(id, depth) {
|
|
777
675
|
if (depth > maxDepth) return;
|
|
778
|
-
for (const [type, map] of
|
|
779
|
-
if (!(map instanceof Map)) continue;
|
|
676
|
+
for (const [type, map] of store) {
|
|
780
677
|
for (const [itemId, item] of map) {
|
|
781
678
|
if (visited.has(itemId)) continue;
|
|
782
679
|
const itemStr = JSON.stringify(item);
|