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.
- package/README.md +955 -697
- 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 +149 -38
- 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 +50 -50
- package/scripts/diff-version.js +96 -0
- package/scripts/generate-adapters.js +80 -80
- package/scripts/generate-from-template.js +97 -97
- package/scripts/generate-openapi.js +75 -75
- package/scripts/github-team-sync.js +80 -80
- package/scripts/install-hooks.js +20 -20
- 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 -62
- 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 -807
- 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
|
@@ -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();
|
package/scripts/install-hooks.js
CHANGED
|
@@ -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.");
|
package/scripts/load-plugins.js
CHANGED
|
@@ -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();
|