@specmarket/cli 0.0.4 → 0.0.6

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 (39) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-MS2DYACY.js → chunk-OTXWWFAO.js} +42 -3
  3. package/dist/chunk-OTXWWFAO.js.map +1 -0
  4. package/dist/{config-R5KWZSJP.js → config-5JMI3YAR.js} +2 -2
  5. package/dist/index.js +1945 -252
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/commands/comment.test.ts +211 -0
  9. package/src/commands/comment.ts +176 -0
  10. package/src/commands/fork.test.ts +163 -0
  11. package/src/commands/info.test.ts +192 -0
  12. package/src/commands/info.ts +66 -2
  13. package/src/commands/init.test.ts +245 -0
  14. package/src/commands/init.ts +359 -25
  15. package/src/commands/issues.test.ts +382 -0
  16. package/src/commands/issues.ts +436 -0
  17. package/src/commands/login.test.ts +99 -0
  18. package/src/commands/login.ts +2 -6
  19. package/src/commands/logout.test.ts +54 -0
  20. package/src/commands/publish.test.ts +159 -0
  21. package/src/commands/publish.ts +1 -0
  22. package/src/commands/report.test.ts +181 -0
  23. package/src/commands/run.test.ts +419 -0
  24. package/src/commands/run.ts +71 -3
  25. package/src/commands/search.test.ts +147 -0
  26. package/src/commands/validate.test.ts +206 -2
  27. package/src/commands/validate.ts +315 -192
  28. package/src/commands/whoami.test.ts +106 -0
  29. package/src/index.ts +6 -0
  30. package/src/lib/convex-client.ts +6 -2
  31. package/src/lib/format-detection.test.ts +223 -0
  32. package/src/lib/format-detection.ts +172 -0
  33. package/src/lib/meta-instructions.test.ts +340 -0
  34. package/src/lib/meta-instructions.ts +562 -0
  35. package/src/lib/ralph-loop.test.ts +404 -0
  36. package/src/lib/ralph-loop.ts +501 -95
  37. package/src/lib/telemetry.ts +7 -1
  38. package/dist/chunk-MS2DYACY.js.map +0 -1
  39. /package/dist/{config-R5KWZSJP.js.map → config-5JMI3YAR.js.map} +0 -0
@@ -4,6 +4,18 @@ import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
5
  import { randomUUID } from 'crypto';
6
6
  import { validateSpec, detectCircularReferences } from './validate.js';
7
+ import { SIDECAR_FILENAME } from '@specmarket/shared';
8
+
9
+ const VALID_SPECMARKET_YAML = `spec_format: specmarket
10
+ display_name: "Test Spec"
11
+ description: "A valid test spec with enough description length to pass."
12
+ output_type: web-app
13
+ primary_stack: nextjs-typescript
14
+ tags: []
15
+ estimated_tokens: 50000
16
+ estimated_cost_usd: 2.50
17
+ estimated_time_minutes: 30
18
+ `;
7
19
 
