@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.
- package/README.md +1 -1
- package/dist/{chunk-MS2DYACY.js → chunk-OTXWWFAO.js} +42 -3
- package/dist/chunk-OTXWWFAO.js.map +1 -0
- package/dist/{config-R5KWZSJP.js → config-5JMI3YAR.js} +2 -2
- package/dist/index.js +1945 -252
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/comment.test.ts +211 -0
- package/src/commands/comment.ts +176 -0
- package/src/commands/fork.test.ts +163 -0
- package/src/commands/info.test.ts +192 -0
- package/src/commands/info.ts +66 -2
- package/src/commands/init.test.ts +245 -0
- package/src/commands/init.ts +359 -25
- package/src/commands/issues.test.ts +382 -0
- package/src/commands/issues.ts +436 -0
- package/src/commands/login.test.ts +99 -0
- package/src/commands/login.ts +2 -6
- package/src/commands/logout.test.ts +54 -0
- package/src/commands/publish.test.ts +159 -0
- package/src/commands/publish.ts +1 -0
- package/src/commands/report.test.ts +181 -0
- package/src/commands/run.test.ts +419 -0
- package/src/commands/run.ts +71 -3
- package/src/commands/search.test.ts +147 -0
- package/src/commands/validate.test.ts +206 -2
- package/src/commands/validate.ts +315 -192
- package/src/commands/whoami.test.ts +106 -0
- package/src/index.ts +6 -0
- package/src/lib/convex-client.ts +6 -2
- package/src/lib/format-detection.test.ts +223 -0
- package/src/lib/format-detection.ts +172 -0
- package/src/lib/meta-instructions.test.ts +340 -0
- package/src/lib/meta-instructions.ts +562 -0
- package/src/lib/ralph-loop.test.ts +404 -0
- package/src/lib/ralph-loop.ts +501 -95
- package/src/lib/telemetry.ts +7 -1
- package/dist/chunk-MS2DYACY.js.map +0 -1
- /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
|
|
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
|
|
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
|
+
});
|