@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
@@ -0,0 +1,340 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdir, writeFile, rm } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { randomUUID } from 'crypto';
6
+ import { generateMetaInstructions, META_INSTRUCTION_FILENAME } from './meta-instructions.js';
7
+
8
+ describe('generateMetaInstructions', () => {
9
+ let tmpDir: string;
10
+
11
+ beforeEach(async () => {
12
+ tmpDir = join(tmpdir(), `meta-instr-${randomUUID()}`);
13
+ await mkdir(tmpDir, { recursive: true });
14
+ });
15
+
16
+ afterEach(async () => {
17
+ await rm(tmpDir, { recursive: true, force: true });
18
+ });
19
+
20
+ // ── META_INSTRUCTION_FILENAME constant ───────────────────────────────────
21
+
22
+ it('exports META_INSTRUCTION_FILENAME as .specmarket-runner.md', () => {
23
+ expect(META_INSTRUCTION_FILENAME).toBe('.specmarket-runner.md');
24
+ });
25
+
26
+ // ── Sidecar data injection ───────────────────────────────────────────────
27
+
28
+ it('includes display_name and description from specmarket.yaml when present', async () => {
29
+ await writeFile(
30
+ join(tmpDir, 'specmarket.yaml'),
31
+ 'spec_format: specmarket\ndisplay_name: My Spec\ndescription: A test spec.'
32
+ );
33
+ const content = await generateMetaInstructions(tmpDir, 'specmarket');
34
+ expect(content).toContain('My Spec');
35
+ expect(content).toContain('A test spec.');
36
+ });
37
+
38
+ it('omits display_name/description lines when specmarket.yaml absent', async () => {
39
+ const content = await generateMetaInstructions(tmpDir, 'specmarket');
40
+ expect(content).not.toContain('**Spec:**');
41
+ expect(content).not.toContain('**Description:**');
42
+ });
43
+
44
+ it('includes preamble for every format', async () => {
45
+ for (const fmt of ['specmarket', 'speckit', 'bmad', 'ralph', 'custom']) {
46
+ const content = await generateMetaInstructions(tmpDir, fmt);
47
+ expect(content).toContain('# SpecMarket Runner Instructions');
48
+ expect(content).toContain('You are an AI coding agent');
49
+ }
50
+ });
51
+
52
+ // ── specmarket format ────────────────────────────────────────────────────
53
+
54
+ describe('specmarket format', () => {
55
+ it('returns content identifying specmarket format', async () => {
56
+ const content = await generateMetaInstructions(tmpDir, 'specmarket');
57
+ expect(content).toContain('SpecMarket (Native)');
58
+ });
59
+
60
+ it('lists present spec files', async () => {
61
+ await writeFile(join(tmpDir, 'PROMPT.md'), '# Prompt');
62
+ await writeFile(join(tmpDir, 'SPEC.md'), '# Spec');
63
+ const content = await generateMetaInstructions(tmpDir, 'specmarket');
64
+ expect(content).toContain('`PROMPT.md`');
65
+ expect(content).toContain('`SPEC.md`');
66
+ });
67
+
68
+ it('does not list absent spec files', async () => {
69
+ // Only write PROMPT.md — SPEC.md absent
70
+ await writeFile(join(tmpDir, 'PROMPT.md'), '# Prompt');
71
+ const content = await generateMetaInstructions(tmpDir, 'specmarket');
72
+ expect(content).toContain('`PROMPT.md`');
73
+ expect(content).not.toContain('`SPEC.md`');
74
+ });
75
+
76
+ it('includes task tracking with TASKS.md format', async () => {
77
+ const content = await generateMetaInstructions(tmpDir, 'specmarket');
78
+ expect(content).toContain('TASKS.md');
79
+ expect(content).toContain('- [ ]');
80
+ expect(content).toContain('- [x]');
81
+ });
82
+
83
+ it('includes SUCCESS_CRITERIA.md in completion criteria', async () => {
84
+ const content = await generateMetaInstructions(tmpDir, 'specmarket');
85
+ expect(content).toContain('SUCCESS_CRITERIA.md');
86
+ });
87
+
88
+ it('includes testing section', async () => {
89
+ const content = await generateMetaInstructions(tmpDir, 'specmarket');
90
+ expect(content).toContain('Running Tests');
91
+ expect(content).toContain('npm test');
92
+ });
93
+
94
+ it('includes completion reminders', async () => {
95
+ const content = await generateMetaInstructions(tmpDir, 'specmarket');
96
+ expect(content).toContain('Important Reminders');
97
+ });
98
+ });
99
+
100
+ // ── speckit format ───────────────────────────────────────────────────────
101
+
102
+ describe('speckit format', () => {
103
+ it('returns content identifying speckit format', async () => {
104
+ const content = await generateMetaInstructions(tmpDir, 'speckit');
105
+ expect(content).toContain('Spec Kit');
106
+ });
107
+
108
+ it('references tasks.md when present', async () => {
109
+ await writeFile(join(tmpDir, 'spec.md'), '# Spec');
110
+ await writeFile(join(tmpDir, 'tasks.md'), '# Tasks');
111
+ const content = await generateMetaInstructions(tmpDir, 'speckit');
112
+ expect(content).toContain('tasks.md');
113
+ });
114
+
115
+ it('references plan.md when tasks.md absent but plan.md present', async () => {
116
+ await writeFile(join(tmpDir, 'spec.md'), '# Spec');
117
+ await writeFile(join(tmpDir, 'plan.md'), '# Plan');
118
+ const content = await generateMetaInstructions(tmpDir, 'speckit');
119
+ expect(content).toContain('plan.md');
120
+ });
121
+
122
+ it('defaults to tasks.md when neither file is present', async () => {
123
+ const content = await generateMetaInstructions(tmpDir, 'speckit');
124
+ expect(content).toContain('tasks.md');
125
+ });
126
+
127
+ it('mentions .specify/ when that directory exists', async () => {
128
+ await mkdir(join(tmpDir, '.specify'), { recursive: true });
129
+ const content = await generateMetaInstructions(tmpDir, 'speckit');
130
+ expect(content).toContain('.specify/');
131
+ });
132
+
133
+ it('does not mention .specify/ when absent', async () => {
134
+ const content = await generateMetaInstructions(tmpDir, 'speckit');
135
+ expect(content).not.toContain('.specify/');
136
+ });
137
+
138
+ it('includes testing section', async () => {
139
+ const content = await generateMetaInstructions(tmpDir, 'speckit');
140
+ expect(content).toContain('Running Tests');
141
+ });
142
+ });
143
+
144
+ // ── bmad format ──────────────────────────────────────────────────────────
145
+
146
+ describe('bmad format', () => {
147
+ it('returns content identifying bmad format', async () => {
148
+ const content = await generateMetaInstructions(tmpDir, 'bmad');
149
+ expect(content).toContain('BMAD Method');
150
+ });
151
+
152
+ it('lists present story files', async () => {
153
+ await writeFile(join(tmpDir, 'story-1.md'), '# Story 1');
154
+ await writeFile(join(tmpDir, 'story-2.md'), '# Story 2');
155
+ const content = await generateMetaInstructions(tmpDir, 'bmad');
156
+ expect(content).toContain('story-1.md');
157
+ expect(content).toContain('story-2.md');
158
+ });
159
+
160
+ it('notes when no story files found', async () => {
161
+ const content = await generateMetaInstructions(tmpDir, 'bmad');
162
+ expect(content).toContain('No story');
163
+ });
164
+
165
+ it('references prd.md and architecture.md when present', async () => {
166
+ await writeFile(join(tmpDir, 'prd.md'), '# PRD');
167
+ await writeFile(join(tmpDir, 'architecture.md'), '# Arch');
168
+ const content = await generateMetaInstructions(tmpDir, 'bmad');
169
+ expect(content).toContain('`prd.md`');
170
+ expect(content).toContain('`architecture.md`');
171
+ });
172
+
173
+ it('includes instructions for marking stories done', async () => {
174
+ const content = await generateMetaInstructions(tmpDir, 'bmad');
175
+ expect(content).toContain('Status: Done');
176
+ });
177
+
178
+ it('includes testing section', async () => {
179
+ const content = await generateMetaInstructions(tmpDir, 'bmad');
180
+ expect(content).toContain('Running Tests');
181
+ });
182
+
183
+ it('sorts story files numerically so story-10.md follows story-9.md', async () => {
184
+ for (let i = 1; i <= 11; i++) {
185
+ await writeFile(join(tmpDir, `story-${i}.md`), `# Story ${i}`);
186
+ }
187
+ const content = await generateMetaInstructions(tmpDir, 'bmad');
188
+ const pos9 = content.indexOf('story-9.md');
189
+ const pos10 = content.indexOf('story-10.md');
190
+ expect(pos9).toBeGreaterThan(-1);
191
+ expect(pos10).toBeGreaterThan(-1);
192
+ // story-9 must appear before story-10 in the listing
193
+ expect(pos9).toBeLessThan(pos10);
194
+ });
195
+ });
196
+
197
+ // ── ralph format ─────────────────────────────────────────────────────────
198
+
199
+ describe('ralph format', () => {
200
+ it('returns content identifying ralph format', async () => {
201
+ const content = await generateMetaInstructions(tmpDir, 'ralph');
202
+ expect(content).toContain('Ralph');
203
+ });
204
+
205
+ it('references prd.json', async () => {
206
+ const content = await generateMetaInstructions(tmpDir, 'ralph');
207
+ expect(content).toContain('prd.json');
208
+ });
209
+
210
+ it('lists user story titles from prd.json when available', async () => {
211
+ await writeFile(
212
+ join(tmpDir, 'prd.json'),
213
+ JSON.stringify({
214
+ userStories: [
215
+ { title: 'User can log in' },
216
+ { title: 'User can view dashboard' },
217
+ ],
218
+ })
219
+ );
220
+ const content = await generateMetaInstructions(tmpDir, 'ralph');
221
+ expect(content).toContain('User can log in');
222
+ expect(content).toContain('User can view dashboard');
223
+ });
224
+
225
+ it('uses name field when title is absent', async () => {
226
+ await writeFile(
227
+ join(tmpDir, 'prd.json'),
228
+ JSON.stringify({ userStories: [{ name: 'Build API' }] })
229
+ );
230
+ const content = await generateMetaInstructions(tmpDir, 'ralph');
231
+ expect(content).toContain('Build API');
232
+ });
233
+
234
+ it('falls back to Story N when no title or name', async () => {
235
+ await writeFile(
236
+ join(tmpDir, 'prd.json'),
237
+ JSON.stringify({ userStories: [{ description: 'Some story' }] })
238
+ );
239
+ const content = await generateMetaInstructions(tmpDir, 'ralph');
240
+ expect(content).toContain('Story 1');
241
+ });
242
+
243
+ it('handles prd.json with empty userStories without error', async () => {
244
+ await writeFile(
245
+ join(tmpDir, 'prd.json'),
246
+ JSON.stringify({ userStories: [] })
247
+ );
248
+ const content = await generateMetaInstructions(tmpDir, 'ralph');
249
+ expect(content).toContain('prd.json');
250
+ });
251
+
252
+ it('handles missing prd.json without throwing', async () => {
253
+ const content = await generateMetaInstructions(tmpDir, 'ralph');
254
+ expect(content).toContain('prd.json');
255
+ });
256
+
257
+ it('instructs agent to create PROGRESS.md', async () => {
258
+ const content = await generateMetaInstructions(tmpDir, 'ralph');
259
+ expect(content).toContain('PROGRESS.md');
260
+ });
261
+
262
+ it('includes testing section', async () => {
263
+ const content = await generateMetaInstructions(tmpDir, 'ralph');
264
+ expect(content).toContain('Running Tests');
265
+ });
266
+ });
267
+
268
+ // ── custom format ────────────────────────────────────────────────────────
269
+
270
+ describe('custom format', () => {
271
+ it('returns content identifying custom format', async () => {
272
+ const content = await generateMetaInstructions(tmpDir, 'custom');
273
+ expect(content).toContain('Custom');
274
+ });
275
+
276
+ it('lists top-level markdown files when present', async () => {
277
+ await writeFile(join(tmpDir, 'README.md'), '# Readme');
278
+ await writeFile(join(tmpDir, 'requirements.md'), '# Requirements');
279
+ const content = await generateMetaInstructions(tmpDir, 'custom');
280
+ expect(content).toContain('README.md');
281
+ expect(content).toContain('requirements.md');
282
+ });
283
+
284
+ it('notes when no markdown files found', async () => {
285
+ const content = await generateMetaInstructions(tmpDir, 'custom');
286
+ expect(content).toContain('no .md files found');
287
+ });
288
+
289
+ it('instructs agent to create PROGRESS.md when no checkboxes', async () => {
290
+ const content = await generateMetaInstructions(tmpDir, 'custom');
291
+ expect(content).toContain('PROGRESS.md');
292
+ });
293
+
294
+ it('includes checkbox format instructions', async () => {
295
+ const content = await generateMetaInstructions(tmpDir, 'custom');
296
+ expect(content).toContain('- [ ]');
297
+ expect(content).toContain('- [x]');
298
+ });
299
+
300
+ it('includes testing section', async () => {
301
+ const content = await generateMetaInstructions(tmpDir, 'custom');
302
+ expect(content).toContain('Running Tests');
303
+ });
304
+ });
305
+
306
+ // ── Unknown format fallback ───────────────────────────────────────────────
307
+
308
+ it('falls back to custom instructions for unknown format strings', async () => {
309
+ const content = await generateMetaInstructions(tmpDir, 'my-proprietary-format');
310
+ expect(content).toContain('# SpecMarket Runner Instructions');
311
+ expect(content).toContain('Custom');
312
+ });
313
+
314
+ // ── Content structure guarantees ─────────────────────────────────────────
315
+
316
+ it.each(['specmarket', 'speckit', 'bmad', 'ralph', 'custom'])(
317
+ '%s format always includes a testing section with npm test',
318
+ async (fmt) => {
319
+ const content = await generateMetaInstructions(tmpDir, fmt);
320
+ expect(content).toContain('npm test');
321
+ expect(content).toContain('pytest');
322
+ }
323
+ );
324
+
325
+ it.each(['specmarket', 'speckit', 'bmad', 'ralph', 'custom'])(
326
+ '%s format always includes a completion criteria section',
327
+ async (fmt) => {
328
+ const content = await generateMetaInstructions(tmpDir, fmt);
329
+ expect(content).toContain('Completion Criteria');
330
+ }
331
+ );
332
+
333
+ it.each(['specmarket', 'speckit', 'bmad', 'ralph', 'custom'])(
334
+ '%s format always includes important reminders',
335
+ async (fmt) => {
336
+ const content = await generateMetaInstructions(tmpDir, fmt);
337
+ expect(content).toContain('Important Reminders');
338
+ }
339
+ );
340
+ });