specdrive-cli 0.1.0
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 +465 -0
- package/agents/01-constitution.md +202 -0
- package/agents/02-specification.md +226 -0
- package/agents/03-uiux.md +145 -0
- package/agents/04-cascade.md +123 -0
- package/agents/05-discover-skills.md +137 -0
- package/agents/06-documentation.md +146 -0
- package/agents/07-implementation.md +169 -0
- package/agents/08-performance.md +166 -0
- package/agents/09-review-complete.md +169 -0
- package/agents/10-security.md +168 -0
- package/commands/gates.js +64 -0
- package/commands/manifest.json +96 -0
- package/commands/router.js +167 -0
- package/commands/tools.json +25 -0
- package/package.json +31 -0
- package/schemas/plan.schema.json +89 -0
- package/schemas/spec.schema.json +65 -0
- package/schemas/tasks.schema.json +44 -0
- package/schemas/traceability.schema.json +34 -0
- package/schemas/workflow-state.schema.json +41 -0
- package/scripts/anti-redundancy.js +177 -0
- package/scripts/audit-log.js +35 -0
- package/scripts/generate-adapters.js +81 -0
- package/scripts/generate-from-template.js +70 -0
- package/scripts/load-plugins.js +57 -0
- package/scripts/pre-commit.js +20 -0
- package/scripts/team.js +41 -0
- package/scripts/test-adapters.js +128 -0
- package/scripts/test-create.js +14 -0
- package/scripts/test-end-to-end.js +141 -0
- package/scripts/test-router.js +105 -0
- package/scripts/test-state-transitions.js +148 -0
- package/scripts/test-validator.js +148 -0
- package/scripts/validate-governance.js +166 -0
- package/src/index.js +235 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const Ajv = require("ajv");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const ajv = new Ajv({ allErrors: true });
|
|
6
|
+
|
|
7
|
+
// ---------- Resolve project root ----------
|
|
8
|
+
const projectRoot = path.join(__dirname, "..", "..");
|
|
9
|
+
|
|
10
|
+
// ---------- Load schemas ----------
|
|
11
|
+
const schemaDir = path.join(projectRoot, ".sdrive", "schemas");
|
|
12
|
+
const governanceDir = path.join(projectRoot, ".sdrive", "governance");
|
|
13
|
+
const specsDir = path.join(projectRoot, ".sdrive", "specs");
|
|
14
|
+
const workflowStatePath = path.join(projectRoot, ".sdrive", "workflow-state.json");
|
|
15
|
+
|
|
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
|
+
function loadJson(filePath) {
|
|
26
|
+
if (!fs.existsSync(filePath)) {
|
|
27
|
+
console.error(`❌ Schema/file not found: ${filePath}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateData(data, schema, label) {
|
|
34
|
+
const validate = ajv.compile(schema);
|
|
35
|
+
const valid = validate(data);
|
|
36
|
+
if (valid) {
|
|
37
|
+
console.log(`✅ ${label} is valid.`);
|
|
38
|
+
return true;
|
|
39
|
+
} else {
|
|
40
|
+
console.log(`❌ ${label} is INVALID:`);
|
|
41
|
+
console.log(validate.errors);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function featureExists(featureName) {
|
|
47
|
+
return fs.existsSync(path.join(governanceDir, featureName));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateWorkflowState() {
|
|
51
|
+
if (!fs.existsSync(workflowStatePath)) {
|
|
52
|
+
console.log("⚠️ workflow-state.json not found. Skipping.");
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const workflowState = loadJson(workflowStatePath);
|
|
56
|
+
return validateData(workflowState, schemas.workflowState, "workflow-state.json");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateFeature(featureName) {
|
|
60
|
+
const featureDir = path.join(governanceDir, featureName);
|
|
61
|
+
if (!fs.existsSync(featureDir)) {
|
|
62
|
+
console.log(`⚠️ Feature "${featureName}" has no governance folder. Skipping.`);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let failed = false;
|
|
67
|
+
|
|
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
|
+
}
|
|
76
|
+
|
|
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
|
+
}
|
|
85
|
+
|
|
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
|
+
}
|
|
94
|
+
|
|
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
|
+
}
|
|
103
|
+
|
|
104
|
+
return !failed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function discoverFeatures() {
|
|
108
|
+
const features = new Set();
|
|
109
|
+
|
|
110
|
+
// From governance folder
|
|
111
|
+
if (fs.existsSync(governanceDir)) {
|
|
112
|
+
const dirs = fs.readdirSync(governanceDir, { withFileTypes: true })
|
|
113
|
+
.filter((d) => d.isDirectory())
|
|
114
|
+
.map((d) => d.name);
|
|
115
|
+
dirs.forEach((d) => features.add(d));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// From specs folder
|
|
119
|
+
const lifecycleFolders = ["backlog", "ongoing", "completed"];
|
|
120
|
+
lifecycleFolders.forEach((lifecycle) => {
|
|
121
|
+
const lifecyclePath = path.join(specsDir, lifecycle);
|
|
122
|
+
if (fs.existsSync(lifecyclePath)) {
|
|
123
|
+
const dirs = fs.readdirSync(lifecyclePath, { withFileTypes: true })
|
|
124
|
+
.filter((d) => d.isDirectory())
|
|
125
|
+
.map((d) => d.name);
|
|
126
|
+
dirs.forEach((d) => features.add(d));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return Array.from(features);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------- Main ----------
|
|
134
|
+
function main() {
|
|
135
|
+
console.log("🔍 SpecDrive Governance Validation\n");
|
|
136
|
+
|
|
137
|
+
let failed = false;
|
|
138
|
+
|
|
139
|
+
// Validate workflow state
|
|
140
|
+
failed |= !validateWorkflowState();
|
|
141
|
+
|
|
142
|
+
// Discover features
|
|
143
|
+
const features = discoverFeatures();
|
|
144
|
+
|
|
145
|
+
if (features.length === 0) {
|
|
146
|
+
console.log("⚠️ No features found under .sdrive/governance or .sdrive/specs.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Validate each feature
|
|
150
|
+
features.forEach((feature) => {
|
|
151
|
+
console.log(`\n📁 Validating feature: ${feature}`);
|
|
152
|
+
failed |= !validateFeature(feature);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Final summary
|
|
156
|
+
console.log("\n----------------------------------------");
|
|
157
|
+
if (failed) {
|
|
158
|
+
console.log("❌ Validation FAILED. Fix the errors above.");
|
|
159
|
+
process.exit(1);
|
|
160
|
+
} else {
|
|
161
|
+
console.log("✅ Governance validation complete. All files are valid.");
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
main();
|
package/src/index.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require("commander");
|
|
4
|
+
const { execSync } = require("child_process");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name("sdrive")
|
|
12
|
+
.description("SpecDrive Enterprise Spec-Driven Development CLI")
|
|
13
|
+
.version("0.1.0");
|
|
14
|
+
|
|
15
|
+
// ---------- INIT COMMAND ----------
|
|
16
|
+
program
|
|
17
|
+
.command("init")
|
|
18
|
+
.description("Initialize SpecDrive structure and tool adapters")
|
|
19
|
+
.action(() => {
|
|
20
|
+
const projectRoot = process.cwd();
|
|
21
|
+
const sdriveRoot = path.join(projectRoot, ".sdrive");
|
|
22
|
+
|
|
23
|
+
// Create .sdrive directory structure
|
|
24
|
+
const dirs = [
|
|
25
|
+
path.join(sdriveRoot, "agents"),
|
|
26
|
+
path.join(sdriveRoot, "commands"),
|
|
27
|
+
path.join(sdriveRoot, "schemas"),
|
|
28
|
+
path.join(sdriveRoot, "specs", "backlog"),
|
|
29
|
+
path.join(sdriveRoot, "specs", "ongoing"),
|
|
30
|
+
path.join(sdriveRoot, "specs", "completed"),
|
|
31
|
+
path.join(sdriveRoot, "governance"),
|
|
32
|
+
path.join(sdriveRoot, "prototype", "css"),
|
|
33
|
+
path.join(sdriveRoot, "prototype", "js"),
|
|
34
|
+
path.join(sdriveRoot, "prototype", "screens"),
|
|
35
|
+
path.join(sdriveRoot, "reports", "security"),
|
|
36
|
+
path.join(sdriveRoot, "reports", "performance"),
|
|
37
|
+
path.join(sdriveRoot, "reports", "docs"),
|
|
38
|
+
path.join(sdriveRoot, "context"),
|
|
39
|
+
path.join(sdriveRoot, "skills")
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
dirs.forEach((dir) => {
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
console.log(` Created: ${path.relative(projectRoot, dir)}`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Create workflow-state.json if missing
|
|
48
|
+
const workflowStatePath = path.join(sdriveRoot, "workflow-state.json");
|
|
49
|
+
if (!fs.existsSync(workflowStatePath)) {
|
|
50
|
+
fs.writeFileSync(workflowStatePath, JSON.stringify({ features: [] }, null, 2));
|
|
51
|
+
console.log(" Created: .sdrive/workflow-state.json");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Create constitution.md if missing
|
|
55
|
+
const constitutionPath = path.join(sdriveRoot, "constitution.md");
|
|
56
|
+
if (!fs.existsSync(constitutionPath)) {
|
|
57
|
+
fs.writeFileSync(constitutionPath, "# SpecDrive Constitution\n\nInitialized by `sdrive init`.\n");
|
|
58
|
+
console.log(" Created: .sdrive/constitution.md");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Generate tool adapters
|
|
62
|
+
const generatorPath = path.join(sdriveRoot, "scripts", "generate-adapters.js");
|
|
63
|
+
if (fs.existsSync(generatorPath)) {
|
|
64
|
+
try {
|
|
65
|
+
execSync(`node "${generatorPath}"`, { stdio: "inherit" });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn("⚠️ Tool adapter generation skipped.");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log("\n✅ SpecDrive initialized successfully.");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ---------- VALIDATE COMMAND ----------
|
|
75
|
+
program
|
|
76
|
+
.command("validate")
|
|
77
|
+
.description("Validate SpecDrive governance files")
|
|
78
|
+
.action(() => {
|
|
79
|
+
const projectRoot = process.cwd();
|
|
80
|
+
const validatorPath = path.join(projectRoot, ".sdrive", "scripts", "validate-governance.js");
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(validatorPath)) {
|
|
83
|
+
console.error("❌ Validator not found. Run `sdrive init` first.");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
execSync(`node "${validatorPath}"`, { stdio: "inherit" });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("\n❌ Validation failed.");
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ---------- CREATE COMMAND ----------
|
|
96
|
+
program
|
|
97
|
+
.command("create <feature>")
|
|
98
|
+
.description("Create a new feature scaffold (internal)")
|
|
99
|
+
.action((feature) => {
|
|
100
|
+
if (!/^[a-z0-9-]+$/.test(feature)) {
|
|
101
|
+
console.error("❌ Feature name must be lowercase letters, numbers, and hyphens only.");
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const projectRoot = process.cwd();
|
|
106
|
+
const featureSpecDir = path.join(projectRoot, ".sdrive", "specs", "backlog", feature);
|
|
107
|
+
const featureGovDir = path.join(projectRoot, ".sdrive", "governance", feature);
|
|
108
|
+
|
|
109
|
+
if (fs.existsSync(featureSpecDir) || fs.existsSync(featureGovDir)) {
|
|
110
|
+
console.error(`❌ Feature "${feature}" already exists.`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fs.mkdirSync(featureSpecDir, { recursive: true });
|
|
115
|
+
fs.mkdirSync(featureGovDir, { recursive: true });
|
|
116
|
+
|
|
117
|
+
fs.writeFileSync(
|
|
118
|
+
path.join(featureSpecDir, "spec.md"),
|
|
119
|
+
`---\ntitle: ${feature}\nstatus: backlog\ncreated: ${new Date().toISOString()}\nupdated: ${new Date().toISOString()}\n---\n\n# ${feature}\n`
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
fs.writeFileSync(
|
|
123
|
+
path.join(featureSpecDir, "plan.md"),
|
|
124
|
+
`# Technical Plan: ${feature}\n`
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
fs.writeFileSync(
|
|
128
|
+
path.join(featureSpecDir, "tasks.md"),
|
|
129
|
+
`# Execution Tasks: ${feature}\n`
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
fs.writeFileSync(
|
|
133
|
+
path.join(featureGovDir, "spec.json"),
|
|
134
|
+
JSON.stringify({ feature, version: 1, overview: {}, userStories: [], functionalRequirements: [] }, null, 2)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
fs.writeFileSync(
|
|
138
|
+
path.join(featureGovDir, "plan.json"),
|
|
139
|
+
JSON.stringify({ feature, version: 1, architecture: {}, dataModel: {} }, null, 2)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(
|
|
143
|
+
path.join(featureGovDir, "tasks.json"),
|
|
144
|
+
JSON.stringify({ feature, version: 1, phases: [] }, null, 2)
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
fs.writeFileSync(
|
|
148
|
+
path.join(featureGovDir, "traceability.json"),
|
|
149
|
+
JSON.stringify({ feature, version: 1, rows: [] }, null, 2)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
console.log(`✅ Feature "${feature}" created in backlog.`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ---------- STATUS COMMAND ----------
|
|
156
|
+
program
|
|
157
|
+
.command("status")
|
|
158
|
+
.description("Show feature lifecycle state")
|
|
159
|
+
.action(() => {
|
|
160
|
+
const projectRoot = process.cwd();
|
|
161
|
+
const workflowStatePath = path.join(projectRoot, ".sdrive", "workflow-state.json");
|
|
162
|
+
|
|
163
|
+
if (!fs.existsSync(workflowStatePath)) {
|
|
164
|
+
console.error("❌ workflow-state.json not found. Run `sdrive init` first.");
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const state = JSON.parse(fs.readFileSync(workflowStatePath, "utf8"));
|
|
169
|
+
|
|
170
|
+
if (!state.features || state.features.length === 0) {
|
|
171
|
+
console.log("No features found.");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log("\nSpecDrive Feature Status\n");
|
|
176
|
+
|
|
177
|
+
state.features.forEach((f) => {
|
|
178
|
+
console.log(`📁 ${f.name}`);
|
|
179
|
+
console.log(` Phase: ${f.phase}`);
|
|
180
|
+
console.log(` Current Step: ${f.currentStep}`);
|
|
181
|
+
console.log(` Gates:`);
|
|
182
|
+
console.log(` specification: ${f.gates?.specification ?? "pending"}`);
|
|
183
|
+
console.log(` planning: ${f.gates?.planning ?? "pending"}`);
|
|
184
|
+
console.log(` implementation: ${f.gates?.implementation ?? "pending"}`);
|
|
185
|
+
console.log(` review: ${f.gates?.review ?? "pending"}`);
|
|
186
|
+
console.log("");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ---------- APPROVE COMMAND ----------
|
|
191
|
+
program
|
|
192
|
+
.command("approve <gate>")
|
|
193
|
+
.description("Approve a workflow gate (specification, planning, implementation, review)")
|
|
194
|
+
.action((gate) => {
|
|
195
|
+
const allowedGates = ["specification", "planning", "implementation", "review"];
|
|
196
|
+
|
|
197
|
+
if (!allowedGates.includes(gate)) {
|
|
198
|
+
console.error(`❌ Invalid gate. Allowed: ${allowedGates.join(", ")}`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const projectRoot = process.cwd();
|
|
203
|
+
const workflowStatePath = path.join(projectRoot, ".sdrive", "workflow-state.json");
|
|
204
|
+
|
|
205
|
+
if (!fs.existsSync(workflowStatePath)) {
|
|
206
|
+
console.error("❌ workflow-state.json not found. Run `sdrive init` first.");
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const state = JSON.parse(fs.readFileSync(workflowStatePath, "utf8"));
|
|
211
|
+
|
|
212
|
+
if (!state.features || state.features.length === 0) {
|
|
213
|
+
console.error("❌ No features found.");
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const feature = state.features[0];
|
|
218
|
+
|
|
219
|
+
if (!feature.gates) {
|
|
220
|
+
feature.gates = {};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
feature.gates[gate] = "approved";
|
|
224
|
+
feature.history = feature.history || [];
|
|
225
|
+
feature.history.push({
|
|
226
|
+
event: `${gate}_approved`,
|
|
227
|
+
timestamp: new Date().toISOString()
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
fs.writeFileSync(workflowStatePath, JSON.stringify(state, null, 2));
|
|
231
|
+
|
|
232
|
+
console.log(`✅ Gate "${gate}" approved for feature "${feature.name}".`);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
program.parse(process.argv);
|