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,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Feature Specification",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"feature": { "type": "string", "pattern": "^[a-z0-9-]+$" },
|
|
7
|
+
"version": { "type": "integer", "minimum": 1 },
|
|
8
|
+
"overview": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"problemStatement": { "type": "string" },
|
|
12
|
+
"proposedSolution": { "type": "string" },
|
|
13
|
+
"targetAudience": { "type": "string" }
|
|
14
|
+
},
|
|
15
|
+
"required": ["problemStatement", "proposedSolution", "targetAudience"]
|
|
16
|
+
},
|
|
17
|
+
"userStories": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"minItems": 1,
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"id": { "type": "string", "pattern": "^US-[0-9]{3}$" },
|
|
24
|
+
"title": { "type": "string" },
|
|
25
|
+
"acceptanceCriteria": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"minItems": 1,
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"id": { "type": "string", "pattern": "^AC-[0-9]{3}$" },
|
|
32
|
+
"text": { "type": "string" }
|
|
33
|
+
},
|
|
34
|
+
"required": ["id", "text"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required": ["id", "title", "acceptanceCriteria"]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"functionalRequirements": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"items": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"id": { "type": "string", "pattern": "^FR-[0-9]{3}$" },
|
|
47
|
+
"text": { "type": "string" }
|
|
48
|
+
},
|
|
49
|
+
"required": ["id", "text"]
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"nonFunctionalRequirements": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"items": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"id": { "type": "string", "pattern": "^NFR-[0-9]{3}$" },
|
|
58
|
+
"text": { "type": "string" }
|
|
59
|
+
},
|
|
60
|
+
"required": ["id", "text"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"required": ["feature", "version", "overview", "userStories"]
|
|
65
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Execution Tasks",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"feature": { "type": "string", "pattern": "^[a-z0-9-]+$" },
|
|
7
|
+
"version": { "type": "integer", "minimum": 1 },
|
|
8
|
+
"phases": {
|
|
9
|
+
"type": "array",
|
|
10
|
+
"minItems": 1,
|
|
11
|
+
"items": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"phaseId": { "type": "string", "pattern": "^P[0-9]+$" },
|
|
15
|
+
"name": { "type": "string" },
|
|
16
|
+
"tasks": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"minItems": 1,
|
|
19
|
+
"items": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"id": { "type": "string", "pattern": "^T[0-9]+\\.[0-9]+$" },
|
|
23
|
+
"title": { "type": "string" },
|
|
24
|
+
"description": { "type": "string" },
|
|
25
|
+
"linkedRequirements": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": { "type": "string", "pattern": "^(FR|NFR)-[0-9]{3}$" }
|
|
28
|
+
},
|
|
29
|
+
"linkedAcceptanceCriteria": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"items": { "type": "string", "pattern": "^AC-[0-9]{3}$" }
|
|
32
|
+
},
|
|
33
|
+
"completed": { "type": "boolean", "default": false }
|
|
34
|
+
},
|
|
35
|
+
"required": ["id", "title", "linkedRequirements"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"required": ["phaseId", "name", "tasks"]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"required": ["feature", "version", "phases"]
|
|
44
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Traceability Matrix",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"feature": { "type": "string", "pattern": "^[a-z0-9-]+$" },
|
|
7
|
+
"version": { "type": "integer", "minimum": 1 },
|
|
8
|
+
"rows": {
|
|
9
|
+
"type": "array",
|
|
10
|
+
"items": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"properties": {
|
|
13
|
+
"requirementId": { "type": "string", "pattern": "^(FR|NFR)-[0-9]{3}$" },
|
|
14
|
+
"acceptanceCriteriaIds": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": { "type": "string", "pattern": "^AC-[0-9]{3}$" }
|
|
17
|
+
},
|
|
18
|
+
"planSection": { "type": "string" },
|
|
19
|
+
"taskIds": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": { "type": "string", "pattern": "^T[0-9]+\\.[0-9]+$" }
|
|
22
|
+
},
|
|
23
|
+
"testIds": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": { "type": "string", "pattern": "^TEST-[0-9]{3}$" }
|
|
26
|
+
},
|
|
27
|
+
"status": { "enum": ["not_started", "in_progress", "complete"] }
|
|
28
|
+
},
|
|
29
|
+
"required": ["requirementId", "status"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"required": ["feature", "version", "rows"]
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Workflow State",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"features": {
|
|
7
|
+
"type": "array",
|
|
8
|
+
"items": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"name": { "type": "string", "pattern": "^[a-z0-9-]+$" },
|
|
12
|
+
"phase": { "enum": ["backlog", "ongoing", "completed", "blocked"] },
|
|
13
|
+
"currentStep": { "enum": ["specification", "planning", "implementation", "review"] },
|
|
14
|
+
"gates": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"properties": {
|
|
17
|
+
"specification": { "enum": ["pending", "approved", "rejected"] },
|
|
18
|
+
"planning": { "enum": ["pending", "approved", "rejected"] },
|
|
19
|
+
"implementation": { "enum": ["pending", "in_progress", "approved", "rejected"] },
|
|
20
|
+
"review": { "enum": ["pending", "approved", "rejected"] }
|
|
21
|
+
},
|
|
22
|
+
"required": ["specification", "planning", "implementation", "review"]
|
|
23
|
+
},
|
|
24
|
+
"history": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"event": { "type": "string" },
|
|
30
|
+
"timestamp": { "type": "string", "format": "date-time" }
|
|
31
|
+
},
|
|
32
|
+
"required": ["event", "timestamp"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"required": ["name", "phase", "currentStep", "gates"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"required": ["features"]
|
|
41
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const projectRoot = process.cwd();
|
|
5
|
+
|
|
6
|
+
function readJson(filePath) {
|
|
7
|
+
try {
|
|
8
|
+
if (fs.existsSync(filePath)) {
|
|
9
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
10
|
+
}
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error(`ā ļø Could not parse ${filePath}: ${error.message}`);
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getInstalledDependencies() {
|
|
18
|
+
const packageJson = readJson(path.join(projectRoot, "package.json"));
|
|
19
|
+
if (!packageJson) return [];
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
...Object.keys(packageJson.dependencies || {}),
|
|
23
|
+
...Object.keys(packageJson.devDependencies || {})
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function listFiles(dir, extensions = [".ts", ".tsx", ".js", ".jsx"]) {
|
|
28
|
+
if (!fs.existsSync(dir)) return [];
|
|
29
|
+
|
|
30
|
+
let results = [];
|
|
31
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const fullPath = path.join(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
results = results.concat(listFiles(fullPath, extensions));
|
|
37
|
+
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
38
|
+
results.push(fullPath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function detectExistingAssets() {
|
|
46
|
+
const srcDir = path.join(projectRoot, "src");
|
|
47
|
+
const files = listFiles(srcDir);
|
|
48
|
+
|
|
49
|
+
const assets = {
|
|
50
|
+
components: [],
|
|
51
|
+
services: [],
|
|
52
|
+
hooks: [],
|
|
53
|
+
utils: [],
|
|
54
|
+
api: [],
|
|
55
|
+
models: [],
|
|
56
|
+
constants: []
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
files.forEach((file) => {
|
|
60
|
+
const relative = path.relative(projectRoot, file).replace(/\\/g, "/");
|
|
61
|
+
|
|
62
|
+
if (relative.includes("/components/") || relative.includes("/ui/")) {
|
|
63
|
+
assets.components.push(relative);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (relative.includes("/services/") || relative.includes("/api/")) {
|
|
67
|
+
assets.services.push(relative);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (relative.includes("/hooks/") || relative.startsWith("use-")) {
|
|
71
|
+
assets.hooks.push(relative);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (relative.includes("/utils/") || relative.includes("/helpers/")) {
|
|
75
|
+
assets.utils.push(relative);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (relative.includes("/models/") || relative.includes("/entities/")) {
|
|
79
|
+
assets.models.push(relative);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (relative.includes("/constants/") || relative.includes("/config/")) {
|
|
83
|
+
assets.constants.push(relative);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return assets;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkRedundancy(plannedItems) {
|
|
91
|
+
const existingAssets = detectExistingAssets();
|
|
92
|
+
const installedDependencies = getInstalledDependencies();
|
|
93
|
+
|
|
94
|
+
const report = plannedItems.map((item) => {
|
|
95
|
+
let exists = false;
|
|
96
|
+
let location = null;
|
|
97
|
+
|
|
98
|
+
switch (item.type) {
|
|
99
|
+
case "component":
|
|
100
|
+
exists = existingAssets.components.some((path) => path.includes(item.name));
|
|
101
|
+
if (exists) {
|
|
102
|
+
location = existingAssets.components.find((path) => path.includes(item.name));
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case "service":
|
|
106
|
+
case "api":
|
|
107
|
+
exists = existingAssets.services.some((path) => path.includes(item.name));
|
|
108
|
+
if (exists) {
|
|
109
|
+
location = existingAssets.services.find((path) => path.includes(item.name));
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
case "hook":
|
|
113
|
+
exists = existingAssets.hooks.some((path) => path.includes(item.name));
|
|
114
|
+
if (exists) {
|
|
115
|
+
location = existingAssets.hooks.find((path) => path.includes(item.name));
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
case "utility":
|
|
119
|
+
exists = existingAssets.utils.some((path) => path.includes(item.name));
|
|
120
|
+
if (exists) {
|
|
121
|
+
location = existingAssets.utils.find((path) => path.includes(item.name));
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
case "model":
|
|
125
|
+
case "entity":
|
|
126
|
+
exists = existingAssets.models.some((path) => path.includes(item.name));
|
|
127
|
+
if (exists) {
|
|
128
|
+
location = existingAssets.models.find((path) => path.includes(item.name));
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
case "package":
|
|
132
|
+
case "dependency":
|
|
133
|
+
exists = installedDependencies.includes(item.name);
|
|
134
|
+
location = exists ? "package.json" : null;
|
|
135
|
+
break;
|
|
136
|
+
default:
|
|
137
|
+
exists = false;
|
|
138
|
+
location = null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
name: item.name,
|
|
143
|
+
type: item.type,
|
|
144
|
+
exists,
|
|
145
|
+
action: exists ? "reuse" : "create",
|
|
146
|
+
location
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
feature: "current",
|
|
152
|
+
items: report,
|
|
153
|
+
existingAssets,
|
|
154
|
+
installedDependencies
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function main() {
|
|
159
|
+
const plannedItemsPath = process.argv[2];
|
|
160
|
+
|
|
161
|
+
if (!plannedItemsPath) {
|
|
162
|
+
console.error("ā Usage: node anti-redundancy.js <planned-items.json>");
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const plannedItems = readJson(path.resolve(plannedItemsPath));
|
|
167
|
+
|
|
168
|
+
if (!plannedItems || !Array.isArray(plannedItems)) {
|
|
169
|
+
console.error("ā planned-items.json must contain an array of items.");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const report = checkRedundancy(plannedItems);
|
|
174
|
+
console.log(JSON.stringify(report, null, 2));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
main();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const projectRoot = process.cwd();
|
|
5
|
+
const auditLogPath = path.join(projectRoot, ".sdrive", "audit-log.json");
|
|
6
|
+
|
|
7
|
+
function loadAuditLog() {
|
|
8
|
+
if (!fs.existsSync(auditLogPath)) {
|
|
9
|
+
return { events: [] };
|
|
10
|
+
}
|
|
11
|
+
return JSON.parse(fs.readFileSync(auditLogPath, "utf8"));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function saveAuditLog(log) {
|
|
15
|
+
fs.writeFileSync(auditLogPath, JSON.stringify(log, null, 2));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function logEvent(event, details = {}) {
|
|
19
|
+
const log = loadAuditLog();
|
|
20
|
+
|
|
21
|
+
log.events.push({
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
...details,
|
|
24
|
+
event
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
saveAuditLog(log);
|
|
28
|
+
console.log(`š Audit: ${event}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
logEvent,
|
|
33
|
+
loadAuditLog,
|
|
34
|
+
saveAuditLog
|
|
35
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// SAFETY RULE
|
|
2
|
+
// NEVER overwrite existing files in .github/
|
|
3
|
+
// If a file exists, ask the user before modifying it.
|
|
4
|
+
// Only create new files if missing.
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
const projectRoot = process.cwd();
|
|
10
|
+
|
|
11
|
+
function loadTools() {
|
|
12
|
+
const toolsPath = path.join(projectRoot, ".sdrive", "commands", "tools.json");
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(toolsPath)) {
|
|
15
|
+
console.error("ā tools.json not found.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return JSON.parse(fs.readFileSync(toolsPath, "utf8")).tools;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function adapterContent(toolName) {
|
|
23
|
+
return `# SpecDrive Commands
|
|
24
|
+
|
|
25
|
+
This project uses SpecDrive for spec-driven development.
|
|
26
|
+
|
|
27
|
+
## How to use
|
|
28
|
+
|
|
29
|
+
When the user types a /sdrive:* command, follow these steps:
|
|
30
|
+
|
|
31
|
+
1. Read .sdrive/commands/manifest.json
|
|
32
|
+
2. Find the matching command
|
|
33
|
+
3. Read the agent file listed in agentFile
|
|
34
|
+
4. Follow that agent's instructions exactly
|
|
35
|
+
5. Run validators after writing files
|
|
36
|
+
6. Stop at human approval gates
|
|
37
|
+
|
|
38
|
+
## Command list
|
|
39
|
+
|
|
40
|
+
- /sdrive:constitution ā Create or update project constitution
|
|
41
|
+
- /sdrive:propose ā Create spec, traceability, plan, and tasks
|
|
42
|
+
- /sdrive:design ā Build UI/UX prototype
|
|
43
|
+
- /sdrive:apply ā Write production code
|
|
44
|
+
- /sdrive:review ā Final audit and merge
|
|
45
|
+
- /sdrive:secure ā Security audit
|
|
46
|
+
- /sdrive:performance ā Performance benchmark
|
|
47
|
+
- /sdrive:docs ā Generate documentation
|
|
48
|
+
- /sdrive:sync ā Cascade sync
|
|
49
|
+
- /sdrive:skills ā Manage AI skills
|
|
50
|
+
|
|
51
|
+
Always read the manifest first. Never hardcode the command list.
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function generate() {
|
|
56
|
+
const tools = loadTools();
|
|
57
|
+
|
|
58
|
+
tools.forEach((tool) => {
|
|
59
|
+
const adapterFullPath = path.join(projectRoot, tool.adapterPath);
|
|
60
|
+
const adapterDir = path.dirname(adapterFullPath);
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(adapterDir)) {
|
|
63
|
+
fs.mkdirSync(adapterDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// SAFETY GUARD
|
|
67
|
+
if (fs.existsSync(adapterFullPath)) {
|
|
68
|
+
console.log(`ā ļø Skipped existing file: ${tool.adapterPath}`);
|
|
69
|
+
console.log(" SpecDrive never overwrites existing project files.");
|
|
70
|
+
console.log(" Delete the file or rename it to regenerate.\n");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(adapterFullPath, adapterContent(tool.name));
|
|
75
|
+
console.log(`ā
Generated adapter: ${tool.adapterPath}`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log("\nš Adapter generation complete.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
generate();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const projectRoot = process.cwd();
|
|
5
|
+
|
|
6
|
+
function loadTemplate(templateName, fileName) {
|
|
7
|
+
const templatePath = path.join(projectRoot, ".sdrive", "templates", templateName, fileName);
|
|
8
|
+
return fs.readFileSync(templatePath, "utf8");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function applyVariables(content, variables) {
|
|
12
|
+
let result = content;
|
|
13
|
+
Object.keys(variables).forEach((key) => {
|
|
14
|
+
result = result.replaceAll(`{{${key}}}`, variables[key]);
|
|
15
|
+
});
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function generate(templateName, featureName, variables) {
|
|
20
|
+
const templateDir = path.join(projectRoot, ".sdrive", "templates", templateName);
|
|
21
|
+
const govDir = path.join(projectRoot, ".sdrive", "governance", featureName);
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(templateDir)) {
|
|
24
|
+
console.error(`ā Template "${templateName}" not found.`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fs.mkdirSync(govDir, { recursive: true });
|
|
29
|
+
|
|
30
|
+
const files = ["spec.json", "plan.json", "tasks.json"];
|
|
31
|
+
|
|
32
|
+
files.forEach((file) => {
|
|
33
|
+
const templateFile = path.join(templateDir, file);
|
|
34
|
+
if (!fs.existsSync(templateFile)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let content = loadTemplate(templateName, file);
|
|
39
|
+
content = applyVariables(content, {
|
|
40
|
+
featureName,
|
|
41
|
+
...variables
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
fs.writeFileSync(path.join(govDir, file), content);
|
|
45
|
+
console.log(`ā
Generated: ${featureName}/${file}`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(`\nš Feature "${featureName}" generated from "${templateName}" template.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const templateName = process.argv[2];
|
|
52
|
+
const featureName = process.argv[3];
|
|
53
|
+
const problemStatement = process.argv[4] || "";
|
|
54
|
+
const proposedSolution = process.argv[5] || "";
|
|
55
|
+
const targetAudience = process.argv[6] || "";
|
|
56
|
+
const approach = process.argv[7] || "";
|
|
57
|
+
const flowDescription = process.argv[8] || "";
|
|
58
|
+
|
|
59
|
+
if (!templateName || !featureName) {
|
|
60
|
+
console.error("ā Usage: node generate-from-template.js <template> <feature> [problem] [solution] [audience] [approach] [flow]");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
generate(templateName, featureName, {
|
|
65
|
+
problemStatement,
|
|
66
|
+
proposedSolution,
|
|
67
|
+
targetAudience,
|
|
68
|
+
approach,
|
|
69
|
+
flowDescription
|
|
70
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const projectRoot = process.cwd();
|
|
5
|
+
const pluginsDir = path.join(projectRoot, ".sdrive", "plugins");
|
|
6
|
+
|
|
7
|
+
function loadPlugins() {
|
|
8
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const pluginFolders = fs.readdirSync(pluginsDir, { withFileTypes: true })
|
|
13
|
+
.filter((d) => d.isDirectory())
|
|
14
|
+
.map((d) => d.name);
|
|
15
|
+
|
|
16
|
+
const plugins = [];
|
|
17
|
+
|
|
18
|
+
pluginFolders.forEach((folder) => {
|
|
19
|
+
const pluginJsonPath = path.join(pluginsDir, folder, "plugin.json");
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(pluginJsonPath)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pluginDef = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
|
|
26
|
+
plugins.push({
|
|
27
|
+
...pluginDef,
|
|
28
|
+
path: path.join(pluginsDir, folder)
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return plugins;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function runPlugin(pluginName) {
|
|
36
|
+
const plugins = loadPlugins();
|
|
37
|
+
const plugin = plugins.find((p) => p.name === pluginName);
|
|
38
|
+
|
|
39
|
+
if (!plugin) {
|
|
40
|
+
console.error(`ā Plugin "${pluginName}" not found.`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const entryPath = path.join(plugin.path, plugin.entry);
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(entryPath)) {
|
|
47
|
+
console.error(`ā Plugin entry not found: ${plugin.entry}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
require(entryPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
loadPlugins,
|
|
56
|
+
runPlugin
|
|
57
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const { execSync } = require("child_process");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
|
|
5
|
+
const projectRoot = process.cwd();
|
|
6
|
+
const validatorPath = path.join(projectRoot, ".sdrive", "scripts", "validate-governance.js");
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(validatorPath)) {
|
|
9
|
+
console.log("ā ļø Validator not found. Skipping pre-commit validation.");
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
execSync(`node "${validatorPath}"`, { stdio: "inherit" });
|
|
15
|
+
console.log("ā
Pre-commit validation passed.");
|
|
16
|
+
process.exit(0);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error("ā Pre-commit validation failed. Fix errors before committing.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
package/scripts/team.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const projectRoot = process.cwd();
|
|
5
|
+
const teamPath = path.join(projectRoot, ".sdrive", "team.json");
|
|
6
|
+
|
|
7
|
+
function loadTeam() {
|
|
8
|
+
if (!fs.existsSync(teamPath)) {
|
|
9
|
+
console.error("ā team.json not found.");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
return JSON.parse(fs.readFileSync(teamPath, "utf8"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getUserRole(username) {
|
|
16
|
+
const team = loadTeam();
|
|
17
|
+
const member = team.members.find((m) => m.username === username);
|
|
18
|
+
return member ? member.role : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function canApprove(username) {
|
|
22
|
+
const role = getUserRole(username);
|
|
23
|
+
if (!role) return false;
|
|
24
|
+
|
|
25
|
+
const team = loadTeam();
|
|
26
|
+
return team.rules.approval_required.includes(role);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function canImplement(username) {
|
|
30
|
+
const role = getUserRole(username);
|
|
31
|
+
if (!role) return false;
|
|
32
|
+
|
|
33
|
+
const team = loadTeam();
|
|
34
|
+
return team.rules.implementation_allowed.includes(role);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
getUserRole,
|
|
39
|
+
canApprove,
|
|
40
|
+
canImplement
|
|
41
|
+
};
|