specdrive-cli 0.1.2 → 0.1.8

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.
@@ -1,38 +1,80 @@
1
1
  {
2
- "": "http://json-schema.org/draft-07/schema#",
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "title": "Execution Tasks",
4
4
  "type": "object",
5
5
  "properties": {
6
- "feature": { "type": "string", "pattern": "^[a-z0-9-]+$" },
7
- "version": { "type": "integer", "minimum": 1 },
6
+ "feature": {
7
+ "type": "string",
8
+ "pattern": "^[a-z0-9-]+$"
9
+ },
10
+ "version": {
11
+ "type": "integer",
12
+ "minimum": 1
13
+ },
8
14
  "phases": {
9
15
  "type": "array",
10
16
  "minItems": 1,
11
17
  "items": {
12
18
  "type": "object",
13
19
  "properties": {
14
- "phaseId": { "type": "string", "pattern": "^P[0-9]+$" },
15
- "name": { "type": "string" },
20
+ "phaseId": {
21
+ "type": "string",
22
+ "pattern": "^P[0-9]+$"
23
+ },
24
+ "name": {
25
+ "type": "string",
26
+ "minLength": 3
27
+ },
28
+ "dependencies": {
29
+ "type": "array",
30
+ "items": {
31
+ "type": "string",
32
+ "pattern": "^P[0-9]+$"
33
+ }
34
+ },
16
35
  "tasks": {
17
36
  "type": "array",
18
37
  "minItems": 1,
19
38
  "items": {
20
39
  "type": "object",
21
40
  "properties": {
22
- "id": { "type": "string", "pattern": "^T[0-9]+\\.[0-9]+$" },
23
- "title": { "type": "string" },
24
- "description": { "type": "string" },
41
+ "id": {
42
+ "type": "string",
43
+ "pattern": "^T[0-9]+\\.[0-9]+$"
44
+ },
45
+ "title": {
46
+ "type": "string",
47
+ "minLength": 5
48
+ },
49
+ "description": {
50
+ "type": "string"
51
+ },
25
52
  "linkedRequirements": {
26
53
  "type": "array",
27
- "items": { "type": "string", "pattern": "^(FR|NFR)-[0-9]{3}$" }
54
+ "items": {
55
+ "type": "string",
56
+ "pattern": "^(FR|NFR)-[0-9]{3}$"
57
+ },
58
+ "minItems": 1
28
59
  },
29
60
  "linkedAcceptanceCriteria": {
30
61
  "type": "array",
31
- "items": { "type": "string", "pattern": "^AC-[0-9]{3}$" }
62
+ "items": {
63
+ "type": "string",
64
+ "pattern": "^AC-[0-9]{3}$"
65
+ }
32
66
  },
33
- "completed": { "type": "boolean", "default": false }
67
+ "completed": {
68
+ "type": "boolean",
69
+ "default": false
70
+ }
34
71
  },
35
- "required": ["id", "title", "linkedRequirements"]
72
+ "required": [
73
+ "id",
74
+ "title",
75
+ "linkedRequirements",
76
+ "completed"
77
+ ]
36
78
  }
37
79
  }
38
80
  },
@@ -40,5 +82,6 @@
40
82
  }
41
83
  }
42
84
  },
43
- "required": ["feature", "version", "phases"]
44
- }
85
+ "required": ["feature", "version", "phases"],
86
+ "additionalProperties": false
87
+ }
@@ -1,34 +1,63 @@
1
1
  {
2
- "": "http://json-schema.org/draft-07/schema#",
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "title": "Traceability Matrix",
4
4
  "type": "object",
5
5
  "properties": {
6
- "feature": { "type": "string", "pattern": "^[a-z0-9-]+$" },
7
- "version": { "type": "integer", "minimum": 1 },
6
+ "feature": {
7
+ "type": "string",
8
+ "pattern": "^[a-z0-9-]+$"
9
+ },
10
+ "version": {
11
+ "type": "integer",
12
+ "minimum": 1
13
+ },
8
14
  "rows": {
9
15
  "type": "array",
16
+ "minItems": 1,
10
17
  "items": {
11
18
  "type": "object",
12
19
  "properties": {
13
- "requirementId": { "type": "string", "pattern": "^(FR|NFR)-[0-9]{3}$" },
20
+ "requirementId": {
21
+ "type": "string",
22
+ "pattern": "^(FR|NFR)-[0-9]{3}$"
23
+ },
14
24
  "acceptanceCriteriaIds": {
15
25
  "type": "array",
16
- "items": { "type": "string", "pattern": "^AC-[0-9]{3}$" }
26
+ "items": {
27
+ "type": "string",
28
+ "pattern": "^AC-[0-9]{3}$"
29
+ }
30
+ },
31
+ "planSection": {
32
+ "type": "string"
17
33
  },
18
- "planSection": { "type": "string" },
19
34
  "taskIds": {
20
35
  "type": "array",
21
- "items": { "type": "string", "pattern": "^T[0-9]+\\.[0-9]+$" }
36
+ "items": {
37
+ "type": "string",
38
+ "pattern": "^T[0-9]+\\.[0-9]+$"
39
+ }
22
40
  },
23
41
  "testIds": {
24
42
  "type": "array",
25
- "items": { "type": "string", "pattern": "^TEST-[0-9]{3}$" }
43
+ "items": {
44
+ "type": "string",
45
+ "pattern": "^TEST-[0-9]{3}$"
46
+ }
26
47
  },
27
- "status": { "enum": ["not_started", "in_progress", "complete"] }
48
+ "status": {
49
+ "enum": ["not_started", "in_progress", "complete"]
50
+ }
28
51
  },
29
- "required": ["requirementId", "status"]
52
+ "required": [
53
+ "requirementId",
54
+ "acceptanceCriteriaIds",
55
+ "taskIds",
56
+ "status"
57
+ ]
30
58
  }
31
59
  }
32
60
  },
33
- "required": ["feature", "version", "rows"]
34
- }
61
+ "required": ["feature", "version", "rows"],
62
+ "additionalProperties": false
63
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "": "http://json-schema.org/draft-07/schema#",
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "title": "Workflow State",
4
4
  "type": "object",
