specdrive-cli 0.1.10 → 0.1.12
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 +955 -697
- package/agents/00-onboarding.md +261 -0
- package/agents/01-constitution.md +214 -201
- package/agents/02-specification.md +249 -226
- package/agents/03-uiux.md +156 -144
- package/agents/04-cascade.md +151 -122
- package/agents/05-discover-skills.md +136 -136
- package/agents/06-documentation.md +158 -145
- package/agents/07-implementation.md +201 -169
- package/agents/08-performance.md +179 -165
- package/agents/09-review-complete.md +239 -168
- package/agents/10-security.md +180 -167
- package/agents/11-test.md +195 -0
- package/commands/gates.js +73 -73
- package/commands/manifest.json +113 -95
- package/commands/permissions.json +39 -0
- package/commands/router.js +151 -127
- package/commands/tools.json +19 -19
- package/dashboard/app.js +394 -0
- package/dashboard/index.html +74 -0
- package/dashboard/server.js +166 -0
- package/dashboard/style.css +157 -0
- package/mcp/mcp.json +31 -0
- package/mcp/server.js +108 -0
- package/package.json +35 -32
- package/schemas/config.schema.json +20 -0
- package/schemas/workflow-state.schema.json +149 -38
- package/scripts/anti-redundancy.js +176 -176
- package/scripts/audit-log.js +46 -46
- package/scripts/check-permission.js +87 -0
- package/scripts/diff-spec.js +50 -50
- package/scripts/diff-version.js +96 -0
- package/scripts/generate-adapters.js +80 -80
- package/scripts/generate-from-template.js +97 -97
- package/scripts/generate-openapi.js +75 -75
- package/scripts/github-team-sync.js +80 -80
- package/scripts/install-hooks.js +20 -20
- package/scripts/load-plugins.js +65 -65
- package/scripts/migrate-openspec.js +318 -0
- package/scripts/migrate-speckit.js +322 -0
- package/scripts/migrate.js +12 -62
- package/scripts/onboard.js +312 -0
- package/scripts/pre-commit.js +56 -20
- package/scripts/team.js +113 -113
- package/scripts/test-adapters.js +118 -118
- package/scripts/test-create.js +13 -13
- package/scripts/test-end-to-end.js +137 -137
- package/scripts/test-router.js +110 -110
- package/scripts/test-state-transitions.js +146 -146
- package/scripts/test-validator.js +152 -152
- package/scripts/validate-config.js +36 -0
- package/scripts/validate-governance.js +150 -130
- package/scripts/verify.js +525 -0
- package/scripts/version-new.js +202 -0
- package/src/index.js +1010 -807
- package/templates/expo/plan.json +12 -0
- package/templates/expo/spec.json +12 -0
- package/templates/expo/tasks.json +5 -0
- package/templates/fastapi/plan.json +12 -0
- package/templates/fastapi/spec.json +12 -0
- package/templates/fastapi/tasks.json +5 -0
- package/templates/generic/plan.json +12 -0
- package/templates/generic/spec.json +11 -0
- package/templates/generic/tasks.json +5 -0
- package/templates/nextjs/plan.json +23 -0
- package/templates/nextjs/spec.json +12 -0
- package/templates/nextjs/tasks.json +5 -0
- package/templates/react-node/plan.json +15 -0
- package/templates/react-node/spec.json +12 -0
- package/templates/react-node/tasks.json +5 -0
- package/templates/registry.json +30 -0
- package/templates/turborepo/plan.json +12 -0
- package/templates/turborepo/spec.json +12 -0
- package/templates/turborepo/tasks.json +5 -0
package/src/index.js
CHANGED
|
@@ -1,808 +1,1011 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const { Command } = require("commander");
|
|
4
|
-
const { execSync } = require("child_process");
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
const os = require("os");
|
|
8
|
-
const readline = require("readline");
|
|
9
|
-
|
|
10
|
-
const program = new Command();
|
|
11
|
-
|
|
12
|
-
program
|
|
13
|
-
.name("sdrive")
|
|
14
|
-
.description("SpecDrive Enterprise Spec-Driven Development CLI")
|
|
15
|
-
.version("0.1.0");
|
|
16
|
-
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
path.join(sdriveRoot, "
|
|
212
|
-
path.join(sdriveRoot, "
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
path.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const mcpConfigPath = path.join(sdriveRoot, "mcp", "mcp.json");
|
|
282
|
-
if (!fs.existsSync(mcpConfigPath)) {
|
|
283
|
-
fs.writeFileSync(mcpConfigPath, mcpConfigContent());
|
|
284
|
-
console.log(" Created: .sdrive/mcp/mcp.json");
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
console.log("\n
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
console.log(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const templates = ["generic", "react-node", "nextjs", "expo", "fastapi", "turborepo"];
|
|
412
|
-
|
|
413
|
-
if (!templates.includes(name)) {
|
|
414
|
-
console.error(`❌ Invalid template. Run \`sdrive template:list\` to see available templates.`);
|
|
415
|
-
process.exit(1);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const configPath = path.join(process.cwd(), ".sdrive", "config.json");
|
|
419
|
-
|
|
420
|
-
if (!fs.existsSync(configPath)) {
|
|
421
|
-
console.error("❌ config.json not found. Run `sdrive init` first.");
|
|
422
|
-
process.exit(1);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const config =
|
|
426
|
-
config.defaultTemplate = name;
|
|
427
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
428
|
-
|
|
429
|
-
console.log(`✅ Default template set to: ${name}`);
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
// ---------- VALIDATE
|
|
433
|
-
program
|
|
434
|
-
.command("validate")
|
|
435
|
-
.description("Validate
|
|
436
|
-
.action(() => {
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
);
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
if (!
|
|
594
|
-
console.error(
|
|
595
|
-
process.exit(1);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
program
|
|
641
|
-
.command("team:
|
|
642
|
-
.description("
|
|
643
|
-
.action(() => {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
console.error("❌
|
|
705
|
-
process.exit(1);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
console.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
.
|
|
752
|
-
.
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
.
|
|
773
|
-
.
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require("commander");
|
|
4
|
+
const { execSync } = require("child_process");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const readline = require("readline");
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name("sdrive")
|
|
14
|
+
.description("SpecDrive Enterprise Spec-Driven Development CLI")
|
|
15
|
+
.version("0.1.0");
|
|
16
|
+
|
|
17
|
+
function copyRecursive(src, dest) {
|
|
18
|
+
if (!fs.existsSync(src)) return;
|
|
19
|
+
|
|
20
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
21
|
+
|
|
22
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
23
|
+
const srcPath = path.join(src, entry.name);
|
|
24
|
+
const destPath = path.join(dest, entry.name);
|
|
25
|
+
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
copyRecursive(srcPath, destPath);
|
|
28
|
+
} else {
|
|
29
|
+
if (!fs.existsSync(destPath)) {
|
|
30
|
+
fs.copyFileSync(srcPath, destPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requirePermission(cliCommand) {
|
|
37
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "check-permission.js");
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(scriptPath)) return;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
execSync(`node "${scriptPath}" cli "${cliCommand}"`, { stdio: "pipe" });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`❌ Permission denied for 'sdrive ${cliCommand}'.`);
|
|
45
|
+
if (error.stdout) console.error(error.stdout.toString());
|
|
46
|
+
if (error.stderr) console.error(error.stderr.toString());
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadConfig() {
|
|
52
|
+
const configPath = path.join(process.cwd(), ".sdrive", "config.json");
|
|
53
|
+
const schemaPath = path.join(process.cwd(), ".sdrive", "schemas", "config.schema.json");
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(configPath)) {
|
|
56
|
+
console.error("❌ config.json not found. Run `sdrive init` first.");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!fs.existsSync(schemaPath)) {
|
|
61
|
+
console.error("❌ config.schema.json not found. Run `sdrive init` first.");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const Ajv = require("ajv");
|
|
66
|
+
const ajv = new Ajv({ allErrors: true });
|
|
67
|
+
|
|
68
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
|
|
69
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
70
|
+
|
|
71
|
+
const valid = ajv.validate(schema, config);
|
|
72
|
+
|
|
73
|
+
if (!valid) {
|
|
74
|
+
console.error("❌ config.json is invalid:");
|
|
75
|
+
console.error(ajv.errors);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return config;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function adapterContent(toolName) {
|
|
83
|
+
return `# SpecDrive Commands
|
|
84
|
+
|
|
85
|
+
This project uses SpecDrive for spec-driven development.
|
|
86
|
+
|
|
87
|
+
## How to use
|
|
88
|
+
|
|
89
|
+
When the user types a /sdrive:* command, follow these steps:
|
|
90
|
+
|
|
91
|
+
1. Read .sdrive/commands/manifest.json
|
|
92
|
+
2. Find the matching command
|
|
93
|
+
3. Read the agent file listed in agentFile
|
|
94
|
+
4. Follow that agent's instructions exactly
|
|
95
|
+
5. Run validators after writing files
|
|
96
|
+
6. Stop at human approval gates
|
|
97
|
+
|
|
98
|
+
Always read the manifest first. Never hardcode the command list.
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mcpConfigContent() {
|
|
103
|
+
return JSON.stringify(
|
|
104
|
+
{
|
|
105
|
+
mcpServers: {
|
|
106
|
+
specdrive: {
|
|
107
|
+
command: "node",
|
|
108
|
+
args: [".sdrive/mcp/server.js"]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
null,
|
|
113
|
+
2
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function safeMergeMcpConfig(configPath, mcpContent) {
|
|
118
|
+
if (!fs.existsSync(configPath)) {
|
|
119
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
120
|
+
fs.writeFileSync(configPath, mcpContent);
|
|
121
|
+
console.log(`✅ Created: ${configPath}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const existing = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
127
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
128
|
+
|
|
129
|
+
if (existing.mcpServers.specdrive) {
|
|
130
|
+
console.log(`⚠️ SpecDrive already configured in ${configPath}`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
existing.mcpServers.specdrive = {
|
|
135
|
+
command: "node",
|
|
136
|
+
args: [".sdrive/mcp/server.js"]
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2));
|
|
140
|
+
console.log(`✅ Merged SpecDrive into: ${configPath}`);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`⚠️ Could not merge into ${configPath}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getMcpConfigPaths() {
|
|
147
|
+
const paths = [];
|
|
148
|
+
|
|
149
|
+
const platform = os.platform();
|
|
150
|
+
const home = os.homedir();
|
|
151
|
+
|
|
152
|
+
if (platform === "darwin") {
|
|
153
|
+
paths.push({
|
|
154
|
+
name: "Claude Desktop",
|
|
155
|
+
path: path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (platform === "win32") {
|
|
160
|
+
paths.push({
|
|
161
|
+
name: "Claude Desktop",
|
|
162
|
+
path: path.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json")
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
paths.push({
|
|
167
|
+
name: "Cursor",
|
|
168
|
+
path: path.join(process.cwd(), ".cursor", "mcp.json")
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
paths.push({
|
|
172
|
+
name: "Cline",
|
|
173
|
+
path: path.join(process.cwd(), ".cline", "mcp.json")
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
paths.push({
|
|
177
|
+
name: "VS Code",
|
|
178
|
+
path: path.join(process.cwd(), ".vscode", "mcp.json")
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return paths;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------- INIT COMMAND ----------
|
|
185
|
+
program
|
|
186
|
+
.command("init")
|
|
187
|
+
.description("Initialize SpecDrive structure, tool adapters, and default template")
|
|
188
|
+
.action(() => {
|
|
189
|
+
const projectRoot = process.cwd();
|
|
190
|
+
const sdriveRoot = path.join(projectRoot, ".sdrive");
|
|
191
|
+
|
|
192
|
+
const dirs = [
|
|
193
|
+
path.join(sdriveRoot, "agents"),
|
|
194
|
+
path.join(sdriveRoot, "commands"),
|
|
195
|
+
path.join(sdriveRoot, "schemas"),
|
|
196
|
+
path.join(sdriveRoot, "scripts"),
|
|
197
|
+
path.join(sdriveRoot, "dashboard"),
|
|
198
|
+
path.join(sdriveRoot, "mcp"),
|
|
199
|
+
path.join(sdriveRoot, "templates"),
|
|
200
|
+
path.join(sdriveRoot, "specs", "backlog"),
|
|
201
|
+
path.join(sdriveRoot, "specs", "ongoing"),
|
|
202
|
+
path.join(sdriveRoot, "specs", "completed"),
|
|
203
|
+
path.join(sdriveRoot, "governance"),
|
|
204
|
+
path.join(sdriveRoot, "prototype", "css"),
|
|
205
|
+
path.join(sdriveRoot, "prototype", "js"),
|
|
206
|
+
path.join(sdriveRoot, "prototype", "screens"),
|
|
207
|
+
path.join(sdriveRoot, "reports", "security"),
|
|
208
|
+
path.join(sdriveRoot, "reports", "performance"),
|
|
209
|
+
path.join(sdriveRoot, "reports", "docs"),
|
|
210
|
+
path.join(sdriveRoot, "reports", "tests"),
|
|
211
|
+
path.join(sdriveRoot, "context"),
|
|
212
|
+
path.join(sdriveRoot, "skills")
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
dirs.forEach((dir) => {
|
|
216
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
217
|
+
console.log(` Created: ${path.relative(projectRoot, dir)}`);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const packageRoot = path.join(__dirname, "..");
|
|
221
|
+
|
|
222
|
+
const dirsToCopy = [
|
|
223
|
+
"agents",
|
|
224
|
+
"schemas",
|
|
225
|
+
"scripts",
|
|
226
|
+
"commands",
|
|
227
|
+
"dashboard",
|
|
228
|
+
"mcp",
|
|
229
|
+
"templates"
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
dirsToCopy.forEach((dir) => {
|
|
233
|
+
const srcDir = path.join(packageRoot, dir);
|
|
234
|
+
const destDir = path.join(sdriveRoot, dir);
|
|
235
|
+
|
|
236
|
+
if (fs.existsSync(srcDir)) {
|
|
237
|
+
copyRecursive(srcDir, destDir);
|
|
238
|
+
console.log(` Copied: .sdrive/${dir}/`);
|
|
239
|
+
} else {
|
|
240
|
+
console.log(` ⚠️ Missing in package: ${dir}/`);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const workflowStatePath = path.join(sdriveRoot, "workflow-state.json");
|
|
245
|
+
if (!fs.existsSync(workflowStatePath)) {
|
|
246
|
+
fs.writeFileSync(workflowStatePath, JSON.stringify({ features: [] }, null, 2));
|
|
247
|
+
console.log(" Created: .sdrive/workflow-state.json");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const constitutionPath = path.join(sdriveRoot, "constitution.md");
|
|
251
|
+
if (!fs.existsSync(constitutionPath)) {
|
|
252
|
+
fs.writeFileSync(constitutionPath, "# SpecDrive Constitution\n\nInitialized by `sdrive init`.\n");
|
|
253
|
+
console.log(" Created: .sdrive/constitution.md");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const configPath = path.join(sdriveRoot, "config.json");
|
|
257
|
+
if (!fs.existsSync(configPath)) {
|
|
258
|
+
fs.writeFileSync(configPath, JSON.stringify({ defaultTemplate: "generic" }, null, 2));
|
|
259
|
+
console.log(" Created: .sdrive/config.json");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const teamPath = path.join(sdriveRoot, "team.json");
|
|
263
|
+
if (!fs.existsSync(teamPath)) {
|
|
264
|
+
fs.writeFileSync(
|
|
265
|
+
teamPath,
|
|
266
|
+
JSON.stringify(
|
|
267
|
+
{
|
|
268
|
+
members: [],
|
|
269
|
+
rules: {
|
|
270
|
+
approval_required: ["admin", "reviewer"],
|
|
271
|
+
implementation_allowed: ["developer", "admin"]
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
null,
|
|
275
|
+
2
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
console.log(" Created: .sdrive/team.json");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const mcpConfigPath = path.join(sdriveRoot, "mcp", "mcp.json");
|
|
282
|
+
if (!fs.existsSync(mcpConfigPath)) {
|
|
283
|
+
fs.writeFileSync(mcpConfigPath, mcpConfigContent());
|
|
284
|
+
console.log(" Created: .sdrive/mcp/mcp.json");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const toolsPath = path.join(sdriveRoot, "commands", "tools.json");
|
|
288
|
+
|
|
289
|
+
if (!fs.existsSync(toolsPath)) {
|
|
290
|
+
console.log("\n⚠️ tools.json not found. Skipping tool selection.");
|
|
291
|
+
console.log("\n✅ SpecDrive initialized successfully.");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const tools = JSON.parse(fs.readFileSync(toolsPath, "utf8")).tools;
|
|
296
|
+
|
|
297
|
+
console.log("\n🔧 Select tools to set up (comma-separated numbers):\n");
|
|
298
|
+
tools.forEach((tool, index) => {
|
|
299
|
+
console.log(` ${index + 1}. ${tool.name}`);
|
|
300
|
+
});
|
|
301
|
+
console.log(" Or press Enter to generate all.\n");
|
|
302
|
+
|
|
303
|
+
const rl = readline.createInterface({
|
|
304
|
+
input: process.stdin,
|
|
305
|
+
output: process.stdout
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
rl.question("Selection: ", (answer) => {
|
|
309
|
+
rl.close();
|
|
310
|
+
|
|
311
|
+
let selectedTools = tools;
|
|
312
|
+
|
|
313
|
+
if (answer.trim() !== "") {
|
|
314
|
+
const selectedIndexes = answer.split(",").map((n) => parseInt(n.trim()) - 1);
|
|
315
|
+
selectedTools = selectedIndexes
|
|
316
|
+
.filter((i) => i >= 0 && i < tools.length)
|
|
317
|
+
.map((i) => tools[i]);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
selectedTools.forEach((tool) => {
|
|
321
|
+
const adapterFullPath = path.join(projectRoot, tool.adapterPath);
|
|
322
|
+
const adapterDir = path.dirname(adapterFullPath);
|
|
323
|
+
|
|
324
|
+
if (!fs.existsSync(adapterDir)) {
|
|
325
|
+
fs.mkdirSync(adapterDir, { recursive: true });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (fs.existsSync(adapterFullPath)) {
|
|
329
|
+
console.log(`⚠️ Skipped existing: ${tool.adapterPath}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fs.writeFileSync(adapterFullPath, adapterContent(tool.name));
|
|
334
|
+
console.log(`✅ Generated: ${tool.adapterPath}`);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const templates = ["generic", "react-node", "nextjs", "expo", "fastapi", "turborepo"];
|
|
338
|
+
|
|
339
|
+
console.log("\n📦 Select default template:\n");
|
|
340
|
+
templates.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
|
|
341
|
+
|
|
342
|
+
const rl2 = readline.createInterface({
|
|
343
|
+
input: process.stdin,
|
|
344
|
+
output: process.stdout
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
rl2.question("Template (default: 1): ", (templateAnswer) => {
|
|
348
|
+
rl2.close();
|
|
349
|
+
|
|
350
|
+
const idx = templateAnswer.trim() === "" ? 0 : parseInt(templateAnswer.trim()) - 1;
|
|
351
|
+
const selectedTemplate = templates[idx] || "generic";
|
|
352
|
+
|
|
353
|
+
const cfgPath = path.join(sdriveRoot, "config.json");
|
|
354
|
+
fs.writeFileSync(cfgPath, JSON.stringify({ defaultTemplate: selectedTemplate }, null, 2));
|
|
355
|
+
|
|
356
|
+
console.log(`\n✅ Default template set to: ${selectedTemplate}`);
|
|
357
|
+
|
|
358
|
+
const mcpPaths = getMcpConfigPaths();
|
|
359
|
+
|
|
360
|
+
console.log("\n🔌 MCP Client Wiring\n");
|
|
361
|
+
mcpPaths.forEach((p, i) => {
|
|
362
|
+
console.log(` ${i + 1}. ${p.name} (${p.path})`);
|
|
363
|
+
});
|
|
364
|
+
console.log(" Or press Enter to skip MCP wiring.\n");
|
|
365
|
+
|
|
366
|
+
const rl3 = readline.createInterface({
|
|
367
|
+
input: process.stdin,
|
|
368
|
+
output: process.stdout
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
rl3.question("Wire MCP clients: ", (mcpAnswer) => {
|
|
372
|
+
rl3.close();
|
|
373
|
+
|
|
374
|
+
if (mcpAnswer.trim() === "") {
|
|
375
|
+
console.log("\n⚠️ MCP wiring skipped.");
|
|
376
|
+
console.log("\n✅ SpecDrive initialized successfully.");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const selectedIndexes = mcpAnswer.split(",").map((n) => parseInt(n.trim()) - 1);
|
|
381
|
+
const selectedPaths = selectedIndexes
|
|
382
|
+
.filter((i) => i >= 0 && i < mcpPaths.length)
|
|
383
|
+
.map((i) => mcpPaths[i]);
|
|
384
|
+
|
|
385
|
+
selectedPaths.forEach((p) => {
|
|
386
|
+
safeMergeMcpConfig(p.path, mcpConfigContent());
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
console.log("\n✅ SpecDrive initialized successfully.");
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ---------- TEMPLATE COMMANDS ----------
|
|
396
|
+
program
|
|
397
|
+
.command("template:list")
|
|
398
|
+
.description("List available templates")
|
|
399
|
+
.action(() => {
|
|
400
|
+
const templates = ["generic", "react-node", "nextjs", "expo", "fastapi", "turborepo"];
|
|
401
|
+
console.log("\nAvailable Templates\n");
|
|
402
|
+
templates.forEach((t) => console.log(` ${t}`));
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
program
|
|
406
|
+
.command("template:set <name>")
|
|
407
|
+
.description("Set default template")
|
|
408
|
+
.action((name) => {
|
|
409
|
+
requirePermission("template:set");
|
|
410
|
+
|
|
411
|
+
const templates = ["generic", "react-node", "nextjs", "expo", "fastapi", "turborepo"];
|
|
412
|
+
|
|
413
|
+
if (!templates.includes(name)) {
|
|
414
|
+
console.error(`❌ Invalid template. Run \`sdrive template:list\` to see available templates.`);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const configPath = path.join(process.cwd(), ".sdrive", "config.json");
|
|
419
|
+
|
|
420
|
+
if (!fs.existsSync(configPath)) {
|
|
421
|
+
console.error("❌ config.json not found. Run `sdrive init` first.");
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const config = loadConfig();
|
|
426
|
+
config.defaultTemplate = name;
|
|
427
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
428
|
+
|
|
429
|
+
console.log(`✅ Default template set to: ${name}`);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ---------- CONFIG VALIDATE ----------
|
|
433
|
+
program
|
|
434
|
+
.command("config:validate")
|
|
435
|
+
.description("Validate .sdrive/config.json")
|
|
436
|
+
.action(() => {
|
|
437
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "validate-config.js");
|
|
438
|
+
|
|
439
|
+
if (!fs.existsSync(scriptPath)) {
|
|
440
|
+
console.error("❌ validate-config.js not found. Run `sdrive init` first.");
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
execSync(`node "${scriptPath}"`, { stdio: "inherit" });
|
|
446
|
+
} catch (error) {
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ---------- VALIDATE COMMAND ----------
|
|
452
|
+
program
|
|
453
|
+
.command("validate")
|
|
454
|
+
.description("Validate SpecDrive governance files")
|
|
455
|
+
.action(() => {
|
|
456
|
+
const projectRoot = process.cwd();
|
|
457
|
+
const validatorPath = path.join(projectRoot, ".sdrive", "scripts", "validate-governance.js");
|
|
458
|
+
|
|
459
|
+
if (!fs.existsSync(validatorPath)) {
|
|
460
|
+
console.error("❌ Validator not found. Run `sdrive init` first.");
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
execSync(`node "${validatorPath}"`, { stdio: "inherit" });
|
|
466
|
+
} catch (error) {
|
|
467
|
+
console.error("\n❌ Validation failed.");
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// ---------- VERIFY COMMAND ----------
|
|
473
|
+
program
|
|
474
|
+
.command("verify <feature>")
|
|
475
|
+
.description("Verify code matches the plan (spec-to-code diff)")
|
|
476
|
+
.action((feature) => {
|
|
477
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "verify.js");
|
|
478
|
+
|
|
479
|
+
if (!fs.existsSync(scriptPath)) {
|
|
480
|
+
console.error("❌ verify.js not found. Run `sdrive init` first.");
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
execSync(`node "${scriptPath}" "${feature}"`, { stdio: "inherit" });
|
|
486
|
+
} catch (error) {
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ---------- CREATE COMMAND ----------
|
|
492
|
+
program
|
|
493
|
+
.command("create <feature>")
|
|
494
|
+
.description("Create a new feature scaffold (internal)")
|
|
495
|
+
.action((feature) => {
|
|
496
|
+
if (!/^[a-z0-9-]+$/.test(feature)) {
|
|
497
|
+
console.error("❌ Feature name must be lowercase letters, numbers, and hyphens only.");
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const projectRoot = process.cwd();
|
|
502
|
+
const featureSpecDir = path.join(projectRoot, ".sdrive", "specs", "backlog", feature);
|
|
503
|
+
const featureGovDir = path.join(projectRoot, ".sdrive", "governance", feature);
|
|
504
|
+
|
|
505
|
+
if (fs.existsSync(featureSpecDir) || fs.existsSync(featureGovDir)) {
|
|
506
|
+
console.error(`❌ Feature "${feature}" already exists.`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
fs.mkdirSync(featureSpecDir, { recursive: true });
|
|
511
|
+
fs.mkdirSync(featureGovDir, { recursive: true });
|
|
512
|
+
|
|
513
|
+
fs.writeFileSync(
|
|
514
|
+
path.join(featureSpecDir, "spec.md"),
|
|
515
|
+
`---\ntitle: ${feature}\nstatus: backlog\ncreated: ${new Date().toISOString()}\nupdated: ${new Date().toISOString()}\n---\n\n# ${feature}\n`
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
fs.writeFileSync(path.join(featureSpecDir, "plan.md"), `# Technical Plan: ${feature}\n`);
|
|
519
|
+
fs.writeFileSync(path.join(featureSpecDir, "tasks.md"), `# Execution Tasks: ${feature}\n`);
|
|
520
|
+
|
|
521
|
+
fs.writeFileSync(
|
|
522
|
+
path.join(featureGovDir, "spec.json"),
|
|
523
|
+
JSON.stringify({ feature, title: feature, version: 1, overview: {}, userStories: [], functionalRequirements: [] }, null, 2)
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
fs.writeFileSync(
|
|
527
|
+
path.join(featureGovDir, "plan.json"),
|
|
528
|
+
JSON.stringify({ feature, version: 1, architecture: {}, dataModel: {} }, null, 2)
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
fs.writeFileSync(
|
|
532
|
+
path.join(featureGovDir, "tasks.json"),
|
|
533
|
+
JSON.stringify({ feature, version: 1, phases: [] }, null, 2)
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
fs.writeFileSync(
|
|
537
|
+
path.join(featureGovDir, "traceability.json"),
|
|
538
|
+
JSON.stringify({ feature, version: 1, rows: [] }, null, 2)
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
console.log(`✅ Feature "${feature}" created in backlog.`);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// ---------- STATUS COMMAND ----------
|
|
545
|
+
program
|
|
546
|
+
.command("status")
|
|
547
|
+
.description("Show feature lifecycle state")
|
|
548
|
+
.action(() => {
|
|
549
|
+
const projectRoot = process.cwd();
|
|
550
|
+
const workflowStatePath = path.join(projectRoot, ".sdrive", "workflow-state.json");
|
|
551
|
+
|
|
552
|
+
if (!fs.existsSync(workflowStatePath)) {
|
|
553
|
+
console.error("❌ workflow-state.json not found. Run `sdrive init` first.");
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const state = JSON.parse(fs.readFileSync(workflowStatePath, "utf8"));
|
|
558
|
+
|
|
559
|
+
if (!state.features || state.features.length === 0) {
|
|
560
|
+
console.log("No features found.");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
console.log("\nSpecDrive Feature Status\n");
|
|
565
|
+
|
|
566
|
+
state.features.forEach((f) => {
|
|
567
|
+
console.log(`📁 ${f.name}`);
|
|
568
|
+
console.log(` Current Version: ${f.currentVersion}`);
|
|
569
|
+
|
|
570
|
+
(f.versions || []).forEach((v) => {
|
|
571
|
+
console.log(` Version ${v.id}:`);
|
|
572
|
+
console.log(` Phase: ${v.phase}`);
|
|
573
|
+
console.log(` Current Step: ${v.currentStep}`);
|
|
574
|
+
console.log(` Gates:`);
|
|
575
|
+
console.log(` specification: ${v.gates?.specification ?? "pending"}`);
|
|
576
|
+
console.log(` planning: ${v.gates?.planning ?? "pending"}`);
|
|
577
|
+
console.log(` implementation: ${v.gates?.implementation ?? "pending"}`);
|
|
578
|
+
console.log(` review: ${v.gates?.review ?? "pending"}`);
|
|
579
|
+
});
|
|
580
|
+
console.log("");
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// ---------- APPROVE COMMAND ----------
|
|
585
|
+
program
|
|
586
|
+
.command("approve <gate>")
|
|
587
|
+
.description("Approve a workflow gate (specification, planning, implementation, review)")
|
|
588
|
+
.action((gate) => {
|
|
589
|
+
requirePermission("approve");
|
|
590
|
+
|
|
591
|
+
const allowedGates = ["specification", "planning", "implementation", "review"];
|
|
592
|
+
|
|
593
|
+
if (!allowedGates.includes(gate)) {
|
|
594
|
+
console.error(`❌ Invalid gate. Allowed: ${allowedGates.join(", ")}`);
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const projectRoot = process.cwd();
|
|
599
|
+
const workflowStatePath = path.join(projectRoot, ".sdrive", "workflow-state.json");
|
|
600
|
+
|
|
601
|
+
if (!fs.existsSync(workflowStatePath)) {
|
|
602
|
+
console.error("❌ workflow-state.json not found. Run `sdrive init` first.");
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const state = JSON.parse(fs.readFileSync(workflowStatePath, "utf8"));
|
|
607
|
+
|
|
608
|
+
if (!state.features || state.features.length === 0) {
|
|
609
|
+
console.error("❌ No features found.");
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const feature = state.features[0];
|
|
614
|
+
const versionId = feature.currentVersion;
|
|
615
|
+
|
|
616
|
+
if (!feature.versions || feature.versions.length === 0) {
|
|
617
|
+
console.error("❌ No versions found.");
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const version = feature.versions.find((v) => v.id === versionId) || feature.versions[0];
|
|
622
|
+
|
|
623
|
+
if (!version.gates) {
|
|
624
|
+
version.gates = {};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
version.gates[gate] = "approved";
|
|
628
|
+
version.history = version.history || [];
|
|
629
|
+
version.history.push({
|
|
630
|
+
event: `${gate}_approved`,
|
|
631
|
+
timestamp: new Date().toISOString()
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
fs.writeFileSync(workflowStatePath, JSON.stringify(state, null, 2));
|
|
635
|
+
|
|
636
|
+
console.log(`✅ Gate "${gate}" approved for feature "${feature.name}" version "${version.id}".`);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// ---------- TEAM COMMANDS ----------
|
|
640
|
+
program
|
|
641
|
+
.command("team:add <username> <role>")
|
|
642
|
+
.description("Add a new team member")
|
|
643
|
+
.action((username, role) => {
|
|
644
|
+
requirePermission("team:add");
|
|
645
|
+
|
|
646
|
+
const teamPath = path.join(process.cwd(), ".sdrive", "team.json");
|
|
647
|
+
|
|
648
|
+
if (!fs.existsSync(teamPath)) {
|
|
649
|
+
console.error("❌ team.json not found. Run `sdrive init` first.");
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const team = JSON.parse(fs.readFileSync(teamPath, "utf8"));
|
|
654
|
+
|
|
655
|
+
if (!team.members) team.members = [];
|
|
656
|
+
|
|
657
|
+
const existing = team.members.find((m) => m.username === username);
|
|
658
|
+
if (existing) {
|
|
659
|
+
console.error(`❌ User "${username}" already exists. Use team:update to change role.`);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
team.members.push({ username, role });
|
|
664
|
+
fs.writeFileSync(teamPath, JSON.stringify(team, null, 2));
|
|
665
|
+
|
|
666
|
+
console.log(`✅ Added ${username} as ${role}.`);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
program
|
|
670
|
+
.command("team:remove <username>")
|
|
671
|
+
.description("Remove a team member")
|
|
672
|
+
.action((username) => {
|
|
673
|
+
requirePermission("team:remove");
|
|
674
|
+
|
|
675
|
+
const teamPath = path.join(process.cwd(), ".sdrive", "team.json");
|
|
676
|
+
|
|
677
|
+
if (!fs.existsSync(teamPath)) {
|
|
678
|
+
console.error("❌ team.json not found. Run `sdrive init` first.");
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const team = JSON.parse(fs.readFileSync(teamPath, "utf8"));
|
|
683
|
+
|
|
684
|
+
const before = team.members?.length || 0;
|
|
685
|
+
team.members = (team.members || []).filter((m) => m.username !== username);
|
|
686
|
+
const after = team.members.length;
|
|
687
|
+
|
|
688
|
+
if (before === after) {
|
|
689
|
+
console.error(`❌ User "${username}" not found.`);
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
fs.writeFileSync(teamPath, JSON.stringify(team, null, 2));
|
|
694
|
+
console.log(`✅ Removed ${username}.`);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
program
|
|
698
|
+
.command("team:list")
|
|
699
|
+
.description("List all team members")
|
|
700
|
+
.action(() => {
|
|
701
|
+
const teamPath = path.join(process.cwd(), ".sdrive", "team.json");
|
|
702
|
+
|
|
703
|
+
if (!fs.existsSync(teamPath)) {
|
|
704
|
+
console.error("❌ team.json not found. Run `sdrive init` first.");
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const team = JSON.parse(fs.readFileSync(teamPath, "utf8"));
|
|
709
|
+
|
|
710
|
+
if (!team.members || team.members.length === 0) {
|
|
711
|
+
console.log("No team members found.");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
console.log("\nSpecDrive Team Members\n");
|
|
716
|
+
team.members.forEach((m) => {
|
|
717
|
+
console.log(`👤 ${m.username} — ${m.role}`);
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
program
|
|
722
|
+
.command("team:update <username> <role>")
|
|
723
|
+
.description("Update a team member's role")
|
|
724
|
+
.action((username, role) => {
|
|
725
|
+
requirePermission("team:update");
|
|
726
|
+
|
|
727
|
+
const teamPath = path.join(process.cwd(), ".sdrive", "team.json");
|
|
728
|
+
|
|
729
|
+
if (!fs.existsSync(teamPath)) {
|
|
730
|
+
console.error("❌ team.json not found. Run `sdrive init` first.");
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const team = JSON.parse(fs.readFileSync(teamPath, "utf8"));
|
|
735
|
+
|
|
736
|
+
const member = team.members?.find((m) => m.username === username);
|
|
737
|
+
|
|
738
|
+
if (!member) {
|
|
739
|
+
console.error(`❌ User "${username}" not found.`);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
member.role = role;
|
|
744
|
+
fs.writeFileSync(teamPath, JSON.stringify(team, null, 2));
|
|
745
|
+
|
|
746
|
+
console.log(`✅ Updated ${username} to ${role}.`);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
program
|
|
750
|
+
.command("team:sync")
|
|
751
|
+
.description("Sync team members from GitHub collaborators")
|
|
752
|
+
.action(() => {
|
|
753
|
+
requirePermission("team:sync");
|
|
754
|
+
|
|
755
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "github-team-sync.js");
|
|
756
|
+
|
|
757
|
+
if (!fs.existsSync(scriptPath)) {
|
|
758
|
+
console.error("❌ github-team-sync.js not found. Run `sdrive init` first.");
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
try {
|
|
763
|
+
execSync(`node "${scriptPath}"`, { stdio: "inherit" });
|
|
764
|
+
} catch (error) {
|
|
765
|
+
console.error("❌ Team sync failed.");
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// ---------- HOOKS ----------
|
|
771
|
+
program
|
|
772
|
+
.command("hooks:install")
|
|
773
|
+
.description("Install SpecDrive pre-commit hook")
|
|
774
|
+
.action(() => {
|
|
775
|
+
requirePermission("hooks:install");
|
|
776
|
+
|
|
777
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "install-hooks.js");
|
|
778
|
+
|
|
779
|
+
if (!fs.existsSync(scriptPath)) {
|
|
780
|
+
console.error("❌ install-hooks.js not found. Run `sdrive init` first.");
|
|
781
|
+
process.exit(1);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
try {
|
|
785
|
+
execSync(`node "${scriptPath}"`, { stdio: "inherit" });
|
|
786
|
+
} catch (error) {
|
|
787
|
+
console.error("❌ Hook installation failed.");
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// ---------- DIFF ----------
|
|
793
|
+
program
|
|
794
|
+
.command("diff <oldFile> <newFile>")
|
|
795
|
+
.description("Show differences between two spec files")
|
|
796
|
+
.action((oldFile, newFile) => {
|
|
797
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "diff-spec.js");
|
|
798
|
+
|
|
799
|
+
if (!fs.existsSync(scriptPath)) {
|
|
800
|
+
console.error("❌ diff-spec.js not found. Run `sdrive init` first.");
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
execSync(`node "${scriptPath}" "${oldFile}" "${newFile}"`, { stdio: "inherit" });
|
|
806
|
+
} catch (error) {
|
|
807
|
+
console.error("❌ Diff failed.");
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// ---------- OPENAPI ----------
|
|
813
|
+
program
|
|
814
|
+
.command("openapi:generate <feature>")
|
|
815
|
+
.description("Generate OpenAPI spec from plan.json API contracts")
|
|
816
|
+
.action((feature) => {
|
|
817
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "generate-openapi.js");
|
|
818
|
+
|
|
819
|
+
if (!fs.existsSync(scriptPath)) {
|
|
820
|
+
console.error("❌ generate-openapi.js not found. Run `sdrive init` first.");
|
|
821
|
+
process.exit(1);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
execSync(`node "${scriptPath}" "${feature}"`, { stdio: "inherit" });
|
|
826
|
+
} catch (error) {
|
|
827
|
+
console.error("❌ OpenAPI generation failed.");
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// ---------- DASHBOARD ----------
|
|
833
|
+
program
|
|
834
|
+
.command("dashboard")
|
|
835
|
+
.description("Start SpecDrive web dashboard")
|
|
836
|
+
.action(() => {
|
|
837
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "dashboard", "server.js");
|
|
838
|
+
|
|
839
|
+
if (!fs.existsSync(scriptPath)) {
|
|
840
|
+
console.error("❌ Dashboard not found. Run `sdrive init` first.");
|
|
841
|
+
process.exit(1);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
try {
|
|
845
|
+
execSync(`node "${scriptPath}"`, { stdio: "inherit" });
|
|
846
|
+
} catch (error) {
|
|
847
|
+
console.error("❌ Dashboard failed to start.");
|
|
848
|
+
process.exit(1);
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// ---------- REOPEN ----------
|
|
853
|
+
program
|
|
854
|
+
.command("reopen <feature>")
|
|
855
|
+
.description("Reopen a completed feature")
|
|
856
|
+
.action((feature) => {
|
|
857
|
+
requirePermission("reopen");
|
|
858
|
+
|
|
859
|
+
const projectRoot = process.cwd();
|
|
860
|
+
const sdriveRoot = path.join(projectRoot, ".sdrive");
|
|
861
|
+
|
|
862
|
+
const completedSpecDir = path.join(sdriveRoot, "specs", "completed", feature);
|
|
863
|
+
const ongoingSpecDir = path.join(sdriveRoot, "specs", "ongoing", feature);
|
|
864
|
+
|
|
865
|
+
const completedGovDir = path.join(sdriveRoot, "governance", "completed", feature);
|
|
866
|
+
const ongoingGovDir = path.join(sdriveRoot, "governance", feature);
|
|
867
|
+
|
|
868
|
+
if (fs.existsSync(completedSpecDir)) {
|
|
869
|
+
fs.mkdirSync(path.dirname(ongoingSpecDir), { recursive: true });
|
|
870
|
+
fs.renameSync(completedSpecDir, ongoingSpecDir);
|
|
871
|
+
console.log(`✅ Moved spec: completed/${feature} → ongoing/${feature}`);
|
|
872
|
+
} else if (!fs.existsSync(ongoingSpecDir)) {
|
|
873
|
+
console.error(`❌ Feature "${feature}" not found in completed/ or ongoing/.`);
|
|
874
|
+
process.exit(1);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (fs.existsSync(completedGovDir)) {
|
|
878
|
+
fs.mkdirSync(path.dirname(ongoingGovDir), { recursive: true });
|
|
879
|
+
fs.renameSync(completedGovDir, ongoingGovDir);
|
|
880
|
+
console.log(`✅ Moved governance: completed/${feature} → ${feature}`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const workflowStatePath = path.join(sdriveRoot, "workflow-state.json");
|
|
884
|
+
|
|
885
|
+
if (!fs.existsSync(workflowStatePath)) {
|
|
886
|
+
console.error("❌ workflow-state.json not found.");
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const state = JSON.parse(fs.readFileSync(workflowStatePath, "utf8"));
|
|
891
|
+
const target = state.features?.find((f) => f.name === feature);
|
|
892
|
+
|
|
893
|
+
if (!target) {
|
|
894
|
+
console.error(`❌ Feature "${feature}" not found in workflow-state.json.`);
|
|
895
|
+
process.exit(1);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (!target.versions || target.versions.length === 0) {
|
|
899
|
+
console.error(`❌ Feature "${feature}" has no versions.`);
|
|
900
|
+
process.exit(1);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const currentVersion = target.versions.find((v) => v.id === target.currentVersion);
|
|
904
|
+
|
|
905
|
+
if (currentVersion) {
|
|
906
|
+
currentVersion.phase = "ongoing";
|
|
907
|
+
currentVersion.currentStep = "specification";
|
|
908
|
+
currentVersion.gates.review = "pending";
|
|
909
|
+
currentVersion.history = currentVersion.history || [];
|
|
910
|
+
currentVersion.history.push({
|
|
911
|
+
event: "feature_reopened",
|
|
912
|
+
timestamp: new Date().toISOString()
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
fs.writeFileSync(workflowStatePath, JSON.stringify(state, null, 2));
|
|
917
|
+
|
|
918
|
+
console.log(`✅ Feature "${feature}" reopened.`);
|
|
919
|
+
console.log(" Review gate reset to pending.");
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// ---------- ONBOARD COMMAND ----------
|
|
923
|
+
// Internal helper. Usually run automatically by the Onboarding Agent.
|
|
924
|
+
program
|
|
925
|
+
.command("onboard")
|
|
926
|
+
.description("Scan an existing project for onboarding (usually run automatically by /sdrive:onboard)")
|
|
927
|
+
.action(() => {
|
|
928
|
+
requirePermission("onboard");
|
|
929
|
+
|
|
930
|
+
const projectRoot = process.cwd();
|
|
931
|
+
const sdriveRoot = path.join(projectRoot, ".sdrive");
|
|
932
|
+
const workflowStatePath = path.join(sdriveRoot, "workflow-state.json");
|
|
933
|
+
|
|
934
|
+
// Guard: refuse to run twice
|
|
935
|
+
if (fs.existsSync(workflowStatePath)) {
|
|
936
|
+
try {
|
|
937
|
+
const state = JSON.parse(fs.readFileSync(workflowStatePath, "utf8"));
|
|
938
|
+
|
|
939
|
+
if (state.onboarding && state.onboarding.date) {
|
|
940
|
+
console.error(
|
|
941
|
+
`❌ Onboarding already ran on ${state.onboarding.date}. Refusing to re-run.`
|
|
942
|
+
);
|
|
943
|
+
console.error(" If you want to re-run, remove the 'onboarding' block from workflow-state.json.");
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
} catch (error) {
|
|
947
|
+
// ignore parse errors and continue
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const scriptPath = path.join(sdriveRoot, "scripts", "onboard.js");
|
|
952
|
+
|
|
953
|
+
if (!fs.existsSync(scriptPath)) {
|
|
954
|
+
console.error("❌ onboard.js not found. Run `sdrive init` first.");
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
try {
|
|
959
|
+
execSync(`node "${scriptPath}"`, { stdio: "inherit" });
|
|
960
|
+
|
|
961
|
+
console.log("");
|
|
962
|
+
console.log("📋 The scan is complete.");
|
|
963
|
+
console.log(" You can now run `/sdrive:onboard` in your AI tool,");
|
|
964
|
+
console.log(" or the agent will run this scan for you if needed.");
|
|
965
|
+
} catch (error) {
|
|
966
|
+
console.error("❌ Onboarding scan failed.");
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// ---------- VERSION NEW ----------
|
|
972
|
+
program
|
|
973
|
+
.command("version:new <feature>")
|
|
974
|
+
.description("Create a new version of a feature")
|
|
975
|
+
.action((feature) => {
|
|
976
|
+
requirePermission("version:new");
|
|
977
|
+
|
|
978
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "version-new.js");
|
|
979
|
+
|
|
980
|
+
if (!fs.existsSync(scriptPath)) {
|
|
981
|
+
console.error("❌ version-new.js not found. Run `sdrive init` first.");
|
|
982
|
+
process.exit(1);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
try {
|
|
986
|
+
execSync(`node "${scriptPath}" "${feature}"`, { stdio: "inherit" });
|
|
987
|
+
} catch (error) {
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
// ---------- DIFF VERSION ----------
|
|
993
|
+
program
|
|
994
|
+
.command("diff:version <feature> <versionA> <versionB>")
|
|
995
|
+
.description("Compare two versions of a feature")
|
|
996
|
+
.action((feature, versionA, versionB) => {
|
|
997
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "diff-version.js");
|
|
998
|
+
|
|
999
|
+
if (!fs.existsSync(scriptPath)) {
|
|
1000
|
+
console.error("❌ diff-version.js not found. Run `sdrive init` first.");
|
|
1001
|
+
process.exit(1);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
try {
|
|
1005
|
+
execSync(`node "${scriptPath}" "${feature}" "${versionA}" "${versionB}"`, { stdio: "inherit" });
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
process.exit(1);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
|
|
808
1011
|
program.parse(process.argv);
|