@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
package/src/commands/init.ts
CHANGED
|
@@ -1,13 +1,54 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import ora from 'ora';
|
|
4
|
-
import { mkdir, writeFile } from 'fs/promises';
|
|
5
|
-
import { join, resolve } from 'path';
|
|
6
|
-
import { EXIT_CODES } from '@specmarket/shared';
|
|
4
|
+
import { mkdir, writeFile, readdir } from 'fs/promises';
|
|
5
|
+
import { join, resolve, basename } from 'path';
|
|
6
|
+
import { EXIT_CODES, SIDECAR_FILENAME } from '@specmarket/shared';
|
|
7
7
|
import createDebug from 'debug';
|
|
8
|
+
import { detectSpecFormat, fileExists, directoryExists } from '../lib/format-detection.js';
|
|
8
9
|
|
|
9
10
|
const debug = createDebug('specmarket:cli');
|
|
10
11
|
|
|
12
|
+
type MetadataData = {
|
|
13
|
+
displayName: string;
|
|
14
|
+
description: string;
|
|
15
|
+
replacesSaas?: string;
|
|
16
|
+
outputType: string;
|
|
17
|
+
primaryStack: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Build specmarket.yaml for any spec format (single source of truth for validate/publish) */
|
|
21
|
+
function buildSpecmarketYaml(data: MetadataData & { specFormat: string }): string {
|
|
22
|
+
return `# SpecMarket metadata (required for validate/publish)
|
|
23
|
+
# Single source of truth for format and marketplace fields.
|
|
24
|
+
# Your existing spec files (Spec Kit, BMAD, Ralph, etc.) are not modified.
|
|
25
|
+
|
|
26
|
+
spec_format: ${data.specFormat}
|
|
27
|
+
display_name: "${data.displayName.replace(/"/g, '\\"')}"
|
|
28
|
+
description: "${data.description.replace(/"/g, '\\"')}"
|
|
29
|
+
output_type: ${data.outputType}
|
|
30
|
+
primary_stack: ${data.primaryStack}
|
|
31
|
+
${data.replacesSaas ? `replaces_saas: "${data.replacesSaas.replace(/"/g, '\\"')}"` : '# replaces_saas: "ProductName"'}
|
|
32
|
+
# replaces_pricing: "$0-16/mo"
|
|
33
|
+
tags: []
|
|
34
|
+
estimated_tokens: 50000
|
|
35
|
+
estimated_cost_usd: 2.50
|
|
36
|
+
estimated_time_minutes: 30
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** specmarket.yaml for new SpecMarket (classic) specs */
|
|
41
|
+
const SPECMARKET_YAML_TEMPLATE = (data: {
|
|
42
|
+
displayName: string;
|
|
43
|
+
replacesSaas?: string;
|
|
44
|
+
outputType: string;
|
|
45
|
+
primaryStack: string;
|
|
46
|
+
}) => buildSpecmarketYaml({
|
|
47
|
+
...data,
|
|
48
|
+
specFormat: 'specmarket',
|
|
49
|
+
description: `A ${data.outputType} spec${data.replacesSaas ? ` that replaces ${data.replacesSaas}` : ''}.`,
|
|
50
|
+
});
|
|
51
|
+
|
|
11
52
|
const SPEC_YAML_TEMPLATE = (data: {
|
|
12
53
|
name: string;
|
|
13
54
|
displayName: string;
|
|
@@ -26,7 +67,7 @@ ${data.replacesSaas ? `replaces_saas: "${data.replacesSaas}"` : '# replaces_saas
|
|
|
26
67
|
output_type: ${data.outputType}
|
|
27
68
|
primary_stack: ${data.primaryStack}
|
|
28
69
|
version: "1.0.0"
|
|
29
|
-
runner: claude
|
|
70
|
+
runner: claude
|
|
30
71
|
min_model: "claude-opus-4-5"
|
|
31
72
|
|
|
32
73
|
estimated_tokens: 50000
|
|
@@ -71,9 +112,9 @@ Read the requirements in SPEC.md and implement the application step by step.
|
|
|
71
112
|
## Process
|
|
72
113
|
|
|
73
114
|
1. Read SPEC.md completely before writing any code
|
|
74
|
-
2. Check
|
|
115
|
+
2. Check TASKS.md for outstanding items
|
|
75
116
|
3. Implement features, run tests, iterate
|
|
76
|
-
4. Update
|
|
117
|
+
4. Update TASKS.md as you complete items
|
|
77
118
|
5. Verify SUCCESS_CRITERIA.md criteria are met
|
|
78
119
|
|
|
79
120
|
## Rules
|
|
@@ -81,7 +122,7 @@ Read the requirements in SPEC.md and implement the application step by step.
|
|
|
81
122
|
- Follow stdlib/STACK.md for technology choices
|
|
82
123
|
- Write tests for all business logic
|
|
83
124
|
- Do not skip steps or take shortcuts
|
|
84
|
-
- Update
|
|
125
|
+
- Update TASKS.md after each significant change
|
|
85
126
|
`;
|
|
86
127
|
|
|
87
128
|
const SPEC_MD_TEMPLATE = (data: { displayName: string }) => `# ${data.displayName} — Specification
|
|
@@ -144,12 +185,12 @@ ${primaryStack}
|
|
|
144
185
|
- Playwright for E2E (optional)
|
|
145
186
|
`;
|
|
146
187
|
|
|
147
|
-
const
|
|
188
|
+
const TASKS_MD_TEMPLATE = (displayName: string) => `# Tasks
|
|
148
189
|
|
|
149
190
|
> This file tracks outstanding work. Update it after each change.
|
|
150
|
-
>
|
|
191
|
+
> All items checked = implementation complete.
|
|
151
192
|
|
|
152
|
-
## ${displayName} — Initial Implementation
|
|
193
|
+
## Phase 1: ${displayName} — Initial Implementation
|
|
153
194
|
|
|
154
195
|
- [ ] Set up project structure and dependencies
|
|
155
196
|
- [ ] Implement core data model
|
|
@@ -158,14 +199,278 @@ const FIX_PLAN_TEMPLATE = (displayName: string) => `# Fix Plan
|
|
|
158
199
|
- [ ] Implement UI/interface
|
|
159
200
|
- [ ] Write integration tests
|
|
160
201
|
- [ ] Update README.md
|
|
202
|
+
|
|
203
|
+
## Discovered Issues
|
|
161
204
|
`;
|
|
162
205
|
|
|
206
|
+
/** Prompt for marketplace metadata only (used when adding sidecar to existing spec) */
|
|
207
|
+
async function promptMetadataOnly(defaultDisplayName?: string): Promise<MetadataData> {
|
|
208
|
+
const { default: inquirer } = await import('inquirer');
|
|
209
|
+
const answers = await inquirer.prompt<{
|
|
210
|
+
displayName: string;
|
|
211
|
+
description: string;
|
|
212
|
+
replacesSaas: string;
|
|
213
|
+
outputType: string;
|
|
214
|
+
primaryStack: string;
|
|
215
|
+
}>([
|
|
216
|
+
{
|
|
217
|
+
type: 'input',
|
|
218
|
+
name: 'displayName',
|
|
219
|
+
message: 'Display name for the marketplace:',
|
|
220
|
+
default: defaultDisplayName ?? 'My Spec',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
type: 'input',
|
|
224
|
+
name: 'description',
|
|
225
|
+
message: 'Short description (min 10 characters):',
|
|
226
|
+
default: 'A spec ready to validate and publish on SpecMarket.',
|
|
227
|
+
validate: (v: string) => (v.length >= 10 ? true : 'Description must be at least 10 characters'),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
type: 'input',
|
|
231
|
+
name: 'replacesSaas',
|
|
232
|
+
message: 'What SaaS product does this replace? (optional, Enter to skip):',
|
|
233
|
+
default: '',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
type: 'list',
|
|
237
|
+
name: 'outputType',
|
|
238
|
+
message: 'Output type:',
|
|
239
|
+
choices: [
|
|
240
|
+
{ name: 'Web Application', value: 'web-app' },
|
|
241
|
+
{ name: 'CLI Tool', value: 'cli-tool' },
|
|
242
|
+
{ name: 'API Service', value: 'api-service' },
|
|
243
|
+
{ name: 'Library/Package', value: 'library' },
|
|
244
|
+
{ name: 'Mobile App', value: 'mobile-app' },
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
type: 'list',
|
|
249
|
+
name: 'primaryStack',
|
|
250
|
+
message: 'Primary stack:',
|
|
251
|
+
choices: [
|
|
252
|
+
{ name: 'Next.js + TypeScript', value: 'nextjs-typescript' },
|
|
253
|
+
{ name: 'Astro + TypeScript', value: 'astro-typescript' },
|
|
254
|
+
{ name: 'Python + FastAPI', value: 'python-fastapi' },
|
|
255
|
+
{ name: 'Go', value: 'go' },
|
|
256
|
+
{ name: 'Rust', value: 'rust' },
|
|
257
|
+
{ name: 'Other', value: 'other' },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
]);
|
|
261
|
+
return {
|
|
262
|
+
displayName: answers.displayName,
|
|
263
|
+
description: answers.description,
|
|
264
|
+
replacesSaas: answers.replacesSaas || undefined,
|
|
265
|
+
outputType: answers.outputType,
|
|
266
|
+
primaryStack: answers.primaryStack,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Returns true if directory exists and has at least one file (not just subdirs). */
|
|
271
|
+
async function dirHasFiles(dir: string): Promise<boolean> {
|
|
272
|
+
try {
|
|
273
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
274
|
+
return entries.some((e) => e.isFile());
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
163
280
|
export async function handleInit(opts: {
|
|
164
281
|
name?: string;
|
|
165
282
|
path?: string;
|
|
283
|
+
from?: string;
|
|
166
284
|
}): Promise<void> {
|
|
167
285
|
const { default: inquirer } = await import('inquirer');
|
|
168
286
|
|
|
287
|
+
// --from: strict import-only mode. Errors if directory doesn't exist or has no spec files.
|
|
288
|
+
if (opts.from !== undefined && opts.from !== '') {
|
|
289
|
+
const targetDir = resolve(opts.from);
|
|
290
|
+
|
|
291
|
+
const dirExists = await directoryExists(targetDir);
|
|
292
|
+
if (!dirExists) {
|
|
293
|
+
console.error(chalk.red(`Directory not found: ${targetDir}`));
|
|
294
|
+
console.error(chalk.gray('--from requires an existing spec directory. Run specmarket init (no flags) to create a new spec from scratch.'));
|
|
295
|
+
process.exit(EXIT_CODES.INVALID_SPEC);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const hasAnyFiles = await dirHasFiles(targetDir);
|
|
299
|
+
if (!hasAnyFiles) {
|
|
300
|
+
console.error(chalk.red(`Directory is empty: ${targetDir}`));
|
|
301
|
+
console.error(chalk.gray('--from requires a directory with spec files (Spec Kit, BMAD, Ralph, or custom markdown). Run specmarket init to create a new spec.'));
|
|
302
|
+
process.exit(EXIT_CODES.INVALID_SPEC);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const sidecarPath = join(targetDir, SIDECAR_FILENAME);
|
|
306
|
+
if (await fileExists(sidecarPath)) {
|
|
307
|
+
console.log(chalk.yellow(`${SIDECAR_FILENAME} already exists in this directory.`));
|
|
308
|
+
console.log(chalk.gray('Run specmarket validate to check your spec, then specmarket publish to publish.'));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const detection = await detectSpecFormat(targetDir);
|
|
313
|
+
const formatLabel =
|
|
314
|
+
detection.format === 'specmarket'
|
|
315
|
+
? 'SpecMarket (spec.yaml + PROMPT.md + …)'
|
|
316
|
+
: detection.format === 'speckit'
|
|
317
|
+
? 'Spec Kit'
|
|
318
|
+
: detection.format === 'bmad'
|
|
319
|
+
? 'BMAD'
|
|
320
|
+
: detection.format === 'ralph'
|
|
321
|
+
? 'Ralph'
|
|
322
|
+
: 'custom markdown';
|
|
323
|
+
console.log(chalk.gray(`Detected ${formatLabel} spec. Adding SpecMarket metadata only; your files will not be modified.`));
|
|
324
|
+
console.log('');
|
|
325
|
+
|
|
326
|
+
const metadata = await promptMetadataOnly(basename(targetDir));
|
|
327
|
+
const yaml = buildSpecmarketYaml({ ...metadata, specFormat: detection.format });
|
|
328
|
+
await writeFile(sidecarPath, yaml);
|
|
329
|
+
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log(chalk.green(`Added ${SIDECAR_FILENAME} to ${targetDir}`));
|
|
332
|
+
console.log('');
|
|
333
|
+
console.log(chalk.bold('Next steps:'));
|
|
334
|
+
console.log(` 1. Run ${chalk.cyan('specmarket validate')} to check your spec`);
|
|
335
|
+
console.log(` 2. Run ${chalk.cyan('specmarket publish')} to publish to the marketplace`);
|
|
336
|
+
console.log(` 3. Run ${chalk.cyan('specmarket run')} to execute the spec locally`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// When -p/--path is set, we're initializing in an existing directory (safe add-sidecar flow)
|
|
341
|
+
if (opts.path !== undefined && opts.path !== '') {
|
|
342
|
+
const targetDir = resolve(opts.path);
|
|
343
|
+
await mkdir(targetDir, { recursive: true });
|
|
344
|
+
|
|
345
|
+
const sidecarPath = join(targetDir, SIDECAR_FILENAME);
|
|
346
|
+
if (await fileExists(sidecarPath)) {
|
|
347
|
+
console.log(chalk.yellow(`${SIDECAR_FILENAME} already exists in this directory.`));
|
|
348
|
+
console.log(chalk.gray('Run specmarket validate to check your spec, then specmarket publish to publish.'));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const detection = await detectSpecFormat(targetDir);
|
|
353
|
+
const hasAnyFiles = await dirHasFiles(targetDir);
|
|
354
|
+
|
|
355
|
+
if (hasAnyFiles && detection.format !== 'custom') {
|
|
356
|
+
// Recognized format (specmarket, speckit, bmad, ralph): add sidecar only, no overwrites
|
|
357
|
+
const formatLabel =
|
|
358
|
+
detection.format === 'specmarket'
|
|
359
|
+
? 'SpecMarket (spec.yaml + PROMPT.md + …)'
|
|
360
|
+
: detection.format === 'speckit'
|
|
361
|
+
? 'Spec Kit'
|
|
362
|
+
: detection.format === 'bmad'
|
|
363
|
+
? 'BMAD'
|
|
364
|
+
: detection.format === 'ralph'
|
|
365
|
+
? 'Ralph'
|
|
366
|
+
: detection.format;
|
|
367
|
+
console.log(chalk.gray(`Detected ${formatLabel} spec. Adding SpecMarket metadata only; your files will not be modified.`));
|
|
368
|
+
console.log('');
|
|
369
|
+
|
|
370
|
+
const metadata = await promptMetadataOnly(basename(targetDir));
|
|
371
|
+
const yaml = buildSpecmarketYaml({
|
|
372
|
+
...metadata,
|
|
373
|
+
specFormat: detection.format,
|
|
374
|
+
});
|
|
375
|
+
await writeFile(sidecarPath, yaml);
|
|
376
|
+
|
|
377
|
+
console.log('');
|
|
378
|
+
console.log(chalk.green(`Added ${SIDECAR_FILENAME} to ${targetDir}`));
|
|
379
|
+
console.log('');
|
|
380
|
+
console.log(chalk.bold('Next steps:'));
|
|
381
|
+
console.log(` 1. Run ${chalk.cyan('specmarket validate')} to check your spec`);
|
|
382
|
+
console.log(` 2. Run ${chalk.cyan('specmarket publish')} to publish to the marketplace`);
|
|
383
|
+
console.log(` 3. Run ${chalk.cyan('specmarket run')} to execute the spec locally`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (hasAnyFiles && detection.format === 'custom') {
|
|
388
|
+
// Custom (e.g. random .md): still add sidecar so they can validate/publish
|
|
389
|
+
console.log(chalk.gray('Detected markdown spec. Adding SpecMarket metadata only; your files will not be modified.'));
|
|
390
|
+
console.log('');
|
|
391
|
+
const metadata = await promptMetadataOnly(basename(targetDir));
|
|
392
|
+
const yaml = buildSpecmarketYaml({
|
|
393
|
+
...metadata,
|
|
394
|
+
specFormat: 'custom',
|
|
395
|
+
});
|
|
396
|
+
await writeFile(sidecarPath, yaml);
|
|
397
|
+
console.log('');
|
|
398
|
+
console.log(chalk.green(`Added ${SIDECAR_FILENAME} to ${targetDir}`));
|
|
399
|
+
console.log('');
|
|
400
|
+
console.log(chalk.bold('Next steps:'));
|
|
401
|
+
console.log(` 1. Run ${chalk.cyan('specmarket validate')} to check your spec`);
|
|
402
|
+
console.log(` 2. Run ${chalk.cyan('specmarket publish')} to publish to the marketplace`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Empty directory: offer to create from scratch
|
|
407
|
+
console.log(chalk.gray('Directory is empty or has no recognized spec format.'));
|
|
408
|
+
const { createNew } = await inquirer.prompt<{ createNew: boolean }>([
|
|
409
|
+
{
|
|
410
|
+
type: 'confirm',
|
|
411
|
+
name: 'createNew',
|
|
412
|
+
message: 'Create a new SpecMarket spec from scratch here?',
|
|
413
|
+
default: true,
|
|
414
|
+
},
|
|
415
|
+
]);
|
|
416
|
+
if (!createNew) {
|
|
417
|
+
console.log(chalk.gray('Exiting. Add spec files (e.g. Spec Kit, BMAD) and run specmarket init -p . again.'));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const spinner = ora(`Creating spec at ${targetDir}...`).start();
|
|
421
|
+
const fullAnswers = await inquirer.prompt<{
|
|
422
|
+
name: string;
|
|
423
|
+
displayName: string;
|
|
424
|
+
replacesSaas: string;
|
|
425
|
+
outputType: string;
|
|
426
|
+
primaryStack: string;
|
|
427
|
+
}>([
|
|
428
|
+
{ type: 'input', name: 'name', message: 'Spec name (lowercase, hyphens only):', default: opts.name ?? (basename(targetDir) || 'my-spec'), validate: (v: string) => /^[a-z0-9-]+$/.test(v) || 'Must be lowercase alphanumeric with hyphens' },
|
|
429
|
+
{ type: 'input', name: 'displayName', message: 'Display name:', default: (a: { name: string }) => a.name.split('-').map((w) => w[0]?.toUpperCase() + w.slice(1)).join(' ') },
|
|
430
|
+
{ type: 'input', name: 'replacesSaas', message: 'What SaaS product does this replace? (optional):', default: '' },
|
|
431
|
+
{ type: 'list', name: 'outputType', message: 'Output type:', choices: [
|
|
432
|
+
{ name: 'Web Application', value: 'web-app' },
|
|
433
|
+
{ name: 'CLI Tool', value: 'cli-tool' },
|
|
434
|
+
{ name: 'API Service', value: 'api-service' },
|
|
435
|
+
{ name: 'Library/Package', value: 'library' },
|
|
436
|
+
{ name: 'Mobile App', value: 'mobile-app' },
|
|
437
|
+
]},
|
|
438
|
+
{ type: 'list', name: 'primaryStack', message: 'Primary stack:', choices: [
|
|
439
|
+
{ name: 'Next.js + TypeScript', value: 'nextjs-typescript' },
|
|
440
|
+
{ name: 'Astro + TypeScript', value: 'astro-typescript' },
|
|
441
|
+
{ name: 'Python + FastAPI', value: 'python-fastapi' },
|
|
442
|
+
{ name: 'Go', value: 'go' },
|
|
443
|
+
{ name: 'Rust', value: 'rust' },
|
|
444
|
+
{ name: 'Other', value: 'other' },
|
|
445
|
+
]},
|
|
446
|
+
]);
|
|
447
|
+
const data = {
|
|
448
|
+
name: fullAnswers.name,
|
|
449
|
+
displayName: fullAnswers.displayName,
|
|
450
|
+
replacesSaas: fullAnswers.replacesSaas || undefined,
|
|
451
|
+
outputType: fullAnswers.outputType,
|
|
452
|
+
primaryStack: fullAnswers.primaryStack,
|
|
453
|
+
};
|
|
454
|
+
await mkdir(join(targetDir, 'stdlib'), { recursive: true });
|
|
455
|
+
await Promise.all([
|
|
456
|
+
writeFile(sidecarPath, SPECMARKET_YAML_TEMPLATE(data)),
|
|
457
|
+
writeFile(join(targetDir, 'spec.yaml'), SPEC_YAML_TEMPLATE(data)),
|
|
458
|
+
writeFile(join(targetDir, 'PROMPT.md'), PROMPT_MD_TEMPLATE(data)),
|
|
459
|
+
writeFile(join(targetDir, 'SPEC.md'), SPEC_MD_TEMPLATE(data)),
|
|
460
|
+
writeFile(join(targetDir, 'SUCCESS_CRITERIA.md'), SUCCESS_CRITERIA_TEMPLATE),
|
|
461
|
+
writeFile(join(targetDir, 'stdlib', 'STACK.md'), STACK_MD_TEMPLATE(fullAnswers.primaryStack)),
|
|
462
|
+
writeFile(join(targetDir, 'TASKS.md'), TASKS_MD_TEMPLATE(fullAnswers.displayName)),
|
|
463
|
+
]);
|
|
464
|
+
spinner.succeed(chalk.green(`Spec created at ${targetDir}`));
|
|
465
|
+
console.log('');
|
|
466
|
+
console.log(chalk.bold('Next steps:'));
|
|
467
|
+
console.log(` 1. Edit ${chalk.cyan('SPEC.md')} with your application requirements`);
|
|
468
|
+
console.log(` 2. Edit ${chalk.cyan('SUCCESS_CRITERIA.md')} with pass/fail criteria`);
|
|
469
|
+
console.log(` 3. Run ${chalk.cyan('specmarket validate')} then ${chalk.cyan('specmarket run')}`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// No path: create new directory (or add sidecar if that directory already exists with content)
|
|
169
474
|
const answers = await inquirer.prompt<{
|
|
170
475
|
name: string;
|
|
171
476
|
displayName: string;
|
|
@@ -185,11 +490,8 @@ export async function handleInit(opts: {
|
|
|
185
490
|
type: 'input',
|
|
186
491
|
name: 'displayName',
|
|
187
492
|
message: 'Display name:',
|
|
188
|
-
default: (
|
|
189
|
-
|
|
190
|
-
.split('-')
|
|
191
|
-
.map((w) => w[0]?.toUpperCase() + w.slice(1))
|
|
192
|
-
.join(' '),
|
|
493
|
+
default: (ans: { name: string }) =>
|
|
494
|
+
ans.name.split('-').map((w) => w[0]?.toUpperCase() + w.slice(1)).join(' '),
|
|
193
495
|
},
|
|
194
496
|
{
|
|
195
497
|
type: 'input',
|
|
@@ -224,14 +526,44 @@ export async function handleInit(opts: {
|
|
|
224
526
|
},
|
|
225
527
|
]);
|
|
226
528
|
|
|
227
|
-
const targetDir = resolve(
|
|
529
|
+
const targetDir = resolve(answers.name);
|
|
228
530
|
const spinner = ora(`Creating spec directory at ${targetDir}...`).start();
|
|
229
531
|
|
|
230
532
|
try {
|
|
231
|
-
// Create directory structure
|
|
232
533
|
await mkdir(targetDir, { recursive: true });
|
|
233
|
-
await mkdir(join(targetDir, 'stdlib'), { recursive: true });
|
|
234
534
|
|
|
535
|
+
const sidecarPath = join(targetDir, SIDECAR_FILENAME);
|
|
536
|
+
if (await fileExists(sidecarPath)) {
|
|
537
|
+
spinner.stop();
|
|
538
|
+
console.log(chalk.yellow(`${SIDECAR_FILENAME} already exists in ${targetDir}.`));
|
|
539
|
+
console.log(chalk.gray('Run specmarket validate to check your spec, then specmarket publish to publish.'));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const hasFiles = await dirHasFiles(targetDir);
|
|
544
|
+
if (hasFiles) {
|
|
545
|
+
const detection = await detectSpecFormat(targetDir);
|
|
546
|
+
spinner.stop();
|
|
547
|
+
console.log(chalk.gray(`Directory already has files (detected: ${detection.format}). Adding ${SIDECAR_FILENAME} only; no files overwritten.`));
|
|
548
|
+
const metadata: MetadataData = {
|
|
549
|
+
displayName: answers.displayName,
|
|
550
|
+
description: `A ${answers.outputType} spec${answers.replacesSaas ? ` that replaces ${answers.replacesSaas}` : ''}.`,
|
|
551
|
+
replacesSaas: answers.replacesSaas || undefined,
|
|
552
|
+
outputType: answers.outputType,
|
|
553
|
+
primaryStack: answers.primaryStack,
|
|
554
|
+
};
|
|
555
|
+
const yaml = buildSpecmarketYaml({ ...metadata, specFormat: detection.format });
|
|
556
|
+
await writeFile(sidecarPath, yaml);
|
|
557
|
+
console.log('');
|
|
558
|
+
console.log(chalk.green(`Added ${SIDECAR_FILENAME} to ${targetDir}`));
|
|
559
|
+
console.log('');
|
|
560
|
+
console.log(chalk.bold('Next steps:'));
|
|
561
|
+
console.log(` 1. Run ${chalk.cyan('specmarket validate')} to check your spec`);
|
|
562
|
+
console.log(` 2. Run ${chalk.cyan('specmarket publish')} to publish to the marketplace`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
await mkdir(join(targetDir, 'stdlib'), { recursive: true });
|
|
235
567
|
const data = {
|
|
236
568
|
name: answers.name,
|
|
237
569
|
displayName: answers.displayName,
|
|
@@ -240,25 +572,24 @@ export async function handleInit(opts: {
|
|
|
240
572
|
primaryStack: answers.primaryStack,
|
|
241
573
|
};
|
|
242
574
|
|
|
243
|
-
// Write all template files
|
|
244
575
|
await Promise.all([
|
|
576
|
+
writeFile(sidecarPath, SPECMARKET_YAML_TEMPLATE(data)),
|
|
245
577
|
writeFile(join(targetDir, 'spec.yaml'), SPEC_YAML_TEMPLATE(data)),
|
|
246
578
|
writeFile(join(targetDir, 'PROMPT.md'), PROMPT_MD_TEMPLATE(data)),
|
|
247
579
|
writeFile(join(targetDir, 'SPEC.md'), SPEC_MD_TEMPLATE(data)),
|
|
248
580
|
writeFile(join(targetDir, 'SUCCESS_CRITERIA.md'), SUCCESS_CRITERIA_TEMPLATE),
|
|
249
581
|
writeFile(join(targetDir, 'stdlib', 'STACK.md'), STACK_MD_TEMPLATE(answers.primaryStack)),
|
|
250
|
-
writeFile(join(targetDir, '
|
|
582
|
+
writeFile(join(targetDir, 'TASKS.md'), TASKS_MD_TEMPLATE(answers.displayName)),
|
|
251
583
|
]);
|
|
252
584
|
|
|
253
585
|
spinner.succeed(chalk.green(`Spec created at ${targetDir}`));
|
|
254
|
-
|
|
255
586
|
console.log('');
|
|
256
587
|
console.log(chalk.bold('Next steps:'));
|
|
257
588
|
console.log(` 1. ${chalk.cyan(`cd ${answers.name}`)}`);
|
|
258
589
|
console.log(` 2. Edit ${chalk.cyan('SPEC.md')} with your application requirements`);
|
|
259
590
|
console.log(` 3. Edit ${chalk.cyan('SUCCESS_CRITERIA.md')} with specific pass/fail criteria`);
|
|
260
|
-
console.log(` 4. Run ${chalk.cyan(
|
|
261
|
-
console.log(` 5. Run ${chalk.cyan(
|
|
591
|
+
console.log(` 4. Run ${chalk.cyan('specmarket validate')} to check your spec`);
|
|
592
|
+
console.log(` 5. Run ${chalk.cyan('specmarket run')} to execute the spec`);
|
|
262
593
|
} catch (err) {
|
|
263
594
|
spinner.fail(chalk.red(`Failed to create spec: ${(err as Error).message}`));
|
|
264
595
|
throw err;
|
|
@@ -267,9 +598,12 @@ export async function handleInit(opts: {
|
|
|
267
598
|
|
|
268
599
|
export function createInitCommand(): Command {
|
|
269
600
|
return new Command('init')
|
|
270
|
-
.description(
|
|
601
|
+
.description(
|
|
602
|
+
'Create a new SpecMarket spec or add specmarket.yaml to an existing spec (Spec Kit, BMAD, Ralph). Use -p . to init in current directory without overwriting existing files.'
|
|
603
|
+
)
|
|
271
604
|
.option('-n, --name <name>', 'Spec name (skip prompt)')
|
|
272
|
-
.option('-p, --path <path>', 'Target directory
|
|
605
|
+
.option('-p, --path <path>', 'Target directory (e.g. . or ./my-spec). When set, detects existing format and adds only specmarket.yaml if present; no files overwritten.')
|
|
606
|
+
.option('--from <path>', 'Import an existing spec directory (Spec Kit, BMAD, Ralph, or custom markdown). Detects format and adds specmarket.yaml metadata sidecar without modifying original files. Errors if the directory is missing or empty.')
|
|
273
607
|
.action(async (opts) => {
|
|
274
608
|
try {
|
|
275
609
|
await handleInit(opts);
|