specdrive-cli 0.1.2 → 0.1.8

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.
@@ -27,10 +27,7 @@ function resetEnvironment() {
27
27
  }
28
28
 
29
29
  const workflowStatePath = path.join(projectRoot, ".sdrive", "workflow-state.json");
30
- fs.writeFileSync(
31
- workflowStatePath,
32
- JSON.stringify({ features: [] }, null, 2)
33
- );
30
+ fs.writeFileSync(workflowStatePath, JSON.stringify({ features: [] }, null, 2));
34
31
  }
35
32
 
36
33
  function testFeatureCreation() {
@@ -17,6 +17,7 @@ function runRouter(command, feature) {
17
17
 
18
18
  function testKnownCommand() {
19
19
  console.log("\n🧪 Test 1: Known command should pass");
20
+
20
21
  const passed = runRouter("/sdrive:constitution");
21
22
 
22
23
  if (passed) {
@@ -30,6 +31,7 @@ function testKnownCommand() {
30
31
 
31
32
  function testUnknownCommand() {
32
33
  console.log("\n🧪 Test 2: Unknown command should fail");
34
+
33
35
  const passed = runRouter("/sdrive:unknown");
34
36
 
35
37
  if (!passed) {
@@ -44,7 +46,6 @@ function testUnknownCommand() {
44
46
  function testMissingFeatureForGatedCommand() {
45
47
  console.log("\n🧪 Test 3: Gated command without feature should fail");
46
48
 
47
- // /sdrive:propose has gates
48
49
  const passed = runRouter("/sdrive:propose");
49
50
 
50
51
  if (!passed) {
@@ -59,25 +60,30 @@ function testMissingFeatureForGatedCommand() {
59
60
  function testFeatureConversion() {
60
61
  console.log("\n🧪 Test 4: Natural feature description should convert to kebab-case");
61
62
 
62
- // Test the function indirectly by checking output
63
- const { execSync } = require("child_process");
64
63
  try {
65
64
  const output = execSync(
66
65
  `node "${routerPath}" /sdrive:propose "User login with email"`,
67
66
  { stdio: "pipe", encoding: "utf8" }
68
67
  );
68
+
69
69
  if (output.includes("user-login-with-email")) {
70
70
  console.log("✅ PASS: Feature converted to kebab-case.");
71
71
  return true;
72
72
  }
73
+
73
74
  console.log("❌ FAIL: Feature not converted correctly.");
74
75
  return false;
75
76
  } catch (error) {
76
- // Command may fail due to gate, but conversion should still show
77
77
  if (error.stdout && error.stdout.includes("user-login-with-email")) {
78
78
  console.log("✅ PASS: Feature converted to kebab-case.");
79
79
  return true;
80
80
  }
81
+
82
+ if (error.stdout && error.stdout.includes("user-login-with-email")) {
83
+ console.log("✅ PASS: Feature converted to kebab-case.");
84
+ return true;
85
+ }
86
+
81
87
  console.log("❌ FAIL: Feature not converted correctly.");
82
88
  return false;
83
89
  }
@@ -4,7 +4,6 @@ const { execSync } = require("child_process");
4
4
 
5
5
  const projectRoot = process.cwd();
6
6
  const workflowStatePath = path.join(projectRoot, ".sdrive", "workflow-state.json");
7
- const gatesPath = path.join(projectRoot, ".sdrive", "commands", "gates.js");
8
7
 
9
8
  function resetState() {
10
9
  const initialState = {
@@ -14,16 +14,20 @@ function createValidFiles() {
14
14
  JSON.stringify(
15
15
  {
16
16
  feature: "test-feature",
17
+ title: "Test Feature",
17
18
  version: 1,
18
19
  overview: {
19
- problemStatement: "Test problem",
20
- proposedSolution: "Test solution",
20
+ problemStatement: "Test problem statement",
21
+ proposedSolution: "Test proposed solution",
21
22
  targetAudience: "Test users"
22
23
  },
23
24
  userStories: [
24
25
  {
25
26
  id: "US-001",
26
27
  title: "Test story",
28
+ role: "user",
29
+ want: "test",
30
+ benefit: "test",
27
31
  acceptanceCriteria: [
28
32
  {
29
33
  id: "AC-001",
@@ -54,6 +58,7 @@ function createValidFiles() {
54
58
  {
55
59
  requirementId: "FR-001",
56
60
  acceptanceCriteriaIds: ["AC-001"],
61
+ taskIds: ["T1.1"],
57
62
  status: "not_started"
58
63
  }
59
64
  ]
@@ -4,24 +4,13 @@ const path = require("path");
4
4
 
5
5
  const ajv = new Ajv({ allErrors: true });
6
6
 
7
- // ---------- Resolve project root ----------
8
7
  const projectRoot = path.join(__dirname, "..", "..");
9
8
 
10
- // ---------- Load schemas ----------
11
9
  const schemaDir = path.join(projectRoot, ".sdrive", "schemas");
12
10
  const governanceDir = path.join(projectRoot, ".sdrive", "governance");
13
11
  const specsDir = path.join(projectRoot, ".sdrive", "specs");
14
12
  const workflowStatePath = path.join(projectRoot, ".sdrive", "workflow-state.json");
15
13
 
16
- const schemas = {
17
- spec: loadJson(path.join(schemaDir, "spec.schema.json")),
18
- plan: loadJson(path.join(schemaDir, "plan.schema.json")),
19
- tasks: loadJson(path.join(schemaDir, "tasks.schema.json")),
20
- traceability: loadJson(path.join(schemaDir, "traceability.schema.json")),
21
- workflowState: loadJson(path.join(schemaDir, "workflow-state.schema.json")),
22
- };
23
-
24
- // ---------- Helper functions ----------
25
14
  function loadJson(filePath) {
26
15
  if (!fs.existsSync(filePath)) {
27
16
  console.error(`❌ Schema/file not found: ${filePath}`);
@@ -33,6 +22,7 @@ function loadJson(filePath) {
33
22
  function validateData(data, schema, label) {
34
23
  const validate = ajv.compile(schema);
35
24
  const valid = validate(data);
25
+
36
26
  if (valid) {
37
27
  console.log(`✅ ${label} is valid.`);
38
28
  return true;
@@ -43,21 +33,20 @@ function validateData(data, schema, label) {
43
33
  }
44
34
  }
45
35
 
46
- function featureExists(featureName) {
47
- return fs.existsSync(path.join(governanceDir, featureName));
48
- }
49
-
50
36
  function validateWorkflowState() {
51
37
  if (!fs.existsSync(workflowStatePath)) {
52
38
  console.log("⚠️ workflow-state.json not found. Skipping.");
53
39
  return true;
54
40
  }
55
- const workflowState = loadJson(workflowStatePath);
56
- return validateData(workflowState, schemas.workflowState, "workflow-state.json");
41
+
42
+ const schema = loadJson(path.join(schemaDir, "workflow-state.schema.json"));
43
+ const data = loadJson(workflowStatePath);
44
+ return validateData(data, schema, "workflow-state.json");
57
45
  }
58
46
 
59
47
  function validateFeature(featureName) {
60
48
  const featureDir = path.join(governanceDir, featureName);
49
+
61
50
  if (!fs.existsSync(featureDir)) {
62
51
  console.log(`⚠️ Feature "${featureName}" has no governance folder. Skipping.`);
63
52
  return true;
@@ -65,41 +54,26 @@ function validateFeature(featureName) {
65
54
 
66
55
  let failed = false;
67
56
 
68
- // Validate spec.json
69
- const specPath = path.join(featureDir, "spec.json");
70
- if (fs.existsSync(specPath)) {
71
- const specData = loadJson(specPath);
72
- failed |= !validateData(specData, schemas.spec, `${featureName}/spec.json`);
73
- } else {
74
- console.log(`⚠️ ${featureName}/spec.json not found. Skipping.`);
75
- }
57
+ const files = [
58
+ { file: "spec.json", schema: "spec.schema.json" },
59
+ { file: "plan.json", schema: "plan.schema.json" },
60
+ { file: "tasks.json", schema: "tasks.schema.json" },
61
+ { file: "traceability.json", schema: "traceability.schema.json" }
62
+ ];
76
63
 
77
- // Validate plan.json
78
- const planPath = path.join(featureDir, "plan.json");
79
- if (fs.existsSync(planPath)) {
80
- const planData = loadJson(planPath);
81
- failed |= !validateData(planData, schemas.plan, `${featureName}/plan.json`);
82
- } else {
83
- console.log(`⚠️ ${featureName}/plan.json not found. Skipping.`);
84
- }
64
+ files.forEach(({ file, schema }) => {
65
+ const filePath = path.join(featureDir, file);
66
+ const schemaPath = path.join(schemaDir, schema);
85
67
 
86
- // Validate tasks.json
87
- const tasksPath = path.join(featureDir, "tasks.json");
88
- if (fs.existsSync(tasksPath)) {
89
- const tasksData = loadJson(tasksPath);
90
- failed |= !validateData(tasksData, schemas.tasks, `${featureName}/tasks.json`);
91
- } else {
92
- console.log(`⚠️ ${featureName}/tasks.json not found. Skipping.`);
93
- }
68
+ if (!fs.existsSync(filePath)) {
69
+ console.log(`⚠️ ${featureName}/${file} not found. Skipping.`);
70
+ return;
71
+ }
94
72
 
95
- // Validate traceability.json
96
- const traceabilityPath = path.join(featureDir, "traceability.json");
97
- if (fs.existsSync(traceabilityPath)) {
98
- const traceData = loadJson(traceabilityPath);
99
- failed |= !validateData(traceData, schemas.traceability, `${featureName}/traceability.json`);
100
- } else {
101
- console.log(`⚠️ ${featureName}/traceability.json not found. Skipping.`);
102
- }
73
+ const data = loadJson(filePath);
74
+ const schemaData = loadJson(schemaPath);
75
+ failed |= !validateData(data, schemaData, `${featureName}/${file}`);
76
+ });
103
77
 
104
78
  return !failed;
105
79
  }
@@ -107,53 +81,44 @@ function validateFeature(featureName) {
107
81
  function discoverFeatures() {
108
82
  const features = new Set();
109
83
 
110
- // From governance folder
111
84
  if (fs.existsSync(governanceDir)) {
112
- const dirs = fs.readdirSync(governanceDir, { withFileTypes: true })
85
+ fs.readdirSync(governanceDir, { withFileTypes: true })
113
86
  .filter((d) => d.isDirectory())
114
- .map((d) => d.name);
115
- dirs.forEach((d) => features.add(d));
87
+ .forEach((d) => features.add(d.name));
116
88
  }
117
89
 
118
- // From specs folder
119
- const lifecycleFolders = ["backlog", "ongoing", "completed"];
120
- lifecycleFolders.forEach((lifecycle) => {
90
+ ["backlog", "ongoing", "completed"].forEach((lifecycle) => {
121
91
  const lifecyclePath = path.join(specsDir, lifecycle);
122
92
  if (fs.existsSync(lifecyclePath)) {
123
- const dirs = fs.readdirSync(lifecyclePath, { withFileTypes: true })
93
+ fs.readdirSync(lifecyclePath, { withFileTypes: true })
124
94
  .filter((d) => d.isDirectory())
125
- .map((d) => d.name);
126
- dirs.forEach((d) => features.add(d));
95
+ .forEach((d) => features.add(d.name));
127
96
  }
128
97
  });
129
98
 
130
99
  return Array.from(features);
131
100
  }
132
101
 
133
- // ---------- Main ----------
134
102
  function main() {
135
103
  console.log("🔍 SpecDrive Governance Validation\n");
136
104
 
137
105
  let failed = false;
138
106
 
139
- // Validate workflow state
140
107
  failed |= !validateWorkflowState();
141
108
 
142
- // Discover features
143
109
  const features = discoverFeatures();
144
110
 
145
111
  if (features.length === 0) {
146
112
  console.log("⚠️ No features found under .sdrive/governance or .sdrive/specs.");
147
113
  }
148
114
 
149
- // Validate each feature
150
115
  features.forEach((feature) => {
151
116
  console.log(`\n📁 Validating feature: ${feature}`);
152
117
  failed |= !validateFeature(feature);
153
118
  });
154
119
 
155
- // Final summary
156
120
  console.log("\n----------------------------------------");
121
+
157
122
  if (failed) {
158
123
  console.log("❌ Validation FAILED. Fix the errors above.");
159
124
  process.exit(1);
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 readline = require("readline");
7
8
 
8
9
  const program = new Command();
9
10
 
@@ -12,6 +13,26 @@ program
12
13
  .description("SpecDrive Enterprise Spec-Driven Development CLI")
13
14
  .version("0.1.0");
14
15
 
16
+ function adapterContent(toolName) {
17
+ return `# SpecDrive Commands
18
+
19
+ This project uses SpecDrive for spec-driven development.
20
+
21
+ ## How to use
22
+
23
+ When the user types a /sdrive:* command, follow these steps:
24
+
25
+ 1. Read .sdrive/commands/manifest.json
26
+ 2. Find the matching command
27
+ 3. Read the agent file listed in agentFile
28
+ 4. Follow that agent's instructions exactly
29
+ 5. Run validators after writing files
30
+ 6. Stop at human approval gates
31
+
32
+ Always read the manifest first. Never hardcode the command list.
33
+ `;
34
+ }
35
+
15
36
  // ---------- INIT COMMAND ----------
16
37
  program
17
38
  .command("init")
@@ -55,16 +76,58 @@ program
55
76
  console.log(" Created: .sdrive/constitution.md");
56
77
  }
57
78
 
58
- const generatorPath = path.join(sdriveRoot, "scripts", "generate-adapters.js");
59
- if (fs.existsSync(generatorPath)) {
60
- try {
61
- execSync(`node "${generatorPath}"`, { stdio: "inherit" });
62
- } catch (error) {
63
- console.warn("⚠️ Tool adapter generation skipped.");
64
- }
79
+ const toolsPath = path.join(sdriveRoot, "commands", "tools.json");
80
+
81
+ if (!fs.existsSync(toolsPath)) {
82
+ console.log("\n⚠️ tools.json not found. Skipping tool selection.");
83
+ console.log("\n✅ SpecDrive initialized successfully.");
84
+ return;
65
85
  }
66
86
 
67
- console.log("\n✅ SpecDrive initialized successfully.");
87
+ const tools = JSON.parse(fs.readFileSync(toolsPath, "utf8")).tools;
88
+
89
+ console.log("\n🔧 Select tools to set up (comma-separated numbers):\n");
90
+ tools.forEach((tool, index) => {
91
+ console.log(` ${index + 1}. ${tool.name}`);
92
+ });
93
+ console.log(" Or press Enter to generate all.\n");
94
+
95
+ const rl = readline.createInterface({
96
+ input: process.stdin,
97
+ output: process.stdout
98
+ });
99
+
100
+ rl.question("Selection: ", (answer) => {
101
+ rl.close();
102
+
103
+ let selectedTools = tools;
104
+
105
+ if (answer.trim() !== "") {
106
+ const selectedIndexes = answer.split(",").map((n) => parseInt(n.trim()) - 1);
107
+ selectedTools = selectedIndexes
108
+ .filter((i) => i >= 0 && i < tools.length)
109
+ .map((i) => tools[i]);
110
+ }
111
+
112
+ selectedTools.forEach((tool) => {
113
+ const adapterFullPath = path.join(projectRoot, tool.adapterPath);
114
+ const adapterDir = path.dirname(adapterFullPath);
115
+
116
+ if (!fs.existsSync(adapterDir)) {
117
+ fs.mkdirSync(adapterDir, { recursive: true });
118
+ }
119
+
120
+ if (fs.existsSync(adapterFullPath)) {
121
+ console.log(`⚠️ Skipped existing: ${tool.adapterPath}`);
122
+ return;
123
+ }
124
+
125
+ fs.writeFileSync(adapterFullPath, adapterContent(tool.name));
126
+ console.log(`✅ Generated: ${tool.adapterPath}`);
127
+ });
128
+
129
+ console.log("\n✅ SpecDrive initialized successfully.");
130
+ });
68
131
  });
69
132
 
70
133
  // ---------- VALIDATE COMMAND ----------