specdrive-cli 0.1.10 → 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.
Files changed (74) hide show
  1. package/README.md +955 -697
  2. package/agents/00-onboarding.md +261 -0
  3. package/agents/01-constitution.md +214 -201
  4. package/agents/02-specification.md +249 -226
  5. package/agents/03-uiux.md +156 -144
  6. package/agents/04-cascade.md +151 -122
  7. package/agents/05-discover-skills.md +136 -136
  8. package/agents/06-documentation.md +158 -145
  9. package/agents/07-implementation.md +201 -169
  10. package/agents/08-performance.md +179 -165
  11. package/agents/09-review-complete.md +239 -168
  12. package/agents/10-security.md +180 -167
  13. package/agents/11-test.md +195 -0
  14. package/commands/gates.js +73 -73
  15. package/commands/manifest.json +113 -95
  16. package/commands/permissions.json +39 -0
  17. package/commands/router.js +151 -127
  18. package/commands/tools.json +19 -19
  19. package/dashboard/app.js +394 -0
  20. package/dashboard/index.html +74 -0
  21. package/dashboard/server.js +166 -0
  22. package/dashboard/style.css +157 -0
  23. package/mcp/mcp.json +31 -0
  24. package/mcp/server.js +108 -0
  25. package/package.json +35 -32
  26. package/schemas/config.schema.json +20 -0
  27. package/schemas/workflow-state.schema.json +149 -38
  28. package/scripts/anti-redundancy.js +176 -176
  29. package/scripts/audit-log.js +46 -46
  30. package/scripts/check-permission.js +87 -0
  31. package/scripts/diff-spec.js +50 -50
  32. package/scripts/diff-version.js +96 -0
  33. package/scripts/generate-adapters.js +80 -80
  34. package/scripts/generate-from-template.js +97 -97
  35. package/scripts/generate-openapi.js +75 -75
  36. package/scripts/github-team-sync.js +80 -80
  37. package/scripts/install-hooks.js +20 -20
  38. package/scripts/load-plugins.js +65 -65
  39. package/scripts/migrate-openspec.js +318 -0
  40. package/scripts/migrate-speckit.js +322 -0
  41. package/scripts/migrate.js +12 -62
  42. package/scripts/onboard.js +312 -0
  43. package/scripts/pre-commit.js +56 -20
  44. package/scripts/team.js +113 -113
  45. package/scripts/test-adapters.js +118 -118
  46. package/scripts/test-create.js +13 -13
  47. package/scripts/test-end-to-end.js +137 -137
  48. package/scripts/test-router.js +110 -110
  49. package/scripts/test-state-transitions.js +146 -146
  50. package/scripts/test-validator.js +152 -152
  51. package/scripts/validate-config.js +36 -0
  52. package/scripts/validate-governance.js +150 -130
  53. package/scripts/verify.js +525 -0
  54. package/scripts/version-new.js +202 -0
  55. package/src/index.js +1010 -807
  56. package/templates/expo/plan.json +12 -0
  57. package/templates/expo/spec.json +12 -0
  58. package/templates/expo/tasks.json +5 -0
  59. package/templates/fastapi/plan.json +12 -0
  60. package/templates/fastapi/spec.json +12 -0
  61. package/templates/fastapi/tasks.json +5 -0
  62. package/templates/generic/plan.json +12 -0
  63. package/templates/generic/spec.json +11 -0
  64. package/templates/generic/tasks.json +5 -0
  65. package/templates/nextjs/plan.json +23 -0
  66. package/templates/nextjs/spec.json +12 -0
  67. package/templates/nextjs/tasks.json +5 -0
  68. package/templates/react-node/plan.json +15 -0
  69. package/templates/react-node/spec.json +12 -0
  70. package/templates/react-node/tasks.json +5 -0
  71. package/templates/registry.json +30 -0
  72. package/templates/turborepo/plan.json +12 -0
  73. package/templates/turborepo/spec.json +12 -0
  74. package/templates/turborepo/tasks.json +5 -0
