specdrive-cli 0.1.0

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.
@@ -0,0 +1,65 @@
1
+ {
2
+ "": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Feature Specification",
4
+ "type": "object",
5
+ "properties": {
6
+ "feature": { "type": "string", "pattern": "^[a-z0-9-]+$" },
7
+ "version": { "type": "integer", "minimum": 1 },
8
+ "overview": {
9
+ "type": "object",
10
+ "properties": {
11
+ "problemStatement": { "type": "string" },
12
+ "proposedSolution": { "type": "string" },
13
+ "targetAudience": { "type": "string" }
14
+ },
15
+ "required": ["problemStatement", "proposedSolution", "targetAudience"]
16
+ },
17
+ "userStories": {
18
+ "type": "array",
19
+ "minItems": 1,
20
+ "items": {
21
+ "type": "object",
22
+ "properties": {
23
+ "id": { "type": "string", "pattern": "^US-[0-9]{3}$" },
24
+ "title": { "type": "string" },
25
+ "acceptanceCriteria": {
26
+ "type": "array",
27
+ "minItems": 1,
28
+ "items": {
29
+ "type": "object",
30
+ "properties": {
31
+ "id": { "type": "string", "pattern": "^AC-[0-9]{3}$" },
32
+ "text": { "type": "string" }
33
+ },
34
+ "required": ["id", "text"]
35
+ }
36
+ }
37
+ },
38
+ "required": ["id", "title", "acceptanceCriteria"]
39
+ }
40
+ },
41
+ "functionalRequirements": {
42
+ "type": "array",
43
+ "items": {
44
+ "type": "object",
45
+ "properties": {
46
+ "id": { "type": "string", "pattern": "^FR-[0-9]{3}$" },
47
+ "text": { "type": "string" }
48
+ },
49
+ "required": ["id", "text"]
50
+ }
51
+ },
52
+ "nonFunctionalRequirements": {
53
+ "type": "array",
54
+ "items": {
55
+ "type": "object",
56
+ "properties": {
57
+ "id": { "type": "string", "pattern": "^NFR-[0-9]{3}$" },
58
+ "text": { "type": "string" }
59
+ },
60
+ "required": ["id", "text"]
61
+ }
62
+ }
63
+ },
64
+ "required": ["feature", "version", "overview", "userStories"]
65
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Execution Tasks",
4
+ "type": "object",
5
+ "properties": {
6
+ "feature": { "type": "string", "pattern": "^[a-z0-9-]+$" },
7
+ "version": { "type": "integer", "minimum": 1 },
8
+ "phases": {
9
+ "type": "array",
10
+ "minItems": 1,
11
+ "items": {
12
+ "type": "object",
13
+ "properties": {
14
+ "phaseId": { "type": "string", "pattern": "^P[0-9]+$" },
15
+ "name": { "type": "string" },
16
+ "tasks": {
17
+ "type": "array",
18
+ "minItems": 1,
19
+ "items": {
20
+ "type": "object",
21
+ "properties": {
22
+ "id": { "type": "string", "pattern": "^T[0-9]+\\.[0-9]+$" },
23
+ "title": { "type": "string" },
24
+ "description": { "type": "string" },
25
+ "linkedRequirements": {
26
+ "type": "array",
27
+ "items": { "type": "string", "pattern": "^(FR|NFR)-[0-9]{3}$" }
28
+ },
29
+ "linkedAcceptanceCriteria": {
30
+ "type": "array",
31
+ "items": { "type": "string", "pattern": "^AC-[0-9]{3}$" }
32
+ },
33
+ "completed": { "type": "boolean", "default": false }
34
+ },
35
+ "required": ["id", "title", "linkedRequirements"]
36
+ }
37
+ }
38
+ },
39
+ "required": ["phaseId", "name", "tasks"]
40
+ }
41
+ }
42
+ },
43
+ "required": ["feature", "version", "phases"]
44
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Traceability Matrix",
4
+ "type": "object",
5
+ "properties": {
6
+ "feature": { "type": "string", "pattern": "^[a-z0-9-]+$" },
7
+ "version": { "type": "integer", "minimum": 1 },
8
+ "rows": {
9
+ "type": "array",
10
+ "items": {
11
+ "type": "object",
12
+ "properties": {
13
+ "requirementId": { "type": "string", "pattern": "^(FR|NFR)-[0-9]{3}$" },
14
+ "acceptanceCriteriaIds": {
15
+ "type": "array",
16
+ "items": { "type": "string", "pattern": "^AC-[0-9]{3}$" }
17
+ },
18
+ "planSection": { "type": "string" },
19
+ "taskIds": {
20
+ "type": "array",
21
+ "items": { "type": "string", "pattern": "^T[0-9]+\\.[0-9]+$" }
22
+ },
23
+ "testIds": {
24
+ "type": "array",
25
+ "items": { "type": "string", "pattern": "^TEST-[0-9]{3}$" }
26
+ },
27
+ "status": { "enum": ["not_started", "in_progress", "complete"] }
28
+ },
29
+ "required": ["requirementId", "status"]
30
+ }
31
+ }
32
+ },
33
+ "required": ["feature", "version", "rows"]
34
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Workflow State",
4
+ "type": "object",
5
+ "properties": {
6
+ "features": {
7
+ "type": "array",
8
+ "items": {
9
+ "type": "object",
10
+ "properties": {
11
+ "name": { "type": "string", "pattern": "^[a-z0-9-]+$" },
12
+ "phase": { "enum": ["backlog", "ongoing", "completed", "blocked"] },
13
+ "currentStep": { "enum": ["specification", "planning", "implementation", "review"] },
14
+ "gates": {
15
+ "type": "object",
16
+ "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"] }
21
+ },
22
+ "required": ["specification", "planning", "implementation", "review"]
23
+ },
24
+ "history": {
25
+ "type": "array",
26
+ "items": {
27
+ "type": "object",
28
+ "properties": {
29
+ "event": { "type": "string" },
30
+ "timestamp": { "type": "string", "format": "date-time" }
31
+ },
32
+ "required": ["event", "timestamp"]
33
+ }
34
+ }
35
+ },
36
+ "required": ["name", "phase", "currentStep", "gates"]
37
+ }
38
+ }
39
+ },
40
+ "required": ["features"]
41
+ }
@@ -0,0 +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"]) {
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((path) => path.includes(item.name));
101
+ if (exists) {
102
+ location = existingAssets.components.find((path) => path.includes(item.name));
103
+ }
104
+ break;
105
+ case "service":
106
+ case "api":
107
+ exists = existingAssets.services.some((path) => path.includes(item.name));
108
+ if (exists) {
109
+ location = existingAssets.services.find((path) => path.includes(item.name));
110
+ }
111
+ break;
112
+ case "hook":
113
+ exists = existingAssets.hooks.some((path) => path.includes(item.name));
114
+ if (exists) {
115
+ location = existingAssets.hooks.find((path) => path.includes(item.name));
116
+ }
117
+ break;
118
+ case "utility":
119
+ exists = existingAssets.utils.some((path) => path.includes(item.name));
120
+ if (exists) {
121
+ location = existingAssets.utils.find((path) => path.includes(item.name));
122
+ }
123
+ break;
124
+ case "model":
125
+ case "entity":
126
+ exists = existingAssets.models.some((path) => path.includes(item.name));
127
+ if (exists) {
128
+ location = existingAssets.models.find((path) => path.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
+ main();
@@ -0,0 +1,35 @@
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
+ return JSON.parse(fs.readFileSync(auditLogPath, "utf8"));
12
+ }
13
+
14
+ function saveAuditLog(log) {
15
+ fs.writeFileSync(auditLogPath, JSON.stringify(log, null, 2));
16
+ }
17
+
18
+ function logEvent(event, details = {}) {
19
+ const log = loadAuditLog();
20
+
21
+ log.events.push({
22
+ timestamp: new Date().toISOString(),
23
+ ...details,
24
+ event
25
+ });
26
+
27
+ saveAuditLog(log);
28
+ console.log(`šŸ“ Audit: ${event}`);
29
+ }
30
+
31
+ module.exports = {
32
+ logEvent,
33
+ loadAuditLog,
34
+ saveAuditLog
35
+ };
@@ -0,0 +1,81 @@
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
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const projectRoot = process.cwd();
10
+
11
+ function loadTools() {
12
+ const toolsPath = path.join(projectRoot, ".sdrive", "commands", "tools.json");
13
+
14
+ if (!fs.existsSync(toolsPath)) {
15
+ console.error("āŒ tools.json not found.");
16
+ process.exit(1);
17
+ }
18
+
19
+ return JSON.parse(fs.readFileSync(toolsPath, "utf8")).tools;
20
+ }
21
+
22
+ function adapterContent(toolName) {
23
+ return `# SpecDrive Commands
24
+
25
+ This project uses SpecDrive for spec-driven development.
26
+
27
+ ## How to use
28
+
29
+ When the user types a /sdrive:* command, follow these steps:
30
+
31
+ 1. Read .sdrive/commands/manifest.json
32
+ 2. Find the matching command
33
+ 3. Read the agent file listed in agentFile
34
+ 4. Follow that agent's instructions exactly
35
+ 5. Run validators after writing files
36
+ 6. Stop at human approval gates
37
+
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
+ Always read the manifest first. Never hardcode the command list.
52
+ `;
53
+ }
54
+
55
+ function generate() {
56
+ const tools = loadTools();
57
+
58
+ tools.forEach((tool) => {
59
+ const adapterFullPath = path.join(projectRoot, tool.adapterPath);
60
+ const adapterDir = path.dirname(adapterFullPath);
61
+
62
+ if (!fs.existsSync(adapterDir)) {
63
+ fs.mkdirSync(adapterDir, { recursive: true });
64
+ }
65
+
66
+ // SAFETY GUARD
67
+ 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");
71
+ return;
72
+ }
73
+
74
+ fs.writeFileSync(adapterFullPath, adapterContent(tool.name));
75
+ console.log(`āœ… Generated adapter: ${tool.adapterPath}`);
76
+ });
77
+
78
+ console.log("\nšŸŽ‰ Adapter generation complete.");
79
+ }
80
+
81
+ generate();
@@ -0,0 +1,70 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const projectRoot = process.cwd();
5
+
6
+ function loadTemplate(templateName, fileName) {
7
+ const templatePath = path.join(projectRoot, ".sdrive", "templates", templateName, fileName);
8
+ return fs.readFileSync(templatePath, "utf8");
9
+ }
10
+
11
+ function applyVariables(content, variables) {
12
+ let result = content;
13
+ Object.keys(variables).forEach((key) => {
14
+ result = result.replaceAll(`{{${key}}}`, variables[key]);
15
+ });
16
+ return result;
17
+ }
18
+
19
+ function generate(templateName, featureName, variables) {
20
+ const templateDir = path.join(projectRoot, ".sdrive", "templates", templateName);
21
+ const govDir = path.join(projectRoot, ".sdrive", "governance", featureName);
22
+
23
+ if (!fs.existsSync(templateDir)) {
24
+ console.error(`āŒ Template "${templateName}" not found.`);
25
+ process.exit(1);
26
+ }
27
+
28
+ fs.mkdirSync(govDir, { recursive: true });
29
+
30
+ const files = ["spec.json", "plan.json", "tasks.json"];
31
+
32
+ files.forEach((file) => {
33
+ const templateFile = path.join(templateDir, file);
34
+ if (!fs.existsSync(templateFile)) {
35
+ return;
36
+ }
37
+
38
+ let content = loadTemplate(templateName, file);
39
+ content = applyVariables(content, {
40
+ featureName,
41
+ ...variables
42
+ });
43
+
44
+ fs.writeFileSync(path.join(govDir, file), content);
45
+ console.log(`āœ… Generated: ${featureName}/${file}`);
46
+ });
47
+
48
+ console.log(`\nšŸŽ‰ Feature "${featureName}" generated from "${templateName}" template.`);
49
+ }
50
+
51
+ const templateName = process.argv[2];
52
+ const featureName = process.argv[3];
53
+ const problemStatement = process.argv[4] || "";
54
+ const proposedSolution = process.argv[5] || "";
55
+ const targetAudience = process.argv[6] || "";
56
+ const approach = process.argv[7] || "";
57
+ const flowDescription = process.argv[8] || "";
58
+
59
+ if (!templateName || !featureName) {
60
+ console.error("āŒ Usage: node generate-from-template.js <template> <feature> [problem] [solution] [audience] [approach] [flow]");
61
+ process.exit(1);
62
+ }
63
+
64
+ generate(templateName, featureName, {
65
+ problemStatement,
66
+ proposedSolution,
67
+ targetAudience,
68
+ approach,
69
+ flowDescription
70
+ });
@@ -0,0 +1,57 @@
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
+ const pluginDef = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
26
+ plugins.push({
27
+ ...pluginDef,
28
+ path: path.join(pluginsDir, folder)
29
+ });
30
+ });
31
+
32
+ return plugins;
33
+ }
34
+
35
+ function runPlugin(pluginName) {
36
+ const plugins = loadPlugins();
37
+ const plugin = plugins.find((p) => p.name === pluginName);
38
+
39
+ if (!plugin) {
40
+ console.error(`āŒ Plugin "${pluginName}" not found.`);
41
+ process.exit(1);
42
+ }
43
+
44
+ const entryPath = path.join(plugin.path, plugin.entry);
45
+
46
+ if (!fs.existsSync(entryPath)) {
47
+ console.error(`āŒ Plugin entry not found: ${plugin.entry}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ require(entryPath);
52
+ }
53
+
54
+ module.exports = {
55
+ loadPlugins,
56
+ runPlugin
57
+ };
@@ -0,0 +1,20 @@
1
+ const { execSync } = require("child_process");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+
5
+ const projectRoot = process.cwd();
6
+ const validatorPath = path.join(projectRoot, ".sdrive", "scripts", "validate-governance.js");
7
+
8
+ if (!fs.existsSync(validatorPath)) {
9
+ console.log("āš ļø Validator not found. Skipping pre-commit validation.");
10
+ process.exit(0);
11
+ }
12
+
13
+ try {
14
+ execSync(`node "${validatorPath}"`, { stdio: "inherit" });
15
+ console.log("āœ… Pre-commit validation passed.");
16
+ process.exit(0);
17
+ } catch (error) {
18
+ console.error("āŒ Pre-commit validation failed. Fix errors before committing.");
19
+ process.exit(1);
20
+ }
@@ -0,0 +1,41 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const projectRoot = process.cwd();
5
+ const teamPath = path.join(projectRoot, ".sdrive", "team.json");
6
+
7
+ function loadTeam() {
8
+ if (!fs.existsSync(teamPath)) {
9
+ console.error("āŒ team.json not found.");
10
+ process.exit(1);
11
+ }
12
+ return JSON.parse(fs.readFileSync(teamPath, "utf8"));
13
+ }
14
+
15
+ function getUserRole(username) {
16
+ const team = loadTeam();
17
+ const member = team.members.find((m) => m.username === username);
18
+ return member ? member.role : null;
19
+ }
20
+
21
+ function canApprove(username) {
22
+ const role = getUserRole(username);
23
+ if (!role) return false;
24
+
25
+ const team = loadTeam();
26
+ return team.rules.approval_required.includes(role);
27
+ }
28
+
29
+ function canImplement(username) {
30
+ const role = getUserRole(username);
31
+ if (!role) return false;
32
+
33
+ const team = loadTeam();
34
+ return team.rules.implementation_allowed.includes(role);
35
+ }
36
+
37
+ module.exports = {
38
+ getUserRole,
39
+ canApprove,
40
+ canImplement
41
+ };