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 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 correct.
697
+ This README is now clean, correct, and ready to publish.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specdrive-cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "SpecDrive enterprise spec-driven development CLI",
5
5
  "bin": {
6
6
  "sdrive": "src/index.js"
@@ -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 tool adapters")
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
- console.log("\n✅ SpecDrive initialized successfully.");
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
- path.join(featureSpecDir, "plan.md"),
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);