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.
- package/README.md +955 -506
- 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 +152 -35
- 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 +51 -0
- package/scripts/diff-version.js +96 -0
- package/scripts/generate-adapters.js +80 -80
- package/scripts/generate-from-template.js +97 -70
- package/scripts/generate-openapi.js +76 -0
- package/scripts/github-team-sync.js +81 -0
- package/scripts/install-hooks.js +21 -0
- 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 -0
- 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 -400
- 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
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
|
|
4
|
-
const projectRoot = process.cwd();
|
|
5
|
-
|
|
6
|
-
function loadTools() {
|
|
7
|
-
const toolsPath = path.join(projectRoot, ".sdrive", "commands", "tools.json");
|
|
8
|
-
|
|
9
|
-
if (!fs.existsSync(toolsPath)) {
|
|
10
|
-
console.error("❌ tools.json not found.");
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return JSON.parse(fs.readFileSync(toolsPath, "utf8")).tools;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function adapterContent(toolName) {
|
|
18
|
-
return `# SpecDrive Commands
|
|
19
|
-
|
|
20
|
-
This project uses SpecDrive for spec-driven development.
|
|
21
|
-
|
|
22
|
-
## How to use
|
|
23
|
-
|
|
24
|
-
When the user types a /sdrive:* command, follow these steps:
|
|
25
|
-
|
|
26
|
-
1. Read .sdrive/commands/manifest.json
|
|
27
|
-
2. Find the matching command
|
|
28
|
-
3. Read the agent file listed in agentFile
|
|
29
|
-
4. Follow that agent's instructions exactly
|
|
30
|
-
5. Run validators after writing files
|
|
31
|
-
6. Stop at human approval gates
|
|
32
|
-
|
|
33
|
-
Always read the manifest first. Never hardcode the command list.
|
|
34
|
-
`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function generateForTools(tools) {
|
|
38
|
-
tools.forEach((tool) => {
|
|
39
|
-
const adapterFullPath = path.join(projectRoot, tool.adapterPath);
|
|
40
|
-
const adapterDir = path.dirname(adapterFullPath);
|
|
41
|
-
|
|
42
|
-
if (!fs.existsSync(adapterDir)) {
|
|
43
|
-
fs.mkdirSync(adapterDir, { recursive: true });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (fs.existsSync(adapterFullPath)) {
|
|
47
|
-
console.log(`⚠️ Skipped existing: ${tool.adapterPath}`);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
fs.writeFileSync(adapterFullPath, adapterContent(tool.name));
|
|
52
|
-
console.log(`✅ Generated: ${tool.adapterPath}`);
|
|
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
|
-
}
|
|
72
|
-
|
|
73
|
-
generateForTools(selectedTools);
|
|
74
|
-
console.log("\n🎉 Selected adapters generated successfully.");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
module.exports = {
|
|
78
|
-
generateAll,
|
|
79
|
-
generateSelected,
|
|
80
|
-
loadTools
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const projectRoot = process.cwd();
|
|
5
|
+
|
|
6
|
+
function loadTools() {
|
|
7
|
+
const toolsPath = path.join(projectRoot, ".sdrive", "commands", "tools.json");
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(toolsPath)) {
|
|
10
|
+
console.error("❌ tools.json not found.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return JSON.parse(fs.readFileSync(toolsPath, "utf8")).tools;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function adapterContent(toolName) {
|
|
18
|
+
return `# SpecDrive Commands
|
|
19
|
+
|
|
20
|
+
This project uses SpecDrive for spec-driven development.
|
|
21
|
+
|
|
22
|
+
## How to use
|
|
23
|
+
|
|
24
|
+
When the user types a /sdrive:* command, follow these steps:
|
|
25
|
+
|
|
26
|
+
1. Read .sdrive/commands/manifest.json
|
|
27
|
+
2. Find the matching command
|
|
28
|
+
3. Read the agent file listed in agentFile
|
|
29
|
+
4. Follow that agent's instructions exactly
|
|
30
|
+
5. Run validators after writing files
|
|
31
|
+
6. Stop at human approval gates
|
|
32
|
+
|
|
33
|
+
Always read the manifest first. Never hardcode the command list.
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function generateForTools(tools) {
|
|
38
|
+
tools.forEach((tool) => {
|
|
39
|
+
const adapterFullPath = path.join(projectRoot, tool.adapterPath);
|
|
40
|
+
const adapterDir = path.dirname(adapterFullPath);
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(adapterDir)) {
|
|
43
|
+
fs.mkdirSync(adapterDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (fs.existsSync(adapterFullPath)) {
|
|
47
|
+
console.log(`⚠️ Skipped existing: ${tool.adapterPath}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(adapterFullPath, adapterContent(tool.name));
|
|
52
|
+
console.log(`✅ Generated: ${tool.adapterPath}`);
|
|
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
|
+
}
|
|
72
|
+
|
|
73
|
+
generateForTools(selectedTools);
|
|
74
|
+
console.log("\n🎉 Selected adapters generated successfully.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
generateAll,
|
|
79
|
+
generateSelected,
|
|
80
|
+
loadTools
|
|
81
81
|
};
|
|
@@ -1,71 +1,98 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
|
|
4
|
-
const projectRoot = process.cwd();
|
|
5
|
-
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
if (!
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const projectRoot = process.cwd();
|
|
5
|
+
|
|
6
|
+
function loadRegistry() {
|
|
7
|
+
const registryPath = path.join(projectRoot, ".sdrive", "templates", "registry.json");
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(registryPath)) {
|
|
10
|
+
console.error("❌ registry.json not found.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return JSON.parse(fs.readFileSync(registryPath, "utf8")).templates;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function templateExists(templateName) {
|
|
18
|
+
const registry = loadRegistry();
|
|
19
|
+
return registry.some((t) => t.name === templateName || t.path === templateName);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function listTemplates() {
|
|
23
|
+
const registry = loadRegistry();
|
|
24
|
+
console.log("\nAvailable SpecDrive Templates\n");
|
|
25
|
+
registry.forEach((t) => {
|
|
26
|
+
console.log(` ${t.name} — ${t.description}`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadTemplate(templateName, fileName) {
|
|
31
|
+
const templatePath = path.join(projectRoot, ".sdrive", "templates", templateName, fileName);
|
|
32
|
+
return fs.readFileSync(templatePath, "utf8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function applyVariables(content, variables) {
|
|
36
|
+
let result = content;
|
|
37
|
+
Object.keys(variables).forEach((key) => {
|
|
38
|
+
result = result.replaceAll(`{{${key}}}`, variables[key]);
|
|
39
|
+
});
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function generate(templateName, featureName, variables) {
|
|
44
|
+
const templateDir = path.join(projectRoot, ".sdrive", "templates", templateName);
|
|
45
|
+
const govDir = path.join(projectRoot, ".sdrive", "governance", featureName);
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(templateDir)) {
|
|
48
|
+
console.error(`❌ Template "${templateName}" not found.`);
|
|
49
|
+
listTemplates();
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fs.mkdirSync(govDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
const files = ["spec.json", "plan.json", "tasks.json"];
|
|
56
|
+
|
|
57
|
+
files.forEach((file) => {
|
|
58
|
+
const templateFile = path.join(templateDir, file);
|
|
59
|
+
|
|
60
|
+
if (!fs.existsSync(templateFile)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let content = loadTemplate(templateName, file);
|
|
65
|
+
content = applyVariables(content, {
|
|
66
|
+
featureName,
|
|
67
|
+
...variables
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
fs.writeFileSync(path.join(govDir, file), content);
|
|
71
|
+
console.log(`✅ Generated: ${featureName}/${file}`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(`\n🎉 Feature "${featureName}" generated from "${templateName}" template.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const templateName = process.argv[2];
|
|
78
|
+
const featureName = process.argv[3];
|
|
79
|
+
const problemStatement = process.argv[4] || "";
|
|
80
|
+
const proposedSolution = process.argv[5] || "";
|
|
81
|
+
const targetAudience = process.argv[6] || "";
|
|
82
|
+
const approach = process.argv[7] || "";
|
|
83
|
+
const flowDescription = process.argv[8] || "";
|
|
84
|
+
|
|
85
|
+
if (!templateName || !featureName) {
|
|
86
|
+
console.error("❌ Usage: node generate-from-template.js <template> <feature> [problem] [solution] [audience] [approach] [flow]");
|
|
87
|
+
listTemplates();
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
generate(templateName, featureName, {
|
|
92
|
+
title: featureName,
|
|
93
|
+
problemStatement,
|
|
94
|
+
proposedSolution,
|
|
95
|
+
targetAudience,
|
|
96
|
+
approach,
|
|
97
|
+
flowDescription
|
|
71
98
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const projectRoot = process.cwd();
|
|
7
|
+
|
|
8
|
+
function loadPlan(featureName) {
|
|
9
|
+
const planPath = path.join(projectRoot, ".sdrive", "governance", featureName, "plan.json");
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(planPath)) {
|
|
12
|
+
console.error(`❌ plan.json not found for feature "${featureName}".`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return JSON.parse(fs.readFileSync(planPath, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function generateOpenAPI(featureName) {
|
|
20
|
+
const plan = loadPlan(featureName);
|
|
21
|
+
|
|
22
|
+
const openapi = {
|
|
23
|
+
openapi: "3.0.0",
|
|
24
|
+
info: {
|
|
25
|
+
title: `${featureName} API`,
|
|
26
|
+
version: "1.0.0",
|
|
27
|
+
description: `API specification for ${featureName}`
|
|
28
|
+
},
|
|
29
|
+
paths: {}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const contracts = plan.apiContracts || [];
|
|
33
|
+
|
|
34
|
+
contracts.forEach((contract) => {
|
|
35
|
+
const method = contract.method.toLowerCase();
|
|
36
|
+
const apiPath = contract.path;
|
|
37
|
+
|
|
38
|
+
if (!openapi.paths[apiPath]) {
|
|
39
|
+
openapi.paths[apiPath] = {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
openapi.paths[apiPath][method] = {
|
|
43
|
+
summary: `${contract.method} ${apiPath}`,
|
|
44
|
+
security: contract.authenticationRequired ? [{ bearerAuth: [] }] : [],
|
|
45
|
+
responses: {
|
|
46
|
+
200: {
|
|
47
|
+
description: "Successful response"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
openapi.components = {
|
|
54
|
+
securitySchemes: {
|
|
55
|
+
bearerAuth: {
|
|
56
|
+
type: "http",
|
|
57
|
+
scheme: "bearer"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const outputPath = path.join(projectRoot, ".sdrive", "governance", featureName, "openapi.json");
|
|
63
|
+
fs.writeFileSync(outputPath, JSON.stringify(openapi, null, 2));
|
|
64
|
+
|
|
65
|
+
console.log(`✅ Generated OpenAPI spec.`);
|
|
66
|
+
console.log(` File: ${outputPath}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const featureName = process.argv[2];
|
|
70
|
+
|
|
71
|
+
if (!featureName) {
|
|
72
|
+
console.error("❌ Usage: node generate-openapi.js <feature-name>");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
generateOpenAPI(featureName);
|
|
@@ -0,0 +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
|
+
|
|
81
|
+
syncTeam();
|
|
@@ -0,0 +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.");
|
|
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
|
};
|