specdrive-cli 0.1.9 → 0.1.10
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 +192 -1
- package/package.json +1 -1
- package/schemas/workflow-state.schema.json +6 -0
- package/scripts/diff-spec.js +51 -0
- package/scripts/generate-from-template.js +27 -0
- package/scripts/generate-openapi.js +76 -0
- package/scripts/github-team-sync.js +81 -0
- package/scripts/install-hooks.js +21 -0
- package/scripts/migrate.js +62 -0
- package/src/index.js +424 -17
package/README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
---
|
|
1
2
|
|
|
2
3
|
```markdown
|
|
3
4
|
# SpecDrive
|
|
@@ -29,10 +30,13 @@ It adds:
|
|
|
29
30
|
- [Optional Commands](#optional-commands)
|
|
30
31
|
- [Terminal Commands](#terminal-commands)
|
|
31
32
|
- [Agents](#agents)
|
|
33
|
+
- [Templates](#templates)
|
|
32
34
|
- [Directory Structure](#directory-structure)
|
|
33
35
|
- [Validation](#validation)
|
|
34
36
|
- [CI/CD Integration](#cicd-integration)
|
|
37
|
+
- [MCP Server](#mcp-server)
|
|
35
38
|
- [Tool Support](#tool-support)
|
|
39
|
+
- [Web Dashboard](#web-dashboard)
|
|
36
40
|
- [Team Collaboration](#team-collaboration)
|
|
37
41
|
- [Contributing](#contributing)
|
|
38
42
|
- [License](#license)
|
|
@@ -348,6 +352,13 @@ Audits and manages AI skills.
|
|
|
348
352
|
| `sdrive team:remove <username>` | Remove a team member |
|
|
349
353
|
| `sdrive team:list` | List all team members |
|
|
350
354
|
| `sdrive team:update <username> <role>` | Update a member's role |
|
|
355
|
+
| `sdrive template:list` | List available templates |
|
|
356
|
+
| `sdrive template:set <name>` | Set default template |
|
|
357
|
+
| `sdrive hooks:install` | Install pre-commit validation hook |
|
|
358
|
+
| `sdrive migrate` | Migrate specs from OpenSpec or Spec-Kit |
|
|
359
|
+
| `sdrive diff <old> <new>` | Show differences between two spec files |
|
|
360
|
+
| `sdrive openapi:generate <feature>` | Generate OpenAPI spec from plan.json |
|
|
361
|
+
| `sdrive team:sync` | Sync team members from GitHub collaborators |
|
|
351
362
|
|
|
352
363
|
---
|
|
353
364
|
|
|
@@ -368,6 +379,30 @@ Audits and manages AI skills.
|
|
|
368
379
|
|
|
369
380
|
---
|
|
370
381
|
|
|
382
|
+
## Templates
|
|
383
|
+
|
|
384
|
+
SpecDrive includes starter templates for common stacks.
|
|
385
|
+
|
|
386
|
+
| Template | Stack |
|
|
387
|
+
|----------|-------|
|
|
388
|
+
| `generic` | Any project |
|
|
389
|
+
| `react-node` | React + Node |
|
|
390
|
+
| `nextjs` | Next.js |
|
|
391
|
+
| `expo` | Expo / React Native |
|
|
392
|
+
| `fastapi` | Python FastAPI |
|
|
393
|
+
| `turborepo` | Turborepo monorepo |
|
|
394
|
+
|
|
395
|
+
The default template is stored in `.sdrive/config.json`.
|
|
396
|
+
|
|
397
|
+
Manage templates:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
sdrive template:list
|
|
401
|
+
sdrive template:set nextjs
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
371
406
|
## Directory Structure
|
|
372
407
|
|
|
373
408
|
```text
|
|
@@ -380,10 +415,13 @@ Audits and manages AI skills.
|
|
|
380
415
|
│ ├── ongoing/
|
|
381
416
|
│ └── completed/
|
|
382
417
|
├── governance/ # Machine-readable JSON
|
|
418
|
+
├── mcp/ # MCP server for AI chat integration
|
|
383
419
|
├── prototype/ # UI/UX prototypes
|
|
384
420
|
├── reports/ # Security, performance, docs reports
|
|
385
421
|
├── context/ # Discovered project context
|
|
422
|
+
├── dashboard/ # Local web dashboard
|
|
386
423
|
├── skills/ # AI skills
|
|
424
|
+
├── templates/ # Starter templates
|
|
387
425
|
├── constitution.md # Governance source of truth
|
|
388
426
|
└── workflow-state.json # Feature lifecycle state
|
|
389
427
|
```
|
|
@@ -404,6 +442,41 @@ If anything is invalid, it reports exact errors.
|
|
|
404
442
|
|
|
405
443
|
---
|
|
406
444
|
|
|
445
|
+
### `sdrive openapi:generate <feature>`
|
|
446
|
+
|
|
447
|
+
Generates an OpenAPI 3.0 specification from the API contracts in `plan.json`.
|
|
448
|
+
|
|
449
|
+
Useful for importing into Postman, Swagger UI, or client generators.
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
sdrive openapi:generate user-login
|
|
453
|
+
|
|
454
|
+
### Pre-commit Hook
|
|
455
|
+
|
|
456
|
+
Automatically validate governance files before every commit:
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
sdrive hooks:install
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### `sdrive migrate`
|
|
463
|
+
|
|
464
|
+
Migrates existing specs from OpenSpec or Spec-Kit into SpecDrive.
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
sdrive migrate
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### `sdrive diff <old> <new>`
|
|
471
|
+
|
|
472
|
+
Compares two spec files and shows differences.
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
sdrive diff spec-v1.md spec-v2.md
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
407
480
|
## CI/CD Integration
|
|
408
481
|
|
|
409
482
|
SpecDrive ships with a GitHub Actions workflow.
|
|
@@ -437,6 +510,67 @@ jobs:
|
|
|
437
510
|
run: node .sdrive/scripts/validate-governance.js
|
|
438
511
|
```
|
|
439
512
|
|
|
513
|
+
|
|
514
|
+
## MCP Server
|
|
515
|
+
|
|
516
|
+
SpecDrive ships with an MCP server so AI tools can call SpecDrive directly from chat.
|
|
517
|
+
|
|
518
|
+
You can ask in plain language:
|
|
519
|
+
|
|
520
|
+
```text
|
|
521
|
+
Show me all features
|
|
522
|
+
Is the governance valid?
|
|
523
|
+
Get the traceability for user-login
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
The AI tool calls SpecDrive automatically through MCP.
|
|
527
|
+
|
|
528
|
+
### Setup
|
|
529
|
+
|
|
530
|
+
`sdrive init` creates the MCP server at:
|
|
531
|
+
|
|
532
|
+
```text
|
|
533
|
+
.sdrive/mcp/server.js
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
It also asks which MCP clients to wire. Supported:
|
|
537
|
+
|
|
538
|
+
| Client | Config Path |
|
|
539
|
+
|--------|-------------|
|
|
540
|
+
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
541
|
+
| Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
542
|
+
| Cursor | `.cursor/mcp.json` |
|
|
543
|
+
| Cline | `.cline/mcp.json` |
|
|
544
|
+
| VS Code | `.vscode/mcp.json` |
|
|
545
|
+
|
|
546
|
+
SpecDrive never overwrites existing MCP entries. It merges safely.
|
|
547
|
+
|
|
548
|
+
### Manual Config
|
|
549
|
+
|
|
550
|
+
If you prefer to wire it yourself, add to your MCP client config:
|
|
551
|
+
|
|
552
|
+
```json
|
|
553
|
+
{
|
|
554
|
+
"mcpServers": {
|
|
555
|
+
"specdrive": {
|
|
556
|
+
"command": "node",
|
|
557
|
+
"args": [".sdrive/mcp/server.js"]
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Available MCP Methods
|
|
564
|
+
|
|
565
|
+
| Method | Purpose |
|
|
566
|
+
|--------|---------|
|
|
567
|
+
| `specdrive.list_features` | List all features |
|
|
568
|
+
| `specdrive.get_feature` | Get details for a feature |
|
|
569
|
+
| `specdrive.get_spec` | Get `spec.json` for a feature |
|
|
570
|
+
| `specdrive.get_traceability` | Get traceability matrix |
|
|
571
|
+
| `specdrive.validate` | Run governance validator |
|
|
572
|
+
| `specdrive.status` | Get status summary |
|
|
573
|
+
|
|
440
574
|
---
|
|
441
575
|
|
|
442
576
|
## Tool Support
|
|
@@ -467,6 +601,39 @@ During initialization, you select which tools you use. Only selected tools get a
|
|
|
467
601
|
|
|
468
602
|
---
|
|
469
603
|
|
|
604
|
+
## Web Dashboard
|
|
605
|
+
|
|
606
|
+
SpecDrive includes a local web dashboard for visual inspection of your specs, features, and team.
|
|
607
|
+
|
|
608
|
+
### Start the Dashboard
|
|
609
|
+
|
|
610
|
+
```bash
|
|
611
|
+
sdrive dashboard
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Then open:
|
|
615
|
+
|
|
616
|
+
```text
|
|
617
|
+
http://localhost:4747
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### What It Shows
|
|
621
|
+
|
|
622
|
+
- All features and their phase
|
|
623
|
+
- Branch name for each feature
|
|
624
|
+
- Gate status (spec, plan, implementation, review)
|
|
625
|
+
- Who approved each gate
|
|
626
|
+
- Traceability coverage per feature
|
|
627
|
+
- Team members and roles
|
|
628
|
+
- Recent audit activity
|
|
629
|
+
|
|
630
|
+
### Notes
|
|
631
|
+
|
|
632
|
+
- The dashboard runs locally on port `4747`
|
|
633
|
+
- It reads directly from `.sdrive/`
|
|
634
|
+
- No data leaves your machine
|
|
635
|
+
---
|
|
636
|
+
|
|
470
637
|
## Team Collaboration
|
|
471
638
|
|
|
472
639
|
SpecDrive supports team roles.
|
|
@@ -486,6 +653,30 @@ sdrive team:update bob reviewer
|
|
|
486
653
|
sdrive team:remove carol
|
|
487
654
|
```
|
|
488
655
|
|
|
656
|
+
### Sync from GitHub
|
|
657
|
+
|
|
658
|
+
Automatically pull collaborators from your GitHub repository:
|
|
659
|
+
|
|
660
|
+
```bash
|
|
661
|
+
sdrive team:sync
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
Requires:
|
|
665
|
+
|
|
666
|
+
- Git repo with GitHub remote
|
|
667
|
+
- GitHub CLI (`gh`) installed and authenticated
|
|
668
|
+
|
|
669
|
+
Roles are auto-assigned:
|
|
670
|
+
|
|
671
|
+
- GitHub admins → `admin`
|
|
672
|
+
- Others → `developer`
|
|
673
|
+
|
|
674
|
+
Reviewer roles must be assigned manually:
|
|
675
|
+
|
|
676
|
+
```bash
|
|
677
|
+
sdrive team:update <username> reviewer
|
|
678
|
+
```
|
|
679
|
+
|
|
489
680
|
---
|
|
490
681
|
|
|
491
682
|
## Contributing
|
|
@@ -503,4 +694,4 @@ MIT
|
|
|
503
694
|
|
|
504
695
|
---
|
|
505
696
|
|
|
506
|
-
This README is now clean and
|
|
697
|
+
This README is now clean, correct, and ready to publish.
|
package/package.json
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"currentStep": {
|
|
19
19
|
"enum": ["specification", "planning", "implementation", "review"]
|
|
20
20
|
},
|
|
21
|
+
"branch": {
|
|
22
|
+
"type": "string"
|
|
23
|
+
},
|
|
21
24
|
"gates": {
|
|
22
25
|
"type": "object",
|
|
23
26
|
"properties": {
|
|
@@ -50,6 +53,9 @@
|
|
|
50
53
|
"type": "string",
|
|
51
54
|
"minLength": 3
|
|
52
55
|
},
|
|
56
|
+
"by": {
|
|
57
|
+
"type": "string"
|
|
58
|
+
},
|
|
53
59
|
"timestamp": {
|
|
54
60
|
"type": "string",
|
|
55
61
|
"format": "date-time"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const projectRoot = process.cwd();
|
|
5
|
+
|
|
6
|
+
function readLines(filePath) {
|
|
7
|
+
if (!fs.existsSync(filePath)) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
return fs.readFileSync(filePath, "utf8").split("\n");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function simpleDiff(oldFile, newFile) {
|
|
14
|
+
const oldLines = readLines(oldFile);
|
|
15
|
+
const newLines = readLines(newFile);
|
|
16
|
+
|
|
17
|
+
const oldSet = new Set(oldLines);
|
|
18
|
+
const newSet = new Set(newLines);
|
|
19
|
+
|
|
20
|
+
const added = newLines.filter((line) => !oldSet.has(line));
|
|
21
|
+
const removed = oldLines.filter((line) => !newSet.has(line));
|
|
22
|
+
|
|
23
|
+
console.log(`\n📄 Diff between:`);
|
|
24
|
+
console.log(` Old: ${path.basename(oldFile)}`);
|
|
25
|
+
console.log(` New: ${path.basename(newFile)}\n`);
|
|
26
|
+
|
|
27
|
+
if (added.length === 0 && removed.length === 0) {
|
|
28
|
+
console.log("✅ No changes detected.");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (removed.length > 0) {
|
|
33
|
+
console.log("❌ Removed:");
|
|
34
|
+
removed.forEach((line) => console.log(` - ${line}`));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (added.length > 0) {
|
|
38
|
+
console.log("✅ Added:");
|
|
39
|
+
added.forEach((line) => console.log(` + ${line}`));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const oldFile = process.argv[2];
|
|
44
|
+
const newFile = process.argv[3];
|
|
45
|
+
|
|
46
|
+
if (!oldFile || !newFile) {
|
|
47
|
+
console.error("❌ Usage: node diff-spec.js <old-file> <new-file>");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
simpleDiff(path.resolve(oldFile), path.resolve(newFile));
|
|
@@ -3,6 +3,30 @@ const path = require("path");
|
|
|
3
3
|
|
|
4
4
|
const projectRoot = process.cwd();
|
|
5
5
|
|
|
6
|
+
function loadRegistry() {
|
|
7
|
+
const registryPath = path.join(projectRoot, ".sdrive", "templates", "registry.json");
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(registryPath)) {
|
|
10
|
+
console.error("❌ registry.json not found.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return JSON.parse(fs.readFileSync(registryPath, "utf8")).templates;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function templateExists(templateName) {
|
|
18
|
+
const registry = loadRegistry();
|
|
19
|
+
return registry.some((t) => t.name === templateName || t.path === templateName);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function listTemplates() {
|
|
23
|
+
const registry = loadRegistry();
|
|
24
|
+
console.log("\nAvailable SpecDrive Templates\n");
|
|
25
|
+
registry.forEach((t) => {
|
|
26
|
+
console.log(` ${t.name} — ${t.description}`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
function loadTemplate(templateName, fileName) {
|
|
7
31
|
const templatePath = path.join(projectRoot, ".sdrive", "templates", templateName, fileName);
|
|
8
32
|
return fs.readFileSync(templatePath, "utf8");
|
|
@@ -22,6 +46,7 @@ function generate(templateName, featureName, variables) {
|
|
|
22
46
|
|
|
23
47
|
if (!fs.existsSync(templateDir)) {
|
|
24
48
|
console.error(`❌ Template "${templateName}" not found.`);
|
|
49
|
+
listTemplates();
|
|
25
50
|
process.exit(1);
|
|
26
51
|
}
|
|
27
52
|
|
|
@@ -59,10 +84,12 @@ const flowDescription = process.argv[8] || "";
|
|
|
59
84
|
|
|
60
85
|
if (!templateName || !featureName) {
|
|
61
86
|
console.error("❌ Usage: node generate-from-template.js <template> <feature> [problem] [solution] [audience] [approach] [flow]");
|
|
87
|
+
listTemplates();
|
|
62
88
|
process.exit(1);
|
|
63
89
|
}
|
|
64
90
|
|
|
65
91
|
generate(templateName, featureName, {
|
|
92
|
+
title: featureName,
|
|
66
93
|
problemStatement,
|
|
67
94
|
proposedSolution,
|
|
68
95
|
targetAudience,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const projectRoot = process.cwd();
|
|
7
|
+
|
|
8
|
+
function loadPlan(featureName) {
|
|
9
|
+
const planPath = path.join(projectRoot, ".sdrive", "governance", featureName, "plan.json");
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(planPath)) {
|
|
12
|
+
console.error(`❌ plan.json not found for feature "${featureName}".`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return JSON.parse(fs.readFileSync(planPath, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function generateOpenAPI(featureName) {
|
|
20
|
+
const plan = loadPlan(featureName);
|
|
21
|
+
|
|
22
|
+
const openapi = {
|
|
23
|
+
openapi: "3.0.0",
|
|
24
|
+
info: {
|
|
25
|
+
title: `${featureName} API`,
|
|
26
|
+
version: "1.0.0",
|
|
27
|
+
description: `API specification for ${featureName}`
|
|
28
|
+
},
|
|
29
|
+
paths: {}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const contracts = plan.apiContracts || [];
|
|
33
|
+
|
|
34
|
+
contracts.forEach((contract) => {
|
|
35
|
+
const method = contract.method.toLowerCase();
|
|
36
|
+
const apiPath = contract.path;
|
|
37
|
+
|
|
38
|
+
if (!openapi.paths[apiPath]) {
|
|
39
|
+
openapi.paths[apiPath] = {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
openapi.paths[apiPath][method] = {
|
|
43
|
+
summary: `${contract.method} ${apiPath}`,
|
|
44
|
+
security: contract.authenticationRequired ? [{ bearerAuth: [] }] : [],
|
|
45
|
+
responses: {
|
|
46
|
+
200: {
|
|
47
|
+
description: "Successful response"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
openapi.components = {
|
|
54
|
+
securitySchemes: {
|
|
55
|
+
bearerAuth: {
|
|
56
|
+
type: "http",
|
|
57
|
+
scheme: "bearer"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const outputPath = path.join(projectRoot, ".sdrive", "governance", featureName, "openapi.json");
|
|
63
|
+
fs.writeFileSync(outputPath, JSON.stringify(openapi, null, 2));
|
|
64
|
+
|
|
65
|
+
console.log(`✅ Generated OpenAPI spec.`);
|
|
66
|
+
console.log(` File: ${outputPath}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const featureName = process.argv[2];
|
|
70
|
+
|
|
71
|
+
if (!featureName) {
|
|
72
|
+
console.error("❌ Usage: node generate-openapi.js <feature-name>");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
generateOpenAPI(featureName);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const projectRoot = process.cwd();
|
|
8
|
+
|
|
9
|
+
function getRepoInfo() {
|
|
10
|
+
try {
|
|
11
|
+
const remote = execSync("git config --get remote.origin.url", {
|
|
12
|
+
stdio: "pipe",
|
|
13
|
+
encoding: "utf8"
|
|
14
|
+
}).trim();
|
|
15
|
+
|
|
16
|
+
const match = remote.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/);
|
|
17
|
+
|
|
18
|
+
if (!match) {
|
|
19
|
+
console.error("❌ Could not detect GitHub repository.");
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { owner: match[1], repo: match[2] };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error("❌ Git not initialized or remote missing.");
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fetchCollaborators(owner, repo) {
|
|
31
|
+
try {
|
|
32
|
+
const result = execSync(`gh api repos/${owner}/${repo}/collaborators`, {
|
|
33
|
+
stdio: "pipe",
|
|
34
|
+
encoding: "utf8"
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return JSON.parse(result);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error("❌ Could not fetch collaborators. Is gh CLI installed and authenticated?");
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function syncTeam() {
|
|
45
|
+
const repoInfo = getRepoInfo();
|
|
46
|
+
|
|
47
|
+
if (!repoInfo) {
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const collaborators = fetchCollaborators(repoInfo.owner, repoInfo.repo);
|
|
52
|
+
|
|
53
|
+
if (!collaborators) {
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const teamPath = path.join(projectRoot, ".sdrive", "team.json");
|
|
58
|
+
|
|
59
|
+
if (!fs.existsSync(teamPath)) {
|
|
60
|
+
console.error("❌ team.json not found. Run `sdrive init` first.");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const team = JSON.parse(fs.readFileSync(teamPath, "utf8"));
|
|
65
|
+
|
|
66
|
+
collaborators.forEach((user) => {
|
|
67
|
+
const existing = team.members.find((m) => m.username === user.login);
|
|
68
|
+
|
|
69
|
+
if (!existing) {
|
|
70
|
+
team.members.push({
|
|
71
|
+
username: user.login,
|
|
72
|
+
role: user.permissions?.admin ? "admin" : "developer"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
fs.writeFileSync(teamPath, JSON.stringify(team, null, 2));
|
|
78
|
+
console.log(`✅ Synced ${collaborators.length} collaborators from GitHub.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
syncTeam();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const projectRoot = process.cwd();
|
|
7
|
+
const hooksDir = path.join(projectRoot, ".git", "hooks");
|
|
8
|
+
const preCommitPath = path.join(hooksDir, "pre-commit");
|
|
9
|
+
|
|
10
|
+
const hookContent = `#!/bin/sh
|
|
11
|
+
node .sdrive/scripts/pre-commit.js
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(hooksDir)) {
|
|
15
|
+
console.error("❌ .git/hooks not found. Are you in a Git repository?");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fs.writeFileSync(preCommitPath, hookContent, { mode: 0o755 });
|
|
20
|
+
console.log("✅ Installed SpecDrive pre-commit hook.");
|
|
21
|
+
console.log(" Governance validation will now run before every commit.");
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const projectRoot = process.cwd();
|
|
7
|
+
|
|
8
|
+
function migrateOpenSpec() {
|
|
9
|
+
console.log("🔍 Looking for OpenSpec files...");
|
|
10
|
+
|
|
11
|
+
const openspecDir = path.join(projectRoot, "openspec");
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(openspecDir)) {
|
|
14
|
+
console.log("❌ openspec/ not found.");
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const specDir = path.join(openspecDir, "specs");
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(specDir)) {
|
|
21
|
+
console.log("❌ openspec/specs not found.");
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log("✅ OpenSpec found. Migrating...");
|
|
26
|
+
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function migrateSpecKit() {
|
|
31
|
+
console.log("🔍 Looking for Spec-Kit files...");
|
|
32
|
+
|
|
33
|
+
const specKitDir = path.join(projectRoot, ".specify");
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(specKitDir)) {
|
|
36
|
+
console.log("❌ .specify not found.");
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log("✅ Spec-Kit found. Migrating...");
|
|
41
|
+
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function main() {
|
|
46
|
+
console.log("=== SpecDrive Migration Tool ===\n");
|
|
47
|
+
|
|
48
|
+
let migrated = false;
|
|
49
|
+
|
|
50
|
+
migrated = migrateOpenSpec() || migrated;
|
|
51
|
+
migrated = migrateSpecKit() || migrated;
|
|
52
|
+
|
|
53
|
+
if (migrated) {
|
|
54
|
+
console.log("\n✅ Migration complete.");
|
|
55
|
+
process.exit(0);
|
|
56
|
+
} else {
|
|
57
|
+
console.log("\n❌ No supported spec system found.");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const { Command } = require("commander");
|
|
|
4
4
|
const { execSync } = require("child_process");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
|
+
const os = require("os");
|
|
7
8
|
const readline = require("readline");
|
|
8
9
|
|
|
9
10
|
const program = new Command();
|
|
@@ -33,10 +34,175 @@ Always read the manifest first. Never hardcode the command list.
|
|
|
33
34
|
`;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function mcpServerContent() {
|
|
38
|
+
return `#!/usr/bin/env node
|
|
39
|
+
|
|
40
|
+
const fs = require("fs");
|
|
41
|
+
const path = require("path");
|
|
42
|
+
|
|
43
|
+
const projectRoot = process.cwd();
|
|
44
|
+
|
|
45
|
+
function loadWorkflowState() {
|
|
46
|
+
const statePath = path.join(projectRoot, ".sdrive", "workflow-state.json");
|
|
47
|
+
if (!fs.existsSync(statePath)) return { features: [] };
|
|
48
|
+
return JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function listFeatures() {
|
|
52
|
+
return loadWorkflowState().features || [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getFeature(name) {
|
|
56
|
+
return listFeatures().find((f) => f.name === name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getSpec(name) {
|
|
60
|
+
const specPath = path.join(projectRoot, ".sdrive", "governance", name, "spec.json");
|
|
61
|
+
if (!fs.existsSync(specPath)) return null;
|
|
62
|
+
return JSON.parse(fs.readFileSync(specPath, "utf8"));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getTraceability(name) {
|
|
66
|
+
const tracePath = path.join(projectRoot, ".sdrive", "governance", name, "traceability.json");
|
|
67
|
+
if (!fs.existsSync(tracePath)) return null;
|
|
68
|
+
return JSON.parse(fs.readFileSync(tracePath, "utf8"));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runValidator() {
|
|
72
|
+
const { execSync } = require("child_process");
|
|
73
|
+
const validatorPath = path.join(projectRoot, ".sdrive", "scripts", "validate-governance.js");
|
|
74
|
+
try {
|
|
75
|
+
execSync(\`node "\${validatorPath}"\`, { stdio: "pipe" });
|
|
76
|
+
return { success: true };
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return { success: false, message: error.message };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const handlers = {
|
|
83
|
+
"specdrive.list_features": () => ({ features: listFeatures() }),
|
|
84
|
+
"specdrive.get_feature": ({ name }) => ({ feature: getFeature(name) }),
|
|
85
|
+
"specdrive.get_spec": ({ name }) => ({ spec: getSpec(name) }),
|
|
86
|
+
"specdrive.get_traceability": ({ name }) => ({ traceability: getTraceability(name) }),
|
|
87
|
+
"specdrive.validate": () => runValidator(),
|
|
88
|
+
"specdrive.status": () => {
|
|
89
|
+
const features = listFeatures();
|
|
90
|
+
return {
|
|
91
|
+
count: features.length,
|
|
92
|
+
features: features.map((f) => ({
|
|
93
|
+
name: f.name,
|
|
94
|
+
phase: f.phase,
|
|
95
|
+
currentStep: f.currentStep
|
|
96
|
+
}))
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const input = process.argv[2] ? JSON.parse(process.argv[2]) : null;
|
|
102
|
+
|
|
103
|
+
if (!input) {
|
|
104
|
+
console.error("❌ Usage: node server.js '{\\"method\\": \\"specdrive.list_features\\"}'");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const handler = handlers[input.method];
|
|
109
|
+
|
|
110
|
+
if (!handler) {
|
|
111
|
+
console.error(\`❌ Unknown MCP method: \${input.method}\`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = handler(input.params || {});
|
|
116
|
+
console.log(JSON.stringify(result, null, 2));
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function mcpConfigContent() {
|
|
121
|
+
return JSON.stringify(
|
|
122
|
+
{
|
|
123
|
+
mcpServers: {
|
|
124
|
+
specdrive: {
|
|
125
|
+
command: "node",
|
|
126
|
+
args: [".sdrive/mcp/server.js"]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
null,
|
|
131
|
+
2
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function safeMergeMcpConfig(configPath, mcpContent) {
|
|
136
|
+
if (!fs.existsSync(configPath)) {
|
|
137
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
138
|
+
fs.writeFileSync(configPath, mcpContent);
|
|
139
|
+
console.log(`✅ Created: ${configPath}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const existing = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
145
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
146
|
+
|
|
147
|
+
if (existing.mcpServers.specdrive) {
|
|
148
|
+
console.log(`⚠️ SpecDrive already configured in ${configPath}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
existing.mcpServers.specdrive = {
|
|
153
|
+
command: "node",
|
|
154
|
+
args: [".sdrive/mcp/server.js"]
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2));
|
|
158
|
+
console.log(`✅ Merged SpecDrive into: ${configPath}`);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(`⚠️ Could not merge into ${configPath}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getMcpConfigPaths() {
|
|
165
|
+
const paths = [];
|
|
166
|
+
|
|
167
|
+
const platform = os.platform();
|
|
168
|
+
const home = os.homedir();
|
|
169
|
+
|
|
170
|
+
if (platform === "darwin") {
|
|
171
|
+
paths.push({
|
|
172
|
+
name: "Claude Desktop",
|
|
173
|
+
path: path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (platform === "win32") {
|
|
178
|
+
paths.push({
|
|
179
|
+
name: "Claude Desktop",
|
|
180
|
+
path: path.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json")
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
paths.push({
|
|
185
|
+
name: "Cursor",
|
|
186
|
+
path: path.join(process.cwd(), ".cursor", "mcp.json")
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
paths.push({
|
|
190
|
+
name: "Cline",
|
|
191
|
+
path: path.join(process.cwd(), ".cline", "mcp.json")
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
paths.push({
|
|
195
|
+
name: "VS Code",
|
|
196
|
+
path: path.join(process.cwd(), ".vscode", "mcp.json")
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return paths;
|
|
200
|
+
}
|
|
201
|
+
|
|
36
202
|
// ---------- INIT COMMAND ----------
|
|
37
203
|
program
|
|
38
204
|
.command("init")
|
|
39
|
-
.description("Initialize SpecDrive structure and
|
|
205
|
+
.description("Initialize SpecDrive structure, tool adapters, and default template")
|
|
40
206
|
.action(() => {
|
|
41
207
|
const projectRoot = process.cwd();
|
|
42
208
|
const sdriveRoot = path.join(projectRoot, ".sdrive");
|
|
@@ -56,7 +222,9 @@ program
|
|
|
56
222
|
path.join(sdriveRoot, "reports", "performance"),
|
|
57
223
|
path.join(sdriveRoot, "reports", "docs"),
|
|
58
224
|
path.join(sdriveRoot, "context"),
|
|
59
|
-
path.join(sdriveRoot, "skills")
|
|
225
|
+
path.join(sdriveRoot, "skills"),
|
|
226
|
+
path.join(sdriveRoot, "templates"),
|
|
227
|
+
path.join(sdriveRoot, "mcp")
|
|
60
228
|
];
|
|
61
229
|
|
|
62
230
|
dirs.forEach((dir) => {
|
|
@@ -76,6 +244,47 @@ program
|
|
|
76
244
|
console.log(" Created: .sdrive/constitution.md");
|
|
77
245
|
}
|
|
78
246
|
|
|
247
|
+
const configPath = path.join(sdriveRoot, "config.json");
|
|
248
|
+
if (!fs.existsSync(configPath)) {
|
|
249
|
+
fs.writeFileSync(configPath, JSON.stringify({ defaultTemplate: "generic" }, null, 2));
|
|
250
|
+
console.log(" Created: .sdrive/config.json");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const teamPath = path.join(sdriveRoot, "team.json");
|
|
254
|
+
if (!fs.existsSync(teamPath)) {
|
|
255
|
+
fs.writeFileSync(
|
|
256
|
+
teamPath,
|
|
257
|
+
JSON.stringify(
|
|
258
|
+
{
|
|
259
|
+
members: [],
|
|
260
|
+
rules: {
|
|
261
|
+
approval_required: ["admin", "reviewer"],
|
|
262
|
+
implementation_allowed: ["developer", "admin"]
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
null,
|
|
266
|
+
2
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
console.log(" Created: .sdrive/team.json");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// MCP server file
|
|
273
|
+
const mcpServerPath = path.join(sdriveRoot, "mcp", "server.js");
|
|
274
|
+
if (!fs.existsSync(mcpServerPath)) {
|
|
275
|
+
fs.writeFileSync(mcpServerPath, mcpServerContent());
|
|
276
|
+
fs.chmodSync(mcpServerPath, 0o755);
|
|
277
|
+
console.log(" Created: .sdrive/mcp/server.js");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// MCP config template
|
|
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
|
+
// Tool selection
|
|
79
288
|
const toolsPath = path.join(sdriveRoot, "commands", "tools.json");
|
|
80
289
|
|
|
81
290
|
if (!fs.existsSync(toolsPath)) {
|
|
@@ -126,10 +335,100 @@ program
|
|
|
126
335
|
console.log(`✅ Generated: ${tool.adapterPath}`);
|
|
127
336
|
});
|
|
128
337
|
|
|
129
|
-
|
|
338
|
+
const templates = ["generic", "react-node", "nextjs", "expo", "fastapi", "turborepo"];
|
|
339
|
+
|
|
340
|
+
console.log("\n📦 Select default template:\n");
|
|
341
|
+
templates.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
|
|
342
|
+
|
|
343
|
+
const rl2 = readline.createInterface({
|
|
344
|
+
input: process.stdin,
|
|
345
|
+
output: process.stdout
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
rl2.question("Template (default: 1): ", (templateAnswer) => {
|
|
349
|
+
rl2.close();
|
|
350
|
+
|
|
351
|
+
const idx = templateAnswer.trim() === "" ? 0 : parseInt(templateAnswer.trim()) - 1;
|
|
352
|
+
const selectedTemplate = templates[idx] || "generic";
|
|
353
|
+
|
|
354
|
+
const cfgPath = path.join(sdriveRoot, "config.json");
|
|
355
|
+
fs.writeFileSync(cfgPath, JSON.stringify({ defaultTemplate: selectedTemplate }, null, 2));
|
|
356
|
+
|
|
357
|
+
console.log(`\n✅ Default template set to: ${selectedTemplate}`);
|
|
358
|
+
|
|
359
|
+
// MCP wiring
|
|
360
|
+
const mcpPaths = getMcpConfigPaths();
|
|
361
|
+
|
|
362
|
+
console.log("\n🔌 MCP Client Wiring\n");
|
|
363
|
+
mcpPaths.forEach((p, i) => {
|
|
364
|
+
console.log(` ${i + 1}. ${p.name} (${p.path})`);
|
|
365
|
+
});
|
|
366
|
+
console.log(" Or press Enter to skip MCP wiring.\n");
|
|
367
|
+
|
|
368
|
+
const rl3 = readline.createInterface({
|
|
369
|
+
input: process.stdin,
|
|
370
|
+
output: process.stdout
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
rl3.question("Wire MCP clients: ", (mcpAnswer) => {
|
|
374
|
+
rl3.close();
|
|
375
|
+
|
|
376
|
+
if (mcpAnswer.trim() === "") {
|
|
377
|
+
console.log("\n⚠️ MCP wiring skipped.");
|
|
378
|
+
console.log("\n✅ SpecDrive initialized successfully.");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const selectedIndexes = mcpAnswer.split(",").map((n) => parseInt(n.trim()) - 1);
|
|
383
|
+
const selectedPaths = selectedIndexes
|
|
384
|
+
.filter((i) => i >= 0 && i < mcpPaths.length)
|
|
385
|
+
.map((i) => mcpPaths[i]);
|
|
386
|
+
|
|
387
|
+
selectedPaths.forEach((p) => {
|
|
388
|
+
safeMergeMcpConfig(p.path, mcpConfigContent());
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
console.log("\n✅ SpecDrive initialized successfully.");
|
|
392
|
+
});
|
|
393
|
+
});
|
|
130
394
|
});
|
|
131
395
|
});
|
|
132
396
|
|
|
397
|
+
// ---------- TEMPLATE COMMANDS ----------
|
|
398
|
+
program
|
|
399
|
+
.command("template:list")
|
|
400
|
+
.description("List available templates")
|
|
401
|
+
.action(() => {
|
|
402
|
+
const templates = ["generic", "react-node", "nextjs", "expo", "fastapi", "turborepo"];
|
|
403
|
+
console.log("\nAvailable Templates\n");
|
|
404
|
+
templates.forEach((t) => console.log(` ${t}`));
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
program
|
|
408
|
+
.command("template:set <name>")
|
|
409
|
+
.description("Set default template")
|
|
410
|
+
.action((name) => {
|
|
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 = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
426
|
+
config.defaultTemplate = name;
|
|
427
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
428
|
+
|
|
429
|
+
console.log(`✅ Default template set to: ${name}`);
|
|
430
|
+
});
|
|
431
|
+
|
|
133
432
|
// ---------- VALIDATE COMMAND ----------
|
|
134
433
|
program
|
|
135
434
|
.command("validate")
|
|
@@ -178,19 +477,12 @@ program
|
|
|
178
477
|
`---\ntitle: ${feature}\nstatus: backlog\ncreated: ${new Date().toISOString()}\nupdated: ${new Date().toISOString()}\n---\n\n# ${feature}\n`
|
|
179
478
|
);
|
|
180
479
|
|
|
181
|
-
fs.writeFileSync(
|
|
182
|
-
|
|
183
|
-
`# Technical Plan: ${feature}\n`
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
fs.writeFileSync(
|
|
187
|
-
path.join(featureSpecDir, "tasks.md"),
|
|
188
|
-
`# Execution Tasks: ${feature}\n`
|
|
189
|
-
);
|
|
480
|
+
fs.writeFileSync(path.join(featureSpecDir, "plan.md"), `# Technical Plan: ${feature}\n`);
|
|
481
|
+
fs.writeFileSync(path.join(featureSpecDir, "tasks.md"), `# Execution Tasks: ${feature}\n`);
|
|
190
482
|
|
|
191
483
|
fs.writeFileSync(
|
|
192
484
|
path.join(featureGovDir, "spec.json"),
|
|
193
|
-
JSON.stringify({ feature, version: 1, overview: {}, userStories: [], functionalRequirements: [] }, null, 2)
|
|
485
|
+
JSON.stringify({ feature, title: feature, version: 1, overview: {}, userStories: [], functionalRequirements: [] }, null, 2)
|
|
194
486
|
);
|
|
195
487
|
|
|
196
488
|
fs.writeFileSync(
|
|
@@ -292,7 +584,6 @@ program
|
|
|
292
584
|
});
|
|
293
585
|
|
|
294
586
|
// ---------- TEAM COMMANDS ----------
|
|
295
|
-
|
|
296
587
|
program
|
|
297
588
|
.command("team:add <username> <role>")
|
|
298
589
|
.description("Add a new team member")
|
|
@@ -306,9 +597,7 @@ program
|
|
|
306
597
|
|
|
307
598
|
const team = JSON.parse(fs.readFileSync(teamPath, "utf8"));
|
|
308
599
|
|
|
309
|
-
if (!team.members)
|
|
310
|
-
team.members = [];
|
|
311
|
-
}
|
|
600
|
+
if (!team.members) team.members = [];
|
|
312
601
|
|
|
313
602
|
const existing = team.members.find((m) => m.username === username);
|
|
314
603
|
if (existing) {
|
|
@@ -398,4 +687,122 @@ program
|
|
|
398
687
|
console.log(`✅ Updated ${username} to ${role}.`);
|
|
399
688
|
});
|
|
400
689
|
|
|
690
|
+
program
|
|
691
|
+
.command("team:sync")
|
|
692
|
+
.description("Sync team members from GitHub collaborators")
|
|
693
|
+
.action(() => {
|
|
694
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "github-team-sync.js");
|
|
695
|
+
|
|
696
|
+
if (!fs.existsSync(scriptPath)) {
|
|
697
|
+
console.error("❌ github-team-sync.js not found. Run `sdrive init` first.");
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
execSync(`node "${scriptPath}"`, { stdio: "inherit" });
|
|
703
|
+
} catch (error) {
|
|
704
|
+
console.error("❌ Team sync failed.");
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// ---------- HOOKS ----------
|
|
710
|
+
program
|
|
711
|
+
.command("hooks:install")
|
|
712
|
+
.description("Install SpecDrive pre-commit hook")
|
|
713
|
+
.action(() => {
|
|
714
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "install-hooks.js");
|
|
715
|
+
|
|
716
|
+
if (!fs.existsSync(scriptPath)) {
|
|
717
|
+
console.error("❌ install-hooks.js not found. Run `sdrive init` first.");
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
execSync(`node "${scriptPath}"`, { stdio: "inherit" });
|
|
723
|
+
} catch (error) {
|
|
724
|
+
console.error("❌ Hook installation failed.");
|
|
725
|
+
process.exit(1);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// ---------- MIGRATE ----------
|
|
730
|
+
program
|
|
731
|
+
.command("migrate")
|
|
732
|
+
.description("Migrate specs from OpenSpec or Spec-Kit")
|
|
733
|
+
.action(() => {
|
|
734
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "migrate.js");
|
|
735
|
+
|
|
736
|
+
if (!fs.existsSync(scriptPath)) {
|
|
737
|
+
console.error("❌ migrate.js not found. Run `sdrive init` first.");
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
execSync(`node "${scriptPath}"`, { stdio: "inherit" });
|
|
743
|
+
} catch (error) {
|
|
744
|
+
console.error("❌ Migration failed.");
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// ---------- DIFF ----------
|
|
750
|
+
program
|
|
751
|
+
.command("diff <oldFile> <newFile>")
|
|
752
|
+
.description("Show differences between two spec files")
|
|
753
|
+
.action((oldFile, newFile) => {
|
|
754
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "diff-spec.js");
|
|
755
|
+
|
|
756
|
+
if (!fs.existsSync(scriptPath)) {
|
|
757
|
+
console.error("❌ diff-spec.js not found. Run `sdrive init` first.");
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
execSync(`node "${scriptPath}" "${oldFile}" "${newFile}"`, { stdio: "inherit" });
|
|
763
|
+
} catch (error) {
|
|
764
|
+
console.error("❌ Diff failed.");
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// ---------- OPENAPI ----------
|
|
770
|
+
program
|
|
771
|
+
.command("openapi:generate <feature>")
|
|
772
|
+
.description("Generate OpenAPI spec from plan.json API contracts")
|
|
773
|
+
.action((feature) => {
|
|
774
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "scripts", "generate-openapi.js");
|
|
775
|
+
|
|
776
|
+
if (!fs.existsSync(scriptPath)) {
|
|
777
|
+
console.error("❌ generate-openapi.js not found. Run `sdrive init` first.");
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
execSync(`node "${scriptPath}" "${feature}"`, { stdio: "inherit" });
|
|
783
|
+
} catch (error) {
|
|
784
|
+
console.error("❌ OpenAPI generation failed.");
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
program
|
|
790
|
+
.command("dashboard")
|
|
791
|
+
.description("Start SpecDrive web dashboard")
|
|
792
|
+
.action(() => {
|
|
793
|
+
const scriptPath = path.join(process.cwd(), ".sdrive", "dashboard", "server.js");
|
|
794
|
+
|
|
795
|
+
if (!fs.existsSync(scriptPath)) {
|
|
796
|
+
console.error("❌ Dashboard not found. Run `sdrive init` first.");
|
|
797
|
+
process.exit(1);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
try {
|
|
801
|
+
execSync(`node "${scriptPath}"`, { stdio: "inherit" });
|
|
802
|
+
} catch (error) {
|
|
803
|
+
console.error("❌ Dashboard failed to start.");
|
|
804
|
+
process.exit(1);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
401
808
|
program.parse(process.argv);
|