5
5
  "properties": {
@@ -8,26 +8,52 @@
8
8
  "items": {
9
9
  "type": "object",
10
10
  "properties": {
11
- "name": { "type": "string", "pattern": "^[a-z0-9-]+$" },
12
- "phase": { "enum": ["backlog", "ongoing", "completed", "blocked"] },
13
- "currentStep": { "enum": ["specification", "planning", "implementation", "review"] },
11
+ "name": {
12
+ "type": "string",
13
+ "pattern": "^[a-z0-9-]+$"
14
+ },
15
+ "phase": {
16
+ "enum": ["backlog", "ongoing", "completed", "blocked"]
17
+ },
18
+ "currentStep": {
19
+ "enum": ["specification", "planning", "implementation", "review"]
20
+ },
14
21
  "gates": {
15
22
  "type": "object",
16
23
  "properties": {
17
- "specification": { "enum": ["pending", "approved", "rejected"] },
18
- "planning": { "enum": ["pending", "approved", "rejected"] },
19
- "implementation": { "enum": ["pending", "in_progress", "approved", "rejected"] },
20
- "review": { "enum": ["pending", "approved", "rejected"] }
24
+ "specification": {
25
+ "enum": ["pending", "approved", "rejected"]
26
+ },
27
+ "planning": {
28
+ "enum": ["pending", "approved", "rejected"]
29
+ },
30
+ "implementation": {
31
+ "enum": ["pending", "in_progress", "approved", "rejected"]
32
+ },
33
+ "review": {
34
+ "enum": ["pending", "approved", "rejected"]
35
+ }
21
36
  },
22
- "required": ["specification", "planning", "implementation", "review"]
37
+ "required": [
38
+ "specification",
39
+ "planning",
40
+ "implementation",
41
+ "review"
42
+ ]
23
43
  },
24
44
  "history": {
25
45
  "type": "array",
26
46
  "items": {
27
47
  "type": "object",
28
48
  "properties": {
29
- "event": { "type": "string" },
30
- "timestamp": { "type": "string", "format": "date-time" }
49
+ "event": {
50
+ "type": "string",
51
+ "minLength": 3
52
+ },
53
+ "timestamp": {
54
+ "type": "string",
55
+ "format": "date-time"
56
+ }
31
57
  },
32
58
  "required": ["event", "timestamp"]
33
59
  }
@@ -37,5 +63,6 @@
37
63
  }
38
64
  }
39
65
  },
40
- "required": ["features"]
41
- }
66
+ "required": ["features"],
67
+ "additionalProperties": false
68
+ }
@@ -24,7 +24,7 @@ function getInstalledDependencies() {
24
24
  ];