8
20
  const VALID_SPEC_YAML = `name: test-spec
9
21
  display_name: "Test Spec"
@@ -11,7 +23,7 @@ description: "A valid test spec with enough description length to pass."
11
23
  output_type: web-app
12
24
  primary_stack: nextjs-typescript
13
25
  version: "1.0.0"
14
- runner: claude-code
26
+ runner: claude
15
27
  min_model: "claude-opus-4-5"
16
28
  estimated_tokens: 50000
17
29
  estimated_cost_usd: 2.50
@@ -51,6 +63,7 @@ describe('validateSpec', () => {
51
63
 
52
64
  async function writeValidSpec() {
53
65
  await Promise.all([
66
+ writeFile(join(tmpDir, SIDECAR_FILENAME), VALID_SPECMARKET_YAML),
54
67
  writeFile(join(tmpDir, 'spec.yaml'), VALID_SPEC_YAML),
55
68
  writeFile(join(tmpDir, 'PROMPT.md'), VALID_PROMPT_MD),
56
69
  writeFile(join(tmpDir, 'SPEC.md'), VALID_SPEC_MD),
@@ -84,6 +97,15 @@ describe('validateSpec', () => {
84
97
  expect(result.errors.some((e) => e.includes('PROMPT.md'))).toBe(true);
85
98
  });
86
99
 
100
+ it('reports error when specmarket.yaml is missing', async () => {
101
+ await writeValidSpec();
102
+ const { unlink } = await import('fs/promises');
103
+ await unlink(join(tmpDir, SIDECAR_FILENAME));
104
+ const result = await validateSpec(tmpDir);
105
+ expect(result.valid).toBe(false);
106
+ expect(result.errors.some((e) => e.includes(SIDECAR_FILENAME) && e.includes('required'))).toBe(true);
107
+ });
108
+
87
109
  it('reports error for missing SUCCESS_CRITERIA.md', async () => {
88
110
  await writeValidSpec();
89
111
  const { unlink } = await import('fs/promises');
@@ -105,7 +127,7 @@ describe('validateSpec', () => {
105
127
  await writeValidSpec();
106
128
  await writeFile(
107
129
  join(tmpDir, 'spec.yaml'),
108
- 'name: Invalid Name With Spaces\ndisplay_name: "Test"\ndescription: "short"\noutput_type: invalid\nprimary_stack: nextjs-typescript\nversion: "1.0.0"\nrunner: claude-code\nmin_model: "model"\nestimated_tokens: 100\nestimated_cost_usd: 0.01\nestimated_time_minutes: 1\n'
130
+ 'name: Invalid Name With Spaces\ndisplay_name: "Test"\ndescription: "short"\noutput_type: invalid\nprimary_stack: nextjs-typescript\nversion: "1.0.0"\nrunner: claude\nmin_model: "model"\nestimated_tokens: 100\nestimated_cost_usd: 0.01\nestimated_time_minutes: 1\n'
109
131
  );
110
132
  const result = await validateSpec(tmpDir);
111
133
  expect(result.valid).toBe(false);
@@ -276,3 +298,185 @@ describe('detectCircularReferences', () => {
276
298
  expect(cycles).toHaveLength(0);
277
299
  });
278
300
  });
301
+
302
+ describe('validateSpec format-aware', () => {
303
+ let tmpDir: string;
304
+
305
+ beforeEach(async () => {
306
+ tmpDir = join(tmpdir(), `spec-format-${randomUUID()}`);
307
+ await mkdir(tmpDir, { recursive: true });
308
+ });
309
+
310
+ afterEach(async () => {
311
+ await rm(tmpDir, { recursive: true, force: true });
312
+ });
313
+
314
+ it('reports format from sidecar for specmarket spec', async () => {
315
+ await mkdir(join(tmpDir, 'stdlib'), { recursive: true });
316
+ await writeFile(join(tmpDir, SIDECAR_FILENAME), VALID_SPECMARKET_YAML);
317
+ await writeFile(join(tmpDir, 'spec.yaml'), VALID_SPEC_YAML);
318
+ await writeFile(join(tmpDir, 'PROMPT.md'), VALID_PROMPT_MD);
319
+ await writeFile(join(tmpDir, 'SPEC.md'), VALID_SPEC_MD);
320
+ await writeFile(join(tmpDir, 'SUCCESS_CRITERIA.md'), VALID_SUCCESS_CRITERIA);
321
+ await writeFile(join(tmpDir, 'stdlib', 'STACK.md'), VALID_STACK_MD);
322
+ const result = await validateSpec(tmpDir);
323
+ expect(result.valid).toBe(true);
324
+ expect(result.format).toBe('specmarket');
325
+ expect(result.formatDetectedBy).toBe('sidecar');
326
+ });
327
+
328
+ const SIDECAR_SPECKIT = `spec_format: speckit
329
+ display_name: My Spec
330
+ description: A long enough description for the sidecar schema.
331
+ output_type: web-app
332
+ primary_stack: nextjs-typescript
333
+ `;
334
+
335
+ it('speckit dir validates successfully', async () => {
336
+ await writeFile(join(tmpDir, SIDECAR_FILENAME), SIDECAR_SPECKIT);
337
+ await writeFile(join(tmpDir, 'spec.md'), '# Spec\nContent here.');
338
+ await writeFile(join(tmpDir, 'tasks.md'), '# Tasks');
339
+ await mkdir(join(tmpDir, '.specify'), { recursive: true });
340
+ const result = await validateSpec(tmpDir);
341
+ expect(result.valid).toBe(true);
342
+ expect(result.format).toBe('speckit');
343
+ });
344
+
345
+ it('speckit missing tasks.md and plan.md returns error', async () => {
346
+ await writeFile(join(tmpDir, SIDECAR_FILENAME), SIDECAR_SPECKIT);
347
+ await writeFile(join(tmpDir, 'spec.md'), '# Spec');
348
+ const result = await validateSpec(tmpDir);
349
+ expect(result.valid).toBe(false);
350
+ expect(result.errors.some((e) => e.includes('tasks.md') || e.includes('plan.md'))).toBe(true);
351
+ });
352
+
353
+ it('bmad dir validates successfully', async () => {
354
+ await writeFile(
355
+ join(tmpDir, SIDECAR_FILENAME),
356
+ `spec_format: bmad
357
+ display_name: PRD Spec
358
+ description: A long enough description for the sidecar schema.
359
+ output_type: web-app
360
+ primary_stack: nextjs-typescript
361
+ `
362
+ );
363
+ await writeFile(join(tmpDir, 'prd.md'), '# PRD\nProduct requirements.');
364
+ await writeFile(join(tmpDir, 'story-1.md'), '# Story 1');
365
+ const result = await validateSpec(tmpDir);
366
+ expect(result.valid).toBe(true);
367
+ expect(result.format).toBe('bmad');
368
+ });
369
+
370
+ it('ralph dir validates successfully', async () => {
371
+ await writeFile(
372
+ join(tmpDir, SIDECAR_FILENAME),
373
+ `spec_format: ralph
374
+ display_name: Ralph Spec
375
+ description: A long enough description for the sidecar schema.
376
+ output_type: web-app
377
+ primary_stack: nextjs-typescript
378
+ `
379
+ );
380
+ await writeFile(
381
+ join(tmpDir, 'prd.json'),
382
+ JSON.stringify({ userStories: [{ title: 'As a user I want X' }] })
383
+ );
384
+ const result = await validateSpec(tmpDir);
385
+ expect(result.valid).toBe(true);
386
+ expect(result.format).toBe('ralph');
387
+ });
388
+
389
+ it('ralph prd.json missing userStories returns error', async () => {
390
+ await writeFile(
391
+ join(tmpDir, SIDECAR_FILENAME),
392
+ `spec_format: ralph
393
+ display_name: Ralph Spec
394
+ description: A long enough description for the sidecar schema.
395
+ output_type: web-app
396
+ primary_stack: nextjs-typescript
397
+ `
398
+ );
399
+ await writeFile(join(tmpDir, 'prd.json'), JSON.stringify({ other: true }));
400
+ const result = await validateSpec(tmpDir);
401
+ expect(result.valid).toBe(false);
402
+ expect(result.errors.some((e) => e.includes('userStories'))).toBe(true);
403
+ });
404
+
405
+ it('custom dir with sufficient .md validates', async () => {
406
+ await writeFile(
407
+ join(tmpDir, SIDECAR_FILENAME),
408
+ `spec_format: custom
409
+ display_name: Custom Spec
410
+ description: A long enough description for the sidecar schema.
411
+ output_type: web-app
412
+ primary_stack: nextjs-typescript
413
+ `
414
+ );
415
+ const content =
416
+ '# Readme\n\nThis is a spec with enough content to pass the 100-byte minimum for custom format. Extra text here.';
417
+ expect(content.length).toBeGreaterThan(100);
418
+ await writeFile(join(tmpDir, 'readme.md'), content);
419
+ const result = await validateSpec(tmpDir);
420
+ expect(result.valid).toBe(true);
421
+ expect(result.format).toBe('custom');
422
+ });
423
+
424
+ it('custom dir with only tiny .md files fails', async () => {
425
+ await writeFile(
426
+ join(tmpDir, SIDECAR_FILENAME),
427
+ `spec_format: custom
428
+ display_name: Custom Spec
429
+ description: A long enough description for the sidecar schema.
430
+ output_type: web-app
431
+ primary_stack: nextjs-typescript
432
+ `
433
+ );
434
+ await writeFile(join(tmpDir, 'tiny.md'), 'x');
435
+ const result = await validateSpec(tmpDir);
436
+ expect(result.valid).toBe(false);
437
+ expect(result.errors.some((e) => e.includes('100 bytes'))).toBe(true);
438
+ });
439
+
440
+ it('sidecar with invalid schema returns validation error', async () => {
441
+ await writeFile(join(tmpDir, 'spec.md'), '# Spec');
442
+ await writeFile(join(tmpDir, 'tasks.md'), '# Tasks');
443
+ await writeFile(
444
+ join(tmpDir, SIDECAR_FILENAME),
445
+ 'spec_format: speckit\ndisplay_name: X\ndescription: short'
446
+ );
447
+ const result = await validateSpec(tmpDir);
448
+ expect(result.valid).toBe(false);
449
+ expect(result.errors.some((e) => e.includes(SIDECAR_FILENAME))).toBe(true);
450
+ });
451
+
452
+ it('sidecar with valid schema passes and format is from sidecar', async () => {
453
+ await writeFile(join(tmpDir, 'spec.md'), '# Spec');
454
+ await writeFile(join(tmpDir, 'tasks.md'), '# Tasks');
455
+ await writeFile(
456
+ join(tmpDir, SIDECAR_FILENAME),
457
+ `spec_format: speckit
458
+ display_name: My Spec
459
+ description: A long enough description for the sidecar schema.
460
+ output_type: web-app
461
+ primary_stack: nextjs-typescript
462
+ `
463
+ );
464
+ const result = await validateSpec(tmpDir);
465
+ expect(result.valid).toBe(true);
466
+ expect(result.format).toBe('speckit');
467
+ expect(result.formatDetectedBy).toBe('sidecar');
468
+ });
469
+
470
+ it('empty directory fails with universal check', async () => {
471
+ const result = await validateSpec(tmpDir);
472
+ expect(result.valid).toBe(false);
473
+ expect(result.errors.some((e) => e.includes('empty') || e.includes('unreadable'))).toBe(true);
474
+ });
475
+
476
+ it('directory with only subdir and no files fails', async () => {
477
+ await mkdir(join(tmpDir, 'sub'), { recursive: true });
478
+ const result = await validateSpec(tmpDir);
479
+ expect(result.valid).toBe(false);
480
+ expect(result.errors.some((e) => e.includes('empty') || e.includes('readable'))).toBe(true);
481
+ });
482
+ });