specdrive-cli 0.1.3 â 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.
- package/README.md +25 -14
- package/commands/gates.js +26 -16
- package/commands/router.js +2 -41
- package/commands/tools.json +15 -20
- package/package.json +1 -1
- package/schemas/plan.schema.json +94 -16
- package/schemas/spec.schema.json +140 -17
- package/schemas/tasks.schema.json +57 -14
- package/schemas/traceability.schema.json +41 -12
- package/schemas/workflow-state.schema.json +40 -13
- package/scripts/anti-redundancy.js +11 -11
- package/scripts/audit-log.js +14 -2
- package/scripts/generate-adapters.js +28 -28
- package/scripts/generate-from-template.js +1 -0
- package/scripts/load-plugins.js +15 -6
- package/scripts/team.js +79 -6
- package/scripts/test-adapters.js +20 -29
- package/scripts/test-end-to-end.js +1 -4
- package/scripts/test-router.js +10 -4
- package/scripts/test-state-transitions.js +0 -1
- package/scripts/test-validator.js +7 -2
- package/scripts/validate-governance.js +29 -64
- package/src/index.js +71 -8
|
@@ -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() {
|
package/scripts/test-router.js
CHANGED
|
@@ -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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
85
|
+
fs.readdirSync(governanceDir, { withFileTypes: true })
|
|
113
86
|
.filter((d) => d.isDirectory())
|
|
114
|
-
.
|
|
115
|
-
dirs.forEach((d) => features.add(d));
|
|
87
|
+
.forEach((d) => features.add(d.name));
|
|
116
88
|
}
|
|
117
89
|
|
|
118
|
-
|
|
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
|
-
|
|
93
|
+
fs.readdirSync(lifecyclePath, { withFileTypes: true })
|
|
124
94
|
.filter((d) => d.isDirectory())
|
|
125
|
-
.
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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 ----------
|