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.
@@ -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);