@@ -1,81 +1,81 @@
1
- #!/usr/bin/env node
2
-
3
- const { execSync } = require("child_process");
4
- const fs = require("fs");
5
- const path = require("path");
6
-
7
- const projectRoot = process.cwd();
8
-
9
- function getRepoInfo() {
10
- try {
11
- const remote = execSync("git config --get remote.origin.url", {
12
- stdio: "pipe",
13
- encoding: "utf8"
14
- }).trim();
15
-
16
- const match = remote.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/);
17
-
18
- if (!match) {
19
- console.error("❌ Could not detect GitHub repository.");
20
- return null;
21
- }
22
-
23
- return { owner: match[1], repo: match[2] };
24
- } catch (error) {
25
- console.error("❌ Git not initialized or remote missing.");
26
- return null;
27
- }
28
- }
29
-
30
- function fetchCollaborators(owner, repo) {
31
- try {
32
- const result = execSync(`gh api repos/${owner}/${repo}/collaborators`, {
33
- stdio: "pipe",
34
- encoding: "utf8"
35
- });
36
-
37
- return JSON.parse(result);
38
- } catch (error) {
39
- console.error("❌ Could not fetch collaborators. Is gh CLI installed and authenticated?");
40
- return null;
41
- }
42
- }
43
-
44
- function syncTeam() {
45
- const repoInfo = getRepoInfo();
46
-
47
- if (!repoInfo) {
48
- process.exit(1);
49
- }
50
-
51
- const collaborators = fetchCollaborators(repoInfo.owner, repoInfo.repo);
52
-
53
- if (!collaborators) {
54
- process.exit(1);
55
- }
56
-
57
- const teamPath = path.join(projectRoot, ".sdrive", "team.json");
58
-
59
- if (!fs.existsSync(teamPath)) {
60
- console.error("❌ team.json not found. Run `sdrive init` first.");
61
- process.exit(1);
62
- }
63
-
64
- const team = JSON.parse(fs.readFileSync(teamPath, "utf8"));
65
-
66
- collaborators.forEach((user) => {
67
- const existing = team.members.find((m) => m.username === user.login);
68
-
69
- if (!existing) {
70
- team.members.push({
71
- username: user.login,
72
- role: user.permissions?.admin ? "admin" : "developer"
73
- });
74
- }
75
- });
76
-
77
- fs.writeFileSync(teamPath, JSON.stringify(team, null, 2));
78
- console.log(`✅ Synced ${collaborators.length} collaborators from GitHub.`);
79
- }
80
-
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync } = require("child_process");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const projectRoot = process.cwd();
8
+
9
+ function getRepoInfo() {
10
+ try {
11
+ const remote = execSync("git config --get remote.origin.url", {
12
+ stdio: "pipe",
13
+ encoding: "utf8"
14
+ }).trim();
15
+
16
+ const match = remote.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/);
17
+
18
+ if (!match) {
19
+ console.error("❌ Could not detect GitHub repository.");
20
+ return null;
21
+ }
22
+
23
+ return { owner: match[1], repo: match[2] };
24
+ } catch (error) {
25
+ console.error("❌ Git not initialized or remote missing.");
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function fetchCollaborators(owner, repo) {
31
+ try {
32
+ const result = execSync(`gh api repos/${owner}/${repo}/collaborators`, {
33
+ stdio: "pipe",
34
+ encoding: "utf8"
35
+ });
36
+
37
+ return JSON.parse(result);
38
+ } catch (error) {
39
+ console.error("❌ Could not fetch collaborators. Is gh CLI installed and authenticated?");
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function syncTeam() {
45
+ const repoInfo = getRepoInfo();
46
+
47
+ if (!repoInfo) {
48
+ process.exit(1);
49
+ }
50
+
51
+ const collaborators = fetchCollaborators(repoInfo.owner, repoInfo.repo);
52
+
53
+ if (!collaborators) {
54
+ process.exit(1);
55
+ }
56
+
57
+ const teamPath = path.join(projectRoot, ".sdrive", "team.json");
58
+
59
+ if (!fs.existsSync(teamPath)) {
60
+ console.error("❌ team.json not found. Run `sdrive init` first.");
61
+ process.exit(1);
62
+ }
63
+
64
+ const team = JSON.parse(fs.readFileSync(teamPath, "utf8"));
65
+
66
+ collaborators.forEach((user) => {
67
+ const existing = team.members.find((m) => m.username === user.login);
68
+
69
+ if (!existing) {
70
+ team.members.push({
71
+ username: user.login,
72
+ role: user.permissions?.admin ? "admin" : "developer"
73
+ });
74
+ }
75
+ });
76
+
77
+ fs.writeFileSync(teamPath, JSON.stringify(team, null, 2));
78
+ console.log(`✅ Synced ${collaborators.length} collaborators from GitHub.`);
79
+ }
80
+
81
81
  syncTeam();
@@ -1,21 +1,21 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- const projectRoot = process.cwd();
7
- const hooksDir = path.join(projectRoot, ".git", "hooks");
8
- const preCommitPath = path.join(hooksDir, "pre-commit");
9
-
10
- const hookContent = `#!/bin/sh
11
- node .sdrive/scripts/pre-commit.js
12
- `;
13
-
14
- if (!fs.existsSync(hooksDir)) {
15
- console.error("❌ .git/hooks not found. Are you in a Git repository?");
16
- process.exit(1);
17
- }
18
-
19
- fs.writeFileSync(preCommitPath, hookContent, { mode: 0o755 });
20
- console.log("✅ Installed SpecDrive pre-commit hook.");
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const projectRoot = process.cwd();
7
+ const hooksDir = path.join(projectRoot, ".git", "hooks");
8
+ const preCommitPath = path.join(hooksDir, "pre-commit");
9
+
10
+ const hookContent = `#!/bin/sh
11
+ node .sdrive/scripts/pre-commit.js
12
+ `;
13
+
14
+ if (!fs.existsSync(hooksDir)) {
15
+ console.error("❌ .git/hooks not found. Are you in a Git repository?");
16
+ process.exit(1);
17
+ }
18
+
19
+ fs.writeFileSync(preCommitPath, hookContent, { mode: 0o755 });
20
+ console.log("✅ Installed SpecDrive pre-commit hook.");
21
21
  console.log(" Governance validation will now run before every commit.");
@@ -1,66 +1,66 @@
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
- try {
26
- const pluginDef = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
27
- plugins.push({
28
- ...pluginDef,
29
- path: path.join(pluginsDir, folder)
30
- });
31
- } catch (error) {
32
- console.warn(`⚠️ Skipping invalid plugin: ${folder}`);
33
- }
34
- });
35
-
36
- return plugins;
37
- }
38
-
39
- function runPlugin(pluginName) {
40
- const plugins = loadPlugins();
41
- const plugin = plugins.find((p) => p.name === pluginName);
42
-
43
- if (!plugin) {
44
- console.error(`❌ Plugin "${pluginName}" not found.`);
45
- process.exit(1);
46
- }
47
-
48
- const entryPath = path.join(plugin.path, plugin.entry);
49
-
50
- if (!fs.existsSync(entryPath)) {
51
- console.error(`❌ Plugin entry not found: ${plugin.entry}`);
52
- process.exit(1);
53
- }
54
-
55
- require(entryPath);
56
- }
57
-
58
- function listPlugins() {
59
- return loadPlugins();
60
- }
61
-
62
- module.exports = {
63
- loadPlugins,
64
- runPlugin,
65
- listPlugins
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
+ try {
26
+ const pluginDef = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
27
+ plugins.push({
28
+ ...pluginDef,
29
+ path: path.join(pluginsDir, folder)
30
+ });
31
+ } catch (error) {
32
+ console.warn(`⚠️ Skipping invalid plugin: ${folder}`);
33
+ }
34
+ });
35
+
36
+ return plugins;
37
+ }
38
+
39
+ function runPlugin(pluginName) {
40
+ const plugins = loadPlugins();
41
+ const plugin = plugins.find((p) => p.name === pluginName);
42
+
43
+ if (!plugin) {
44
+ console.error(`❌ Plugin "${pluginName}" not found.`);
45
+ process.exit(1);
46
+ }
47
+
48
+ const entryPath = path.join(plugin.path, plugin.entry);
49
+
50
+ if (!fs.existsSync(entryPath)) {
51
+ console.error(`❌ Plugin entry not found: ${plugin.entry}`);
52
+ process.exit(1);
53
+ }
54
+
55
+ require(entryPath);
56
+ }
57
+
58
+ function listPlugins() {
59
+ return loadPlugins();
60
+ }
61
+
62
+ module.exports = {
63
+ loadPlugins,
64
+ runPlugin,
65
+ listPlugins
66
66
  };
@@ -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();