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 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, relative, join, resolve, extname, basename } from 'path';
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 clearModelRegistry() {
141
- modelRegistry.clear();
142
- }
143
- function isModelInstance(value) {
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 getSpecsFromRegistry(registry, modelId) {
233
- const map = registry[modelId];
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
- const { registry, loadedFiles, errors: loadErrors } = await loadAllModels(config, cwd);
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 = getSpecsFromRegistry(registry, model.id);
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
- const { registry, loadedFiles, errors: loadErrors } = await loadAllModels(config, cwd);
336
- if (loadErrors.length > 0) {
337
- console.log(chalk4.yellow(` \u26A0 ${loadErrors.length} file(s) failed to load`));
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(registry, options);
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(registry, options) {
261
+ function runModelLint(options) {
357
262
  const issues = [];
358
263
  const models = getAllModels();
359
264
  for (const model of models) {
360
- const specs = getSpecsFromRegistry(registry, model.id);
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
- const { registry } = await loadAllModels(config, cwd);
327
+ registerModelsFromConfig(config.models || []);
423
328
  const results = [];
424
329
  const models = getAllModels();
425
330
  for (const model of models) {
426
- const specs = getSpecsFromRegistry(registry, model.id);
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
- const { registry } = await loadAllModels(config, cwd);
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 = getSpecsFromRegistry(registry, model.id);
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(registry);
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(registry) {
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 = getSpecsFromRegistry(registry, model.id);
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
- const { registry } = await loadAllModels(config, cwd);
752
- const target = findById(registry, targetId);
753
- if (!target) {
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(registry, targetId, target.type, maxDepth);
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 findById(registry, id) {
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 Object.entries(registry)) {
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);