specdrive-cli 0.1.9 → 0.1.12
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 +955 -506
- package/agents/00-onboarding.md +261 -0
- package/agents/01-constitution.md +214 -201
- package/agents/02-specification.md +249 -226
- package/agents/03-uiux.md +156 -144
- package/agents/04-cascade.md +151 -122
- package/agents/05-discover-skills.md +136 -136
- package/agents/06-documentation.md +158 -145
- package/agents/07-implementation.md +201 -169
- package/agents/08-performance.md +179 -165
- package/agents/09-review-complete.md +239 -168
- package/agents/10-security.md +180 -167
- package/agents/11-test.md +195 -0
- package/commands/gates.js +73 -73
- package/commands/manifest.json +113 -95
- package/commands/permissions.json +39 -0
- package/commands/router.js +151 -127
- package/commands/tools.json +19 -19
- package/dashboard/app.js +394 -0
- package/dashboard/index.html +74 -0
- package/dashboard/server.js +166 -0
- package/dashboard/style.css +157 -0
- package/mcp/mcp.json +31 -0
- package/mcp/server.js +108 -0
- package/package.json +35 -32
- package/schemas/config.schema.json +20 -0
- package/schemas/workflow-state.schema.json +152 -35
- package/scripts/anti-redundancy.js +176 -176
- package/scripts/audit-log.js +46 -46
- package/scripts/check-permission.js +87 -0
- package/scripts/diff-spec.js +51 -0
- package/scripts/diff-version.js +96 -0
- package/scripts/generate-adapters.js +80 -80
- package/scripts/generate-from-template.js +97 -70
- package/scripts/generate-openapi.js +76 -0
- package/scripts/github-team-sync.js +81 -0
- package/scripts/install-hooks.js +21 -0
- package/scripts/load-plugins.js +65 -65
- package/scripts/migrate-openspec.js +318 -0
- package/scripts/migrate-speckit.js +322 -0
- package/scripts/migrate.js +12 -0
- package/scripts/onboard.js +312 -0
- package/scripts/pre-commit.js +56 -20
- package/scripts/team.js +113 -113
- package/scripts/test-adapters.js +118 -118
- package/scripts/test-create.js +13 -13
- package/scripts/test-end-to-end.js +137 -137
- package/scripts/test-router.js +110 -110
- package/scripts/test-state-transitions.js +146 -146
- package/scripts/test-validator.js +152 -152
- package/scripts/validate-config.js +36 -0
- package/scripts/validate-governance.js +150 -130
- package/scripts/verify.js +525 -0
- package/scripts/version-new.js +202 -0
- package/src/index.js +1010 -400
- package/templates/expo/plan.json +12 -0
- package/templates/expo/spec.json +12 -0
- package/templates/expo/tasks.json +5 -0
- package/templates/fastapi/plan.json +12 -0
- package/templates/fastapi/spec.json +12 -0
- package/templates/fastapi/tasks.json +5 -0
- package/templates/generic/plan.json +12 -0
- package/templates/generic/spec.json +11 -0
- package/templates/generic/tasks.json +5 -0
- package/templates/nextjs/plan.json +23 -0
- package/templates/nextjs/spec.json +12 -0
- package/templates/nextjs/tasks.json +5 -0
- package/templates/react-node/plan.json +15 -0
- package/templates/react-node/spec.json +12 -0
- package/templates/react-node/tasks.json +5 -0
- package/templates/registry.json +30 -0
- package/templates/turborepo/plan.json +12 -0
- package/templates/turborepo/spec.json +12 -0
- package/templates/turborepo/tasks.json +5 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const projectRoot = process.cwd();
|
|
7
|
+
const sdriveRoot = path.join(projectRoot, ".sdrive");
|
|
8
|
+
|
|
9
|
+
const openspecDir = path.join(projectRoot, "openspec");
|
|
10
|
+
const openspecSpecsDir = path.join(openspecDir, "specs");
|
|
11
|
+
const openspecChangesDir = path.join(openspecDir, "changes");
|
|
12
|
+
const openspecArchiveDir = path.join(openspecDir, "archive");
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(openspecDir)) {
|
|
15
|
+
console.error("❌ openspec/ folder not found. Nothing to migrate.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ensureDir(dir) {
|
|
20
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readFile(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
return fs.readFileSync(filePath, "utf8");
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listDirectories(dir) {
|
|
32
|
+
if (!fs.existsSync(dir)) return [];
|
|
33
|
+
return fs
|
|
34
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
35
|
+
.filter((d) => d.isDirectory())
|
|
36
|
+
.map((d) => d.name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function slugify(name) {
|
|
40
|
+
return name
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
43
|
+
.replace(/^-+|-+$/g, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findSpecFile(dir) {
|
|
47
|
+
const candidates = ["spec.md", "proposal.md", "README.md"];
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
const full = path.join(dir, candidate);
|
|
50
|
+
if (fs.existsSync(full)) return full;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findTasksFile(dir) {
|
|
56
|
+
const candidates = ["tasks.md", "todo.md"];
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
const full = path.join(dir, candidate);
|
|
59
|
+
if (fs.existsSync(full)) return full;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeSpecDriveFeature(featureName, sourceDir, lifecycle) {
|
|
65
|
+
const featureFolder = path.join(
|
|
66
|
+
sdriveRoot,
|
|
67
|
+
"specs",
|
|
68
|
+
lifecycle,
|
|
69
|
+
featureName,
|
|
70
|
+
"v1"
|
|
71
|
+
);
|
|
72
|
+
const govFolder = path.join(sdriveRoot, "governance", featureName, "v1");
|
|
73
|
+
|
|
74
|
+
ensureDir(featureFolder);
|
|
75
|
+
ensureDir(govFolder);
|
|
76
|
+
|
|
77
|
+
const specSource = findSpecFile(sourceDir);
|
|
78
|
+
const tasksSource = findTasksFile(sourceDir);
|
|
79
|
+
|
|
80
|
+
const specContent = specSource
|
|
81
|
+
? readFile(specSource)
|
|
82
|
+
: `# ${featureName}\n\n(Migrated from OpenSpec — no source spec found)`;
|
|
83
|
+
|
|
84
|
+
const tasksContent = tasksSource
|
|
85
|
+
? readFile(tasksSource)
|
|
86
|
+
: `# Execution Tasks: ${featureName}\n\n(Migrated from OpenSpec — no tasks found)`;
|
|
87
|
+
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
|
|
90
|
+
// spec.md
|
|
91
|
+
fs.writeFileSync(
|
|
92
|
+
path.join(featureFolder, "spec.md"),
|
|
93
|
+
`---
|
|
94
|
+
title: ${featureName}
|
|
95
|
+
status: ${lifecycle === "completed" ? "completed" : "ongoing"}
|
|
96
|
+
version: v1
|
|
97
|
+
created: ${now}
|
|
98
|
+
updated: ${now}
|
|
99
|
+
retroactive: true
|
|
100
|
+
migratedFrom: openspec
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
${specContent}
|
|
104
|
+
`
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// plan.md
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(featureFolder, "plan.md"),
|
|
110
|
+
`# Technical Plan: ${featureName} (migrated)\n\nThis plan was migrated from OpenSpec. No technical plan was extracted.\n`
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// tasks.md
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
path.join(featureFolder, "tasks.md"),
|
|
116
|
+
tasksContent
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// governance JSON
|
|
120
|
+
fs.writeFileSync(
|
|
121
|
+
path.join(govFolder, "spec.json"),
|
|
122
|
+
JSON.stringify(
|
|
123
|
+
{
|
|
124
|
+
feature: featureName,
|
|
125
|
+
title: featureName,
|
|
126
|
+
version: 1,
|
|
127
|
+
retroactive: true,
|
|
128
|
+
migratedFrom: "openspec",
|
|
129
|
+
overview: {
|
|
130
|
+
problemStatement: "(migrated from OpenSpec)",
|
|
131
|
+
proposedSolution: "(migrated from OpenSpec)",
|
|
132
|
+
targetAudience: "(migrated from OpenSpec)"
|
|
133
|
+
},
|
|
134
|
+
userStories: [],
|
|
135
|
+
functionalRequirements: []
|
|
136
|
+
},
|
|
137
|
+
null,
|
|
138
|
+
2
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(
|
|
143
|
+
path.join(govFolder, "plan.json"),
|
|
144
|
+
JSON.stringify(
|
|
145
|
+
{
|
|
146
|
+
feature: featureName,
|
|
147
|
+
version: 1,
|
|
148
|
+
architecture: { approach: "(migrated)", components: [] },
|
|
149
|
+
dataModel: { entities: [], flowDescription: "(migrated)" }
|
|
150
|
+
},
|
|
151
|
+
null,
|
|
152
|
+
2
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
fs.writeFileSync(
|
|
157
|
+
path.join(govFolder, "tasks.json"),
|
|
158
|
+
JSON.stringify(
|
|
159
|
+
{ feature: featureName, version: 1, phases: [] },
|
|
160
|
+
null,
|
|
161
|
+
2
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
fs.writeFileSync(
|
|
166
|
+
path.join(govFolder, "traceability.json"),
|
|
167
|
+
JSON.stringify(
|
|
168
|
+
{ feature: featureName, version: 1, rows: [] },
|
|
169
|
+
null,
|
|
170
|
+
2
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
feature: featureName,
|
|
176
|
+
source: path.relative(projectRoot, sourceDir),
|
|
177
|
+
lifecycle,
|
|
178
|
+
spec: specSource ? path.relative(projectRoot, specSource) : null,
|
|
179
|
+
tasks: tasksSource ? path.relative(projectRoot, tasksSource) : null
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function migrate() {
|
|
184
|
+
const migrated = [];
|
|
185
|
+
|
|
186
|
+
// 1. Active specs
|
|
187
|
+
if (fs.existsSync(openspecSpecsDir)) {
|
|
188
|
+
const features = listDirectories(openspecSpecsDir);
|
|
189
|
+
for (const feature of features) {
|
|
190
|
+
const featureName = slugify(feature);
|
|
191
|
+
const sourceDir = path.join(openspecSpecsDir, feature);
|
|
192
|
+
const result = writeSpecDriveFeature(featureName, sourceDir, "ongoing");
|
|
193
|
+
migrated.push(result);
|
|
194
|
+
console.log(`✅ Migrated spec: ${featureName}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 2. Active changes
|
|
199
|
+
if (fs.existsSync(openspecChangesDir)) {
|
|
200
|
+
const changes = listDirectories(openspecChangesDir);
|
|
201
|
+
for (const change of changes) {
|
|
202
|
+
const featureName = slugify(change);
|
|
203
|
+
const sourceDir = path.join(openspecChangesDir, change);
|
|
204
|
+
const result = writeSpecDriveFeature(featureName, sourceDir, "ongoing");
|
|
205
|
+
migrated.push(result);
|
|
206
|
+
console.log(`✅ Migrated change: ${featureName}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 3. Archive
|
|
211
|
+
if (fs.existsSync(openspecArchiveDir)) {
|
|
212
|
+
const archived = listDirectories(openspecArchiveDir);
|
|
213
|
+
for (const feature of archived) {
|
|
214
|
+
const featureName = slugify(feature);
|
|
215
|
+
const sourceDir = path.join(openspecArchiveDir, feature);
|
|
216
|
+
const result = writeSpecDriveFeature(featureName, sourceDir, "completed");
|
|
217
|
+
migrated.push(result);
|
|
218
|
+
console.log(`✅ Migrated archive: ${featureName}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return migrated;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function updateWorkflowState(migrated) {
|
|
226
|
+
const statePath = path.join(sdriveRoot, "workflow-state.json");
|
|
227
|
+
|
|
228
|
+
let state = { features: [] };
|
|
229
|
+
if (fs.existsSync(statePath)) {
|
|
230
|
+
state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!state.onboarding) {
|
|
234
|
+
state.onboarding = {
|
|
235
|
+
mode: "brownfield",
|
|
236
|
+
date: new Date().toISOString(),
|
|
237
|
+
missing: [],
|
|
238
|
+
deferred: [],
|
|
239
|
+
retroactiveFeatures: [],
|
|
240
|
+
verifiedFeatures: [],
|
|
241
|
+
notes: ""
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!state.onboarding.migrated) {
|
|
246
|
+
state.onboarding.migrated = [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const item of migrated) {
|
|
250
|
+
// Add to features list if not present
|
|
251
|
+
const existing = state.features.find((f) => f.name === item.feature);
|
|
252
|
+
|
|
253
|
+
if (!existing) {
|
|
254
|
+
state.features.push({
|
|
255
|
+
name: item.feature,
|
|
256
|
+
currentVersion: "v1",
|
|
257
|
+
versions: [
|
|
258
|
+
{
|
|
259
|
+
id: "v1",
|
|
260
|
+
phase: item.lifecycle === "completed" ? "completed" : "ongoing",
|
|
261
|
+
currentStep: "review",
|
|
262
|
+
gates: {
|
|
263
|
+
specification: item.lifecycle === "completed" ? "approved" : "pending",
|
|
264
|
+
planning: item.lifecycle === "completed" ? "approved" : "pending",
|
|
265
|
+
implementation: item.lifecycle === "completed" ? "approved" : "pending",
|
|
266
|
+
review: item.lifecycle === "completed" ? "approved" : "pending"
|
|
267
|
+
},
|
|
268
|
+
history: [
|
|
269
|
+
{
|
|
270
|
+
event: "feature_migrated",
|
|
271
|
+
timestamp: new Date().toISOString(),
|
|
272
|
+
by: "onboarding"
|
|
273
|
+
}
|
|
274
|
+
]
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Add migration record
|
|
281
|
+
state.onboarding.migrated.push({
|
|
282
|
+
feature: item.feature,
|
|
283
|
+
from: "openspec",
|
|
284
|
+
date: new Date().toISOString(),
|
|
285
|
+
source: item.source
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Add to retroactive features
|
|
289
|
+
if (!state.onboarding.retroactiveFeatures.includes(item.feature)) {
|
|
290
|
+
state.onboarding.retroactiveFeatures.push(item.feature);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function main() {
|
|
298
|
+
console.log("🔍 Migrating from OpenSpec...\n");
|
|
299
|
+
|
|
300
|
+
const migrated = migrate();
|
|
301
|
+
|
|
302
|
+
if (migrated.length === 0) {
|
|
303
|
+
console.log("\n⚠️ No OpenSpec features found to migrate.");
|
|
304
|
+
process.exit(0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
updateWorkflowState(migrated);
|
|
308
|
+
|
|
309
|
+
console.log("");
|
|
310
|
+
console.log(`✅ Migration complete: ${migrated.length} features.`);
|
|
311
|
+
console.log("");
|
|
312
|
+
console.log("Original openspec/ folder was NOT deleted.");
|
|
313
|
+
console.log("All migrated features are marked retroactive: true.");
|
|
314
|
+
console.log("");
|
|
315
|
+
console.log("Next: run sdrive validate to confirm the new files.");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
main();
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const projectRoot = process.cwd();
|
|
7
|
+
const sdriveRoot = path.join(projectRoot, ".sdrive");
|
|
8
|
+
|
|
9
|
+
const speckitDir = path.join(projectRoot, ".specify");
|
|
10
|
+
const speckitSpecsDir = path.join(speckitDir, "specs");
|
|
11
|
+
const speckitMemoryDir = path.join(speckitDir, "memory");
|
|
12
|
+
const speckitConstitution = path.join(speckitMemoryDir, "constitution.md");
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(speckitDir)) {
|
|
15
|
+
console.error("❌ .specify/ folder not found. Nothing to migrate.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ensureDir(dir) {
|
|
20
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readFile(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
return fs.readFileSync(filePath, "utf8");
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listDirectories(dir) {
|
|
32
|
+
if (!fs.existsSync(dir)) return [];
|
|
33
|
+
return fs
|
|
34
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
35
|
+
.filter((d) => d.isDirectory())
|
|
36
|
+
.map((d) => d.name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function stripNumberPrefix(name) {
|
|
40
|
+
// Spec-Kit uses 001-feature-name
|
|
41
|
+
return name.replace(/^[0-9]+[-_]/, "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function slugify(name) {
|
|
45
|
+
return name
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
48
|
+
.replace(/^-+|-+$/g, "");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findInDir(dir, names) {
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
const full = path.join(dir, name);
|
|
54
|
+
if (fs.existsSync(full)) return full;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeSpecDriveFeature(featureName, sourceDir) {
|
|
60
|
+
const featureFolder = path.join(
|
|
61
|
+
sdriveRoot,
|
|
62
|
+
"specs",
|
|
63
|
+
"ongoing",
|
|
64
|
+
featureName,
|
|
65
|
+
"v1"
|
|
66
|
+
);
|
|
67
|
+
const govFolder = path.join(sdriveRoot, "governance", featureName, "v1");
|
|
68
|
+
|
|
69
|
+
ensureDir(featureFolder);
|
|
70
|
+
ensureDir(govFolder);
|
|
71
|
+
|
|
72
|
+
const specSource = findInDir(sourceDir, ["spec.md", "specification.md"]);
|
|
73
|
+
const planSource = findInDir(sourceDir, ["plan.md", "technical-plan.md"]);
|
|
74
|
+
const tasksSource = findInDir(sourceDir, ["tasks.md", "todo.md"]);
|
|
75
|
+
|
|
76
|
+
const now = new Date().toISOString();
|
|
77
|
+
|
|
78
|
+
const specContent = specSource
|
|
79
|
+
? readFile(specSource)
|
|
80
|
+
: `# ${featureName}\n\n(Migrated from Spec-Kit — no source spec found)`;
|
|
81
|
+
|
|
82
|
+
const planContent = planSource
|
|
83
|
+
? readFile(planSource)
|
|
84
|
+
: `# Technical Plan: ${featureName} (migrated)\n\nThis plan was migrated from Spec-Kit. No technical plan was extracted.\n`;
|
|
85
|
+
|
|
86
|
+
const tasksContent = tasksSource
|
|
87
|
+
? readFile(tasksSource)
|
|
88
|
+
: `# Execution Tasks: ${featureName}\n\n(Migrated from Spec-Kit — no tasks found)\n`;
|
|
89
|
+
|
|
90
|
+
// spec.md
|
|
91
|
+
fs.writeFileSync(
|
|
92
|
+
path.join(featureFolder, "spec.md"),
|
|
93
|
+
`---
|
|
94
|
+
title: ${featureName}
|
|
95
|
+
status: ongoing
|
|
96
|
+
version: v1
|
|
97
|
+
created: ${now}
|
|
98
|
+
updated: ${now}
|
|
99
|
+
retroactive: true
|
|
100
|
+
migratedFrom: speckit
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
${specContent}
|
|
104
|
+
`
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// plan.md
|
|
108
|
+
fs.writeFileSync(path.join(featureFolder, "plan.md"), planContent);
|
|
109
|
+
|
|
110
|
+
// tasks.md
|
|
111
|
+
fs.writeFileSync(path.join(featureFolder, "tasks.md"), tasksContent);
|
|
112
|
+
|
|
113
|
+
// governance JSON
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
path.join(govFolder, "spec.json"),
|
|
116
|
+
JSON.stringify(
|
|
117
|
+
{
|
|
118
|
+
feature: featureName,
|
|
119
|
+
title: featureName,
|
|
120
|
+
version: 1,
|
|
121
|
+
retroactive: true,
|
|
122
|
+
migratedFrom: "speckit",
|
|
123
|
+
overview: {
|
|
124
|
+
problemStatement: "(migrated from Spec-Kit)",
|
|
125
|
+
proposedSolution: "(migrated from Spec-Kit)",
|
|
126
|
+
targetAudience: "(migrated from Spec-Kit)"
|
|
127
|
+
},
|
|
128
|
+
userStories: [],
|
|
129
|
+
functionalRequirements: []
|
|
130
|
+
},
|
|
131
|
+
null,
|
|
132
|
+
2
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
fs.writeFileSync(
|
|
137
|
+
path.join(govFolder, "plan.json"),
|
|
138
|
+
JSON.stringify(
|
|
139
|
+
{
|
|
140
|
+
feature: featureName,
|
|
141
|
+
version: 1,
|
|
142
|
+
architecture: { approach: "(migrated)", components: [] },
|
|
143
|
+
dataModel: { entities: [], flowDescription: "(migrated)" }
|
|
144
|
+
},
|
|
145
|
+
null,
|
|
146
|
+
2
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
fs.writeFileSync(
|
|
151
|
+
path.join(govFolder, "tasks.json"),
|
|
152
|
+
JSON.stringify(
|
|
153
|
+
{ feature: featureName, version: 1, phases: [] },
|
|
154
|
+
null,
|
|
155
|
+
2
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
fs.writeFileSync(
|
|
160
|
+
path.join(govFolder, "traceability.json"),
|
|
161
|
+
JSON.stringify(
|
|
162
|
+
{ feature: featureName, version: 1, rows: [] },
|
|
163
|
+
null,
|
|
164
|
+
2
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
feature: featureName,
|
|
170
|
+
source: path.relative(projectRoot, sourceDir),
|
|
171
|
+
spec: specSource ? path.relative(projectRoot, specSource) : null,
|
|
172
|
+
plan: planSource ? path.relative(projectRoot, planSource) : null,
|
|
173
|
+
tasks: tasksSource ? path.relative(projectRoot, tasksSource) : null
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function migrateSpecs() {
|
|
178
|
+
const migrated = [];
|
|
179
|
+
|
|
180
|
+
if (!fs.existsSync(speckitSpecsDir)) {
|
|
181
|
+
return migrated;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const folders = listDirectories(speckitSpecsDir);
|
|
185
|
+
|
|
186
|
+
for (const folder of folders) {
|
|
187
|
+
const cleaned = stripNumberPrefix(folder);
|
|
188
|
+
const featureName = slugify(cleaned);
|
|
189
|
+
const sourceDir = path.join(speckitSpecsDir, folder);
|
|
190
|
+
|
|
191
|
+
const result = writeSpecDriveFeature(featureName, sourceDir);
|
|
192
|
+
migrated.push(result);
|
|
193
|
+
console.log(`✅ Migrated spec: ${featureName}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return migrated;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function migrateConstitution() {
|
|
200
|
+
if (!fs.existsSync(speckitConstitution)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const content = readFile(speckitConstitution);
|
|
205
|
+
if (!content) return false;
|
|
206
|
+
|
|
207
|
+
const targetConstitution = path.join(sdriveRoot, "constitution.md");
|
|
208
|
+
|
|
209
|
+
if (fs.existsSync(targetConstitution)) {
|
|
210
|
+
// Append instead of overwrite
|
|
211
|
+
const existing = readFile(targetConstitution) || "";
|
|
212
|
+
fs.writeFileSync(
|
|
213
|
+
targetConstitution,
|
|
214
|
+
`${existing}\n\n---\n\n## Migrated from Spec-Kit\n\n${content}\n`
|
|
215
|
+
);
|
|
216
|
+
console.log("✅ Appended Spec-Kit constitution to existing SpecDrive constitution.");
|
|
217
|
+
} else {
|
|
218
|
+
fs.writeFileSync(
|
|
219
|
+
targetConstitution,
|
|
220
|
+
`# Project Constitution\n\n> Migrated from Spec-Kit\n\n${content}\n`
|
|
221
|
+
);
|
|
222
|
+
console.log("✅ Created SpecDrive constitution from Spec-Kit constitution.");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function updateWorkflowState(migrated) {
|
|
229
|
+
const statePath = path.join(sdriveRoot, "workflow-state.json");
|
|
230
|
+
|
|
231
|
+
let state = { features: [] };
|
|
232
|
+
if (fs.existsSync(statePath)) {
|
|
233
|
+
state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!state.onboarding) {
|
|
237
|
+
state.onboarding = {
|
|
238
|
+
mode: "brownfield",
|
|
239
|
+
date: new Date().toISOString(),
|
|
240
|
+
missing: [],
|
|
241
|
+
deferred: [],
|
|
242
|
+
retroactiveFeatures: [],
|
|
243
|
+
verifiedFeatures: [],
|
|
244
|
+
notes: ""
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!state.onboarding.migrated) {
|
|
249
|
+
state.onboarding.migrated = [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const item of migrated) {
|
|
253
|
+
const existing = state.features.find((f) => f.name === item.feature);
|
|
254
|
+
|
|
255
|
+
if (!existing) {
|
|
256
|
+
state.features.push({
|
|
257
|
+
name: item.feature,
|
|
258
|
+
currentVersion: "v1",
|
|
259
|
+
versions: [
|
|
260
|
+
{
|
|
261
|
+
id: "v1",
|
|
262
|
+
phase: "ongoing",
|
|
263
|
+
currentStep: "review",
|
|
264
|
+
gates: {
|
|
265
|
+
specification: "pending",
|
|
266
|
+
planning: "pending",
|
|
267
|
+
implementation: "pending",
|
|
268
|
+
review: "pending"
|
|
269
|
+
},
|
|
270
|
+
history: [
|
|
271
|
+
{
|
|
272
|
+
event: "feature_migrated",
|
|
273
|
+
timestamp: new Date().toISOString(),
|
|
274
|
+
by: "onboarding"
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
state.onboarding.migrated.push({
|
|
283
|
+
feature: item.feature,
|
|
284
|
+
from: "speckit",
|
|
285
|
+
date: new Date().toISOString(),
|
|
286
|
+
source: item.source
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (!state.onboarding.retroactiveFeatures.includes(item.feature)) {
|
|
290
|
+
state.onboarding.retroactiveFeatures.push(item.feature);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function main() {
|
|
298
|
+
console.log("🔍 Migrating from Spec-Kit...\n");
|
|
299
|
+
|
|
300
|
+
const migrated = migrateSpecs();
|
|
301
|
+
const constitutionMigrated = migrateConstitution();
|
|
302
|
+
|
|
303
|
+
if (migrated.length === 0 && !constitutionMigrated) {
|
|
304
|
+
console.log("\n⚠️ No Spec-Kit features or constitution found to migrate.");
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
updateWorkflowState(migrated);
|
|
309
|
+
|
|
310
|
+
console.log("");
|
|
311
|
+
console.log(`✅ Migration complete: ${migrated.length} features.`);
|
|
312
|
+
if (constitutionMigrated) {
|
|
313
|
+
console.log("✅ Constitution merged.");
|
|
314
|
+
}
|
|
315
|
+
console.log("");
|
|
316
|
+
console.log("Original .specify/ folder was NOT deleted.");
|
|
317
|
+
console.log("All migrated features are marked retroactive: true.");
|
|
318
|
+
console.log("");
|
|
319
|
+
console.log("Next: run sdrive validate to confirm the new files.");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
main();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
console.error("⚠️ `sdrive migrate` is deprecated.");
|
|
4
|
+
console.error("");
|
|
5
|
+
console.error("Migration now happens during Brownfield Onboarding.");
|
|
6
|
+
console.error("");
|
|
7
|
+
console.error("Run:");
|
|
8
|
+
console.error(" sdrive onboard");
|
|
9
|
+
console.error(" /sdrive:onboard");
|
|
10
|
+
console.error("");
|
|
11
|
+
console.error("The Onboarding Agent will detect OpenSpec or Spec-Kit and offer to migrate.");
|
|
12
|
+
process.exit(1);
|