25
25
  }
26
26
 
27
- function listFiles(dir, extensions = [".ts", ".tsx", ".js", ".jsx"]) {
27
+ function listFiles(dir, extensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]) {
28
28
  if (!fs.existsSync(dir)) return [];
29
29
 
30
30
  let results = [];
@@ -97,35 +97,35 @@ function checkRedundancy(plannedItems) {
97
97
 
98
98
  switch (item.type) {
99
99
  case "component":
100
- exists = existingAssets.components.some((path) => path.includes(item.name));
100
+ exists = existingAssets.components.some((p) => p.includes(item.name));
101
101
  if (exists) {
102
- location = existingAssets.components.find((path) => path.includes(item.name));
102
+ location = existingAssets.components.find((p) => p.includes(item.name));
103
103
  }
104
104
  break;
105
105
  case "service":
106
106
  case "api":
107
- exists = existingAssets.services.some((path) => path.includes(item.name));
107
+ exists = existingAssets.services.some((p) => p.includes(item.name));
108
108
  if (exists) {
109
- location = existingAssets.services.find((path) => path.includes(item.name));
109
+ location = existingAssets.services.find((p) => p.includes(item.name));
110
110
  }
111
111
  break;
112
112
  case "hook":
113
- exists = existingAssets.hooks.some((path) => path.includes(item.name));
113
+ exists = existingAssets.hooks.some((p) => p.includes(item.name));
114
114
  if (exists) {
115
- location = existingAssets.hooks.find((path) => path.includes(item.name));
115
+ location = existingAssets.hooks.find((p) => p.includes(item.name));
116
116
  }
117
117
  break;
118
118
  case "utility":
119
- exists = existingAssets.utils.some((path) => path.includes(item.name));
119
+ exists = existingAssets.utils.some((p) => p.includes(item.name));
120
120
  if (exists) {
121
- location = existingAssets.utils.find((path) => path.includes(item.name));
121
+ location = existingAssets.utils.find((p) => p.includes(item.name));
122
122
  }
123
123
  break;
124
124
  case "model":
125
125
  case "entity":
126
- exists = existingAssets.models.some((path) => path.includes(item.name));
126
+ exists = existingAssets.models.some((p) => p.includes(item.name));
127
127
  if (exists) {
128
- location = existingAssets.models.find((path) => path.includes(item.name));
128
+ location = existingAssets.models.find((p) => p.includes(item.name));
129
129
  }
130
130
  break;
131
131
  case "package":
@@ -8,7 +8,13 @@ function loadAuditLog() {
8
8
  if (!fs.existsSync(auditLogPath)) {
9
9
  return { events: [] };
10
10
  }
11
- return JSON.parse(fs.readFileSync(auditLogPath, "utf8"));
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
+ }
12
18
  }
13
19
 
14
20
  function saveAuditLog(log) {
@@ -28,8 +34,14 @@ function logEvent(event, details = {}) {
28
34
  console.log(`📝 Audit: ${event}`);
29
35
  }
30
36
 
37
+ function getRecentEvents(count = 10) {
38
+ const log = loadAuditLog();
39
+ return log.events.slice(-count).reverse();
40
+ }
41
+
31
42
  module.exports = {
32
43
  logEvent,
33
44
  loadAuditLog,
34
- saveAuditLog
45
+ saveAuditLog,
46
+ getRecentEvents
35
47
  };
@@ -1,8 +1,3 @@
1
- // SAFETY RULE
2
- // NEVER overwrite existing files in .github/
3
- // If a file exists, ask the user before modifying it.
4
- // Only create new files if missing.
5
-
6
1
  const fs = require("fs");
7
2
  const path = require("path");
8
3
 
@@ -35,26 +30,11 @@ When the user types a /sdrive:* command, follow these steps:
35
30
  5. Run validators after writing files
36
31
  6. Stop at human approval gates
37
32
 
38
- ## Command list
39
-
40
- - /sdrive:constitution — Create or update project constitution
41
- - /sdrive:propose — Create spec, traceability, plan, and tasks
42
- - /sdrive:design — Build UI/UX prototype
43
- - /sdrive:apply — Write production code
44
- - /sdrive:review — Final audit and merge
45
- - /sdrive:secure — Security audit
46
- - /sdrive:performance — Performance benchmark
47
- - /sdrive:docs — Generate documentation
48
- - /sdrive:sync — Cascade sync
49
- - /sdrive:skills — Manage AI skills
50
-
51
33
  Always read the manifest first. Never hardcode the command list.
52
34
  `;
53
35
  }
54
36
 
55
- function generate() {
56
- const tools = loadTools();
57
-
37
+ function generateForTools(tools) {
58
38
  tools.forEach((tool) => {
59
39
  const adapterFullPath = path.join(projectRoot, tool.adapterPath);
60
40
  const adapterDir = path.dirname(adapterFullPath);
@@ -63,19 +43,39 @@ function generate() {
63
43
  fs.mkdirSync(adapterDir, { recursive: true });
64
44
  }
65
45
 
66
- // SAFETY GUARD
67
46
  if (fs.existsSync(adapterFullPath)) {
68
- console.log(`⚠️ Skipped existing file: ${tool.adapterPath}`);
69
- console.log(" SpecDrive never overwrites existing project files.");
70
- console.log(" Delete the file or rename it to regenerate.\n");
47
+ console.log(`⚠️ Skipped existing: ${tool.adapterPath}`);
71
48
  return;
72
49
  }
73
50
 
74
51
  fs.writeFileSync(adapterFullPath, adapterContent(tool.name));
75
- console.log(`✅ Generated adapter: ${tool.adapterPath}`);
52
+ console.log(`✅ Generated: ${tool.adapterPath}`);
76
53
  });
54
+ }
55
+
56
+ function generateAll() {
57
+ const tools = loadTools();
58
+ generateForTools(tools);
59
+ console.log("\n🎉 All adapters generated successfully.");
60
+ }
61
+
62
+ function generateSelected(indexes) {
63
+ const tools = loadTools();
64
+ const selectedTools = indexes
65
+ .map((i) => tools[i])
66
+ .filter((tool) => tool);
67
+
68
+ if (selectedTools.length === 0) {
69
+ console.error("❌ No valid tools selected.");
70
+ process.exit(1);
71
+ }
77
72
 
78
- console.log("\n🎉 Adapter generation complete.");
73
+ generateForTools(selectedTools);
74
+ console.log("\n🎉 Selected adapters generated successfully.");
79
75
  }
80
76
 
81
- generate();
77
+ module.exports = {
78
+ generateAll,
79
+ generateSelected,
80
+ loadTools
81
+ };
@@ -31,6 +31,7 @@ function generate(templateName, featureName, variables) {
31
31
 
32
32
  files.forEach((file) => {
33
33
  const templateFile = path.join(templateDir, file);
34
+
34
35
  if (!fs.existsSync(templateFile)) {
35
36
  return;
36
37
  }
@@ -22,11 +22,15 @@ function loadPlugins() {
22
22
  return;
23
23
  }
24
24
 
25
- const pluginDef = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
26
- plugins.push({
27
- ...pluginDef,
28
- path: path.join(pluginsDir, folder)
29
- });
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
+ }
30
34
  });
31
35
 
32
36
  return plugins;
@@ -51,7 +55,12 @@ function runPlugin(pluginName) {
51
55
  require(entryPath);
52
56
  }
53
57
 
58
+ function listPlugins() {
59
+ return loadPlugins();
60
+ }
61
+
54
62
  module.exports = {
55
63
  loadPlugins,
56
- runPlugin
64
+ runPlugin,
65
+ listPlugins
57
66
  };
package/scripts/team.js CHANGED
@@ -6,15 +6,25 @@ const teamPath = path.join(projectRoot, ".sdrive", "team.json");
6
6
 
7
7
  function loadTeam() {
8
8
  if (!fs.existsSync(teamPath)) {
9
- console.error("❌ team.json not found.");
9
+ console.error("❌ team.json not found. Run `sdrive init` first.");
10
10
  process.exit(1);
11
11
  }
12
- return JSON.parse(fs.readFileSync(teamPath, "utf8"));
12
+
13
+ try {
14
+ return JSON.parse(fs.readFileSync(teamPath, "utf8"));
15
+ } catch (error) {
16
+ console.error(`❌ Invalid team.json: ${error.message}`);
17
+ process.exit(1);
18
+ }
19
+ }
20
+
21
+ function saveTeam(team) {
22
+ fs.writeFileSync(teamPath, JSON.stringify(team, null, 2));
13
23
  }
14
24
 
15
25
  function getUserRole(username) {
16
26
  const team = loadTeam();
17
- const member = team.members.find((m) => m.username === username);
27
+ const member = team.members?.find((m) => m.username === username);
18
28
  return member ? member.role : null;
19
29
  }
20
30
 
@@ -23,7 +33,7 @@ function canApprove(username) {
23
33
  if (!role) return false;
24
34
 
25
35
  const team = loadTeam();
26
- return team.rules.approval_required.includes(role);
36
+ return team.rules?.approval_required?.includes(role) || false;
27
37
  }
28
38
 
29
39
  function canImplement(username) {
@@ -31,11 +41,74 @@ function canImplement(username) {
31
41
  if (!role) return false;
32
42
 
33
43
  const team = loadTeam();
34
- return team.rules.implementation_allowed.includes(role);
44
+ return team.rules?.implementation_allowed?.includes(role) || false;
45
+ }
46
+
47
+ function listMembers() {
48
+ const team = loadTeam();
49
+ return team.members || [];
50
+ }
51
+
52
+ function addMember(username, role) {
53
+ const team = loadTeam();
54
+
55
+ if (!team.members) {
56
+ team.members = [];
57
+ }
58
+
59
+ const existing = team.members.find((m) => m.username === username);
60
+ if (existing) {
61
+ console.error(`❌ User "${username}" already exists.`);
62
+ return false;
63
+ }
64
+
65
+ team.members.push({ username, role });
66
+ saveTeam(team);
67
+ console.log(`✅ Added ${username} as ${role}.`);
68
+ return true;
69
+ }
70
+
71
+ function removeMember(username) {
72
+ const team = loadTeam();
73
+
74
+ const before = team.members?.length || 0;
75
+ team.members = (team.members || []).filter((m) => m.username !== username);
76
+ const after = team.members.length;
77
+
78
+ if (before === after) {
79
+ console.error(`❌ User "${username}" not found.`);
80
+ return false;
81
+ }
82
+
83
+ saveTeam(team);
84
+ console.log(`✅ Removed ${username}.`);
85
+ return true;
86
+ }
87
+
88
+ function updateMember(username, role) {
89
+ const team = loadTeam();
90
+
91
+ const member = team.members?.find((m) => m.username === username);
92
+
93
+ if (!member) {
94
+ console.error(`❌ User "${username}" not found.`);
95
+ return false;
96
+ }
97
+
98
+ member.role = role;
99
+ saveTeam(team);
100
+ console.log(`✅ Updated ${username} to ${role}.`);
101
+ return true;
35
102
  }
36
103
 
37
104
  module.exports = {
105
+ loadTeam,
106
+ saveTeam,
38
107
  getUserRole,
39
108
  canApprove,
40
- canImplement
109
+ canImplement,
110
+ listMembers,
111
+ addMember,
112
+ removeMember,
113
+ updateMember
41
114
  };
@@ -6,7 +6,7 @@ const projectRoot = process.cwd();
6
6
  const generatorPath = path.join(projectRoot, ".sdrive", "scripts", "generate-adapters.js");
7
7
 
8
8
  function testGeneratorCreatesAdapters() {
9
- console.log("\n🧪 Test 1: Generator creates all adapters");
9
+ console.log("\n🧪 Test 1: Generator creates adapters");
10
10
 
11
11
  try {
12
12
  execSync(`node "${generatorPath}"`, { stdio: "pipe" });
@@ -15,20 +15,16 @@ function testGeneratorCreatesAdapters() {
15
15
  return false;
16
16
  }
17
17
 
18
- const expectedAdapters = [
19
- ".github/copilot-instructions.md",
20
- ".clinerules/sdrive.md",
21
- ".claude/commands/sdrive.md",
22
- ".cursor/rules/sdrive.md",
23
- ".vscode/sdrive-commands.json"
24
- ];
18
+ const toolsPath = path.join(projectRoot, ".sdrive", "commands", "tools.json");
19
+ const tools = JSON.parse(fs.readFileSync(toolsPath, "utf8")).tools;
20
+
21
+ for (const tool of tools) {
22
+ const fullPath = path.join(projectRoot, tool.adapterPath);
25
23
 
26
- for (const adapter of expectedAdapters) {
27
- const fullPath = path.join(projectRoot, adapter);
28
24
  if (fs.existsSync(fullPath)) {
29
- console.log(` ✅ Found: ${adapter}`);
25
+ console.log(` ✅ Found: ${tool.adapterPath}`);
30
26
  } else {
31
- console.log(` ❌ Missing: ${adapter}`);
27
+ console.log(` ❌ Missing: ${tool.adapterPath}`);
32
28
  return false;
33
29
  }
34
30
  }
@@ -63,10 +59,8 @@ function testSafetyRule() {
63
59
 
64
60
  const adapterPath = path.join(projectRoot, ".github", "copilot-instructions.md");
65
61
 
66
- // Modify the file
67
62
  fs.writeFileSync(adapterPath, "# Custom existing file\n", "utf8");
68
63
 
69
- // Run generator
70
64
  try {
71
65
  execSync(`node "${generatorPath}"`, { stdio: "pipe" });
72
66
  } catch (error) {
@@ -74,7 +68,6 @@ function testSafetyRule() {
74
68
  return false;
75
69
  }
76
70
 
77
- // Check content was not overwritten
78
71
  const content = fs.readFileSync(adapterPath, "utf8");
79
72
 
80
73
  if (content === "# Custom existing file\n") {
@@ -87,20 +80,18 @@ function testSafetyRule() {
87
80
  }
88
81
 
89
82
  function cleanup() {
90
- const adapters = [
91
- ".github/copilot-instructions.md",
92
- ".clinerules/sdrive.md",
93
- ".claude/commands/sdrive.md",
94
- ".cursor/rules/sdrive.md",
95
- ".vscode/sdrive-commands.json"
96
- ];
97
-
98
- adapters.forEach((adapter) => {
99
- const fullPath = path.join(projectRoot, adapter);
100
- if (fs.existsSync(fullPath)) {
101
- fs.rmSync(fullPath, { force: true });
102
- }
103
- });
83
+ const toolsPath = path.join(projectRoot, ".sdrive", "commands", "tools.json");
84
+
85
+ if (fs.existsSync(toolsPath)) {
86
+ const tools = JSON.parse(fs.readFileSync(toolsPath, "utf8")).tools;
87
+
88
+ tools.forEach((tool) => {
89
+ const fullPath = path.join(projectRoot, tool.adapterPath);
90
+ if (fs.existsSync(fullPath)) {
91
+ fs.rmSync(fullPath, { force: true });
92
+ }
93
+ });
94
+ }
104
95
 
105
96
  console.log("\n🧹 Cleaned up adapters.");
106
97
  }