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.
Files changed (74) hide show
  1. package/README.md +955 -506
  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 +152 -35
  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 +51 -0
  32. package/scripts/diff-version.js +96 -0
  33. package/scripts/generate-adapters.js +80 -80
  34. package/scripts/generate-from-template.js +97 -70
  35. package/scripts/generate-openapi.js +76 -0
  36. package/scripts/github-team-sync.js +81 -0
  37. package/scripts/install-hooks.js +21 -0
  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 -0
  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 -400
  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,177 +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", ".py", ".go", ".rs"]) {
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((p) => p.includes(item.name));
101
- if (exists) {
102
- location = existingAssets.components.find((p) => p.includes(item.name));
103
- }
104
- break;
105
- case "service":
106
- case "api":
107
- exists = existingAssets.services.some((p) => p.includes(item.name));
108
- if (exists) {
109
- location = existingAssets.services.find((p) => p.includes(item.name));
110
- }
111
- break;
112
- case "hook":
113
- exists = existingAssets.hooks.some((p) => p.includes(item.name));
114
- if (exists) {
115
- location = existingAssets.hooks.find((p) => p.includes(item.name));
116
- }
117
- break;
118
- case "utility":
119
- exists = existingAssets.utils.some((p) => p.includes(item.name));
120
- if (exists) {
121
- location = existingAssets.utils.find((p) => p.includes(item.name));
122
- }
123
- break;
124
- case "model":
125
- case "entity":
126
- exists = existingAssets.models.some((p) => p.includes(item.name));
127
- if (exists) {
128
- location = existingAssets.models.find((p) => p.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
-
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", ".py", ".go", ".rs"]) {
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((p) => p.includes(item.name));
101
+ if (exists) {
102
+ location = existingAssets.components.find((p) => p.includes(item.name));
103
+ }
104
+ break;
105
+ case "service":
106
+ case "api":
107
+ exists = existingAssets.services.some((p) => p.includes(item.name));
108
+ if (exists) {
109
+ location = existingAssets.services.find((p) => p.includes(item.name));
110
+ }
111
+ break;
112
+ case "hook":
113
+ exists = existingAssets.hooks.some((p) => p.includes(item.name));
114
+ if (exists) {
115
+ location = existingAssets.hooks.find((p) => p.includes(item.name));
116
+ }
117
+ break;
118
+ case "utility":
119
+ exists = existingAssets.utils.some((p) => p.includes(item.name));
120
+ if (exists) {
121
+ location = existingAssets.utils.find((p) => p.includes(item.name));
122
+ }
123
+ break;
124
+ case "model":
125
+ case "entity":
126
+ exists = existingAssets.models.some((p) => p.includes(item.name));
127
+ if (exists) {
128
+ location = existingAssets.models.find((p) => p.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
177
  main();
@@ -1,47 +1,47 @@
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
-
12
- try {
13
- return JSON.parse(fs.readFileSync(auditLogPath, "utf8"));
14
- } catch (error) {
15
- console.error(`⚠️ Could not parse audit log: ${error.message}`);
16
- return { events: [] };
17
- }
18
- }
19
-
20
- function saveAuditLog(log) {
21
- fs.writeFileSync(auditLogPath, JSON.stringify(log, null, 2));
22
- }
23
-
24
- function logEvent(event, details = {}) {
25
- const log = loadAuditLog();
26
-
27
- log.events.push({
28
- timestamp: new Date().toISOString(),
29
- ...details,
30
- event
31
- });
32
-
33
- saveAuditLog(log);
34
- console.log(`📝 Audit: ${event}`);
35
- }
36
-
37
- function getRecentEvents(count = 10) {
38
- const log = loadAuditLog();
39
- return log.events.slice(-count).reverse();
40
- }
41
-
42
- module.exports = {
43
- logEvent,
44
- loadAuditLog,
45
- saveAuditLog,
46
- getRecentEvents
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
+
12
+ try {
13
+ return JSON.parse(fs.readFileSync(auditLogPath, "utf8"));
14
+ } catch (error) {
15
+ console.error(`⚠️ Could not parse audit log: ${error.message}`);
16
+ return { events: [] };
17
+ }
18
+ }
19
+
20
+ function saveAuditLog(log) {
21
+ fs.writeFileSync(auditLogPath, JSON.stringify(log, null, 2));
22
+ }
23
+
24
+ function logEvent(event, details = {}) {
25
+ const log = loadAuditLog();
26
+
27
+ log.events.push({
28
+ timestamp: new Date().toISOString(),
29
+ ...details,
30
+ event
31
+ });
32
+
33
+ saveAuditLog(log);
34
+ console.log(`📝 Audit: ${event}`);
35
+ }
36
+
37
+ function getRecentEvents(count = 10) {
38
+ const log = loadAuditLog();
39
+ return log.events.slice(-count).reverse();
40
+ }
41
+
42
+ module.exports = {
43
+ logEvent,
44
+ loadAuditLog,
45
+ saveAuditLog,
46
+ getRecentEvents
47
47
  };
@@ -0,0 +1,87 @@
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 getCurrentUser() {
10
+ try {
11
+ return execSync("git config user.name", { encoding: "utf8" }).trim();
12
+ } catch (error) {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ function loadTeam() {
18
+ const teamPath = path.join(projectRoot, ".sdrive", "team.json");
19
+
20
+ if (!fs.existsSync(teamPath)) {
21
+ console.error("❌ team.json not found.");
22
+ process.exit(1);
23
+ }
24
+
25
+ return JSON.parse(fs.readFileSync(teamPath, "utf8"));
26
+ }
27
+
28
+ function loadPermissions() {
29
+ const permPath = path.join(projectRoot, ".sdrive", "commands", "permissions.json");
30
+
31
+ if (!fs.existsSync(permPath)) {
32
+ console.error("❌ permissions.json not found.");
33
+ process.exit(1);
34
+ }
35
+
36
+ return JSON.parse(fs.readFileSync(permPath, "utf8"));
37
+ }
38
+
39
+ function getUserRole(username) {
40
+ const team = loadTeam();
41
+ const member = team.members?.find((m) => m.username === username);
42
+ return member ? member.role : null;
43
+ }
44
+
45
+ function checkPermission(commandType, commandName) {
46
+ const username = getCurrentUser();
47
+
48
+ if (!username) {
49
+ console.error("❌ Could not detect Git user. Set git config user.name.");
50
+ process.exit(1);
51
+ }
52
+
53
+ const role = getUserRole(username);
54
+
55
+ if (!role) {
56
+ console.error(`❌ User "${username}" is not in team.json.`);
57
+ process.exit(1);
58
+ }
59
+
60
+ const permissions = loadPermissions();
61
+ const rules = commandType === "cli" ? permissions.cli : permissions.rules;
62
+ const allowedRoles = rules[commandName];
63
+
64
+ if (!allowedRoles) {
65
+ console.log(`⚠️ No permission rule for ${commandType}:${commandName}. Allowing by default.`);
66
+ process.exit(0);
67
+ }
68
+
69
+ if (!allowedRoles.includes(role)) {
70
+ console.error(`❌ User "${username}" (${role}) is not allowed to run ${commandType}:${commandName}.`);
71
+ console.error(` Allowed roles: ${allowedRoles.join(", ")}`);
72
+ process.exit(1);
73
+ }
74
+
75
+ console.log(`✅ User "${username}" (${role}) is allowed.`);
76
+ process.exit(0);
77
+ }
78
+
79
+ const commandType = process.argv[2];
80
+ const commandName = process.argv[3];
81
+
82
+ if (!commandType || !commandName) {
83
+ console.error("❌ Usage: node check-permission.js <cli|slash> <command>");
84
+ process.exit(1);
85
+ }
86
+
87
+ checkPermission(commandType, commandName);
@@ -0,0 +1,51 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const projectRoot = process.cwd();
5
+
6
+ function readLines(filePath) {
7
+ if (!fs.existsSync(filePath)) {
8
+ return [];
9
+ }
10
+ return fs.readFileSync(filePath, "utf8").split("\n");
11
+ }
12
+
13
+ function simpleDiff(oldFile, newFile) {
14
+ const oldLines = readLines(oldFile);
15
+ const newLines = readLines(newFile);
16
+
17
+ const oldSet = new Set(oldLines);
18
+ const newSet = new Set(newLines);
19
+
20
+ const added = newLines.filter((line) => !oldSet.has(line));
21
+ const removed = oldLines.filter((line) => !newSet.has(line));
22
+
23
+ console.log(`\n📄 Diff between:`);
24
+ console.log(` Old: ${path.basename(oldFile)}`);
25
+ console.log(` New: ${path.basename(newFile)}\n`);
26
+
27
+ if (added.length === 0 && removed.length === 0) {
28
+ console.log("✅ No changes detected.");
29
+ return;
30
+ }
31
+
32
+ if (removed.length > 0) {
33
+ console.log("❌ Removed:");
34
+ removed.forEach((line) => console.log(` - ${line}`));
35
+ }
36
+
37
+ if (added.length > 0) {
38
+ console.log("✅ Added:");
39
+ added.forEach((line) => console.log(` + ${line}`));
40
+ }
41
+ }
42
+
43
+ const oldFile = process.argv[2];
44
+ const newFile = process.argv[3];
45
+
46
+ if (!oldFile || !newFile) {
47
+ console.error("❌ Usage: node diff-spec.js <old-file> <new-file>");
48
+ process.exit(1);
49
+ }
50
+
51
+ simpleDiff(path.resolve(oldFile), path.resolve(newFile));
@@ -0,0 +1,96 @@
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 featureName = process.argv[2];
10
+ const versionA = process.argv[3];
11
+ const versionB = process.argv[4];
12
+
13
+ if (!featureName || !versionA || !versionB) {
14
+ console.error("❌ Usage: node diff-version.js <feature> <v1> <v2>");
15
+ process.exit(1);
16
+ }
17
+
18
+ function findFile(baseDir, version, fileName) {
19
+ const candidate = path.join(baseDir, featureName, version, fileName);
20
+
21
+ if (fs.existsSync(candidate)) {
22
+ return candidate;
23
+ }
24
+
25
+ return null;
26
+ }
27
+
28
+ function readLines(filePath) {
29
+ return fs.readFileSync(filePath, "utf8").split("\n");
30
+ }
31
+
32
+ function simpleDiff(oldLines, newLines) {
33
+ const added = [];
34
+ const removed = [];
35
+ const unchanged = [];
36
+
37
+ const oldSet = new Set(oldLines);
38
+ const newSet = new Set(newLines);
39
+
40
+ newLines.forEach((line) => {
41
+ if (oldSet.has(line)) {
42
+ unchanged.push(line);
43
+ } else {
44
+ added.push(line);
45
+ }
46
+ });
47
+
48
+ oldLines.forEach((line) => {
49
+ if (!newSet.has(line)) {
50
+ removed.push(line);
51
+ }
52
+ });
53
+
54
+ return { added, removed, unchanged };
55
+ }
56
+
57
+ function diffFile(fileName, label) {
58
+ const specA = findFile(path.join(sdriveRoot, "specs", "ongoing"), versionA, fileName);
59
+ const specB = findFile(path.join(sdriveRoot, "specs", "ongoing"), versionB, fileName);
60
+
61
+ if (!specA || !specB) {
62
+ console.log(`\n⚠️ ${label}: not both versions have this file. Skipping.`);
63
+ return;
64
+ }
65
+
66
+ const oldLines = readLines(specA);
67
+ const newLines = readLines(specB);
68
+ const { added, removed } = simpleDiff(oldLines, newLines);
69
+
70
+ console.log(`\n📄 ${label} (${versionA} → ${versionB})`);
71
+
72
+ if (added.length === 0 && removed.length === 0) {
73
+ console.log(" ✅ No changes.");
74
+ return;
75
+ }
76
+
77
+ if (removed.length > 0) {
78
+ console.log(" ❌ Removed:");
79
+ removed.forEach((line) => console.log(` - ${line}`));
80
+ }
81
+
82
+ if (added.length > 0) {
83
+ console.log(" ✅ Added:");
84
+ added.forEach((line) => console.log(` + ${line}`));
85
+ }
86
+ }
87
+
88
+ function main() {
89
+ console.log(`\n🔍 Diff: ${featureName} (${versionA} → ${versionB})`);
90
+
91
+ diffFile("spec.md", "Specification");
92
+ diffFile("plan.md", "Plan");
93
+ diffFile("tasks.md", "Tasks");
94
+ }
95
+
96
+ main();