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
|
@@ -0,0 +1,312 @@
|
|
|
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
|
+
const onboardingDir = path.join(sdriveRoot, "onboarding");
|
|
9
|
+
|
|
10
|
+
const inventoryPath = path.join(onboardingDir, "inventory.json");
|
|
11
|
+
const reportPath = path.join(onboardingDir, "report.md");
|
|
12
|
+
|
|
13
|
+
const DETECT_FILES = {
|
|
14
|
+
manifests: [
|
|
15
|
+
"package.json",
|
|
16
|
+
"requirements.txt",
|
|
17
|
+
"pyproject.toml",
|
|
18
|
+
"go.mod",
|
|
19
|
+
"pubspec.yaml",
|
|
20
|
+
"Cargo.toml",
|
|
21
|
+
"pom.xml",
|
|
22
|
+
"build.gradle"
|
|
23
|
+
],
|
|
24
|
+
docs: [
|
|
25
|
+
"README.md",
|
|
26
|
+
"CONTRIBUTING.md"
|
|
27
|
+
],
|
|
28
|
+
codeRoots: [
|
|
29
|
+
"src",
|
|
30
|
+
"app",
|
|
31
|
+
"lib",
|
|
32
|
+
"pages",
|
|
33
|
+
"server",
|
|
34
|
+
"internal",
|
|
35
|
+
"cmd",
|
|
36
|
+
"pkg",
|
|
37
|
+
"api",
|
|
38
|
+
"apps",
|
|
39
|
+
"packages"
|
|
40
|
+
],
|
|
41
|
+
ci: [
|
|
42
|
+
".github/workflows",
|
|
43
|
+
".gitlab-ci.yml"
|
|
44
|
+
],
|
|
45
|
+
tests: [
|
|
46
|
+
"tests",
|
|
47
|
+
"__tests__",
|
|
48
|
+
"spec",
|
|
49
|
+
"test"
|
|
50
|
+
],
|
|
51
|
+
configs: [
|
|
52
|
+
"tsconfig.json",
|
|
53
|
+
"jsconfig.json",
|
|
54
|
+
".eslintrc",
|
|
55
|
+
".eslintrc.js",
|
|
56
|
+
".eslintrc.json",
|
|
57
|
+
".prettierrc",
|
|
58
|
+
".prettierrc.js",
|
|
59
|
+
".prettierrc.json"
|
|
60
|
+
],
|
|
61
|
+
specdrive: [
|
|
62
|
+
".sdrive/constitution.md",
|
|
63
|
+
".sdrive/skills",
|
|
64
|
+
".sdrive/specs",
|
|
65
|
+
".sdrive/governance",
|
|
66
|
+
".sdrive/workflow-state.json",
|
|
67
|
+
".sdrive/schemas",
|
|
68
|
+
".sdrive/scripts",
|
|
69
|
+
".sdrive/commands",
|
|
70
|
+
".sdrive/agents",
|
|
71
|
+
".sdrive/dashboard",
|
|
72
|
+
".sdrive/mcp"
|
|
73
|
+
],
|
|
74
|
+
externalSpecSystems: [
|
|
75
|
+
"openspec",
|
|
76
|
+
"openspec/specs",
|
|
77
|
+
"openspec/changes",
|
|
78
|
+
"openspec/archive",
|
|
79
|
+
".specify",
|
|
80
|
+
".specify/specs",
|
|
81
|
+
".specify/memory"
|
|
82
|
+
]
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function exists(relativePath) {
|
|
86
|
+
return fs.existsSync(path.join(projectRoot, relativePath));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function countFiles(relativePath) {
|
|
90
|
+
const full = path.join(projectRoot, relativePath);
|
|
91
|
+
|
|
92
|
+
if (!fs.existsSync(full)) return 0;
|
|
93
|
+
|
|
94
|
+
const stat = fs.statSync(full);
|
|
95
|
+
if (!stat.isDirectory()) return 1;
|
|
96
|
+
|
|
97
|
+
let count = 0;
|
|
98
|
+
const walk = (dir) => {
|
|
99
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
100
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
101
|
+
const fullEntry = path.join(dir, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
walk(fullEntry);
|
|
104
|
+
} else {
|
|
105
|
+
count++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
walk(full);
|
|
110
|
+
return count;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function scanSection(list) {
|
|
114
|
+
const result = [];
|
|
115
|
+
for (const relativePath of list) {
|
|
116
|
+
const present = exists(relativePath);
|
|
117
|
+
result.push({
|
|
118
|
+
path: relativePath,
|
|
119
|
+
present,
|
|
120
|
+
fileCount: present ? countFiles(relativePath) : 0
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function detectExternalSpecSystems() {
|
|
127
|
+
const detection = {
|
|
128
|
+
openspec: {
|
|
129
|
+
detected: false,
|
|
130
|
+
specs: 0,
|
|
131
|
+
changes: 0,
|
|
132
|
+
archived: 0
|
|
133
|
+
},
|
|
134
|
+
speckit: {
|
|
135
|
+
detected: false,
|
|
136
|
+
specs: 0,
|
|
137
|
+
hasConstitution: false
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// OpenSpec detection
|
|
142
|
+
const openspecDir = path.join(projectRoot, "openspec");
|
|
143
|
+
if (fs.existsSync(openspecDir)) {
|
|
144
|
+
detection.openspec.detected = true;
|
|
145
|
+
|
|
146
|
+
const specsDir = path.join(openspecDir, "specs");
|
|
147
|
+
const changesDir = path.join(openspecDir, "changes");
|
|
148
|
+
const archiveDir = path.join(openspecDir, "archive");
|
|
149
|
+
|
|
150
|
+
if (fs.existsSync(specsDir)) {
|
|
151
|
+
detection.openspec.specs = fs
|
|
152
|
+
.readdirSync(specsDir, { withFileTypes: true })
|
|
153
|
+
.filter((d) => d.isDirectory()).length;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (fs.existsSync(changesDir)) {
|
|
157
|
+
detection.openspec.changes = fs
|
|
158
|
+
.readdirSync(changesDir, { withFileTypes: true })
|
|
159
|
+
.filter((d) => d.isDirectory()).length;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (fs.existsSync(archiveDir)) {
|
|
163
|
+
detection.openspec.archived = fs
|
|
164
|
+
.readdirSync(archiveDir, { withFileTypes: true })
|
|
165
|
+
.filter((d) => d.isDirectory()).length;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Spec-Kit detection
|
|
170
|
+
const speckitDir = path.join(projectRoot, ".specify");
|
|
171
|
+
if (fs.existsSync(speckitDir)) {
|
|
172
|
+
detection.speckit.detected = true;
|
|
173
|
+
|
|
174
|
+
const specsDir = path.join(speckitDir, "specs");
|
|
175
|
+
const constitutionPath = path.join(speckitDir, "memory", "constitution.md");
|
|
176
|
+
|
|
177
|
+
if (fs.existsSync(specsDir)) {
|
|
178
|
+
detection.speckit.specs = fs
|
|
179
|
+
.readdirSync(specsDir, { withFileTypes: true })
|
|
180
|
+
.filter((d) => d.isDirectory()).length;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (fs.existsSync(constitutionPath)) {
|
|
184
|
+
detection.speckit.hasConstitution = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return detection;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildInventory() {
|
|
192
|
+
const inventory = {
|
|
193
|
+
projectRoot,
|
|
194
|
+
scannedAt: new Date().toISOString(),
|
|
195
|
+
mode: "brownfield",
|
|
196
|
+
sections: {
|
|
197
|
+
manifests: scanSection(DETECT_FILES.manifests),
|
|
198
|
+
docs: scanSection(DETECT_FILES.docs),
|
|
199
|
+
codeRoots: scanSection(DETECT_FILES.codeRoots),
|
|
200
|
+
ci: scanSection(DETECT_FILES.ci),
|
|
201
|
+
tests: scanSection(DETECT_FILES.tests),
|
|
202
|
+
configs: scanSection(DETECT_FILES.configs),
|
|
203
|
+
specdrive: scanSection(DETECT_FILES.specdrive),
|
|
204
|
+
externalSpecSystems: scanSection(DETECT_FILES.externalSpecSystems)
|
|
205
|
+
},
|
|
206
|
+
detectedSystems: detectExternalSpecSystems(),
|
|
207
|
+
missing: [],
|
|
208
|
+
present: [],
|
|
209
|
+
deferred: []
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const flatten = (section) => section.map((item) => item);
|
|
213
|
+
|
|
214
|
+
const all = [
|
|
215
|
+
...flatten(inventory.sections.manifests),
|
|
216
|
+
...flatten(inventory.sections.docs),
|
|
217
|
+
...flatten(inventory.sections.codeRoots),
|
|
218
|
+
...flatten(inventory.sections.ci),
|
|
219
|
+
...flatten(inventory.sections.tests),
|
|
220
|
+
...flatten(inventory.sections.configs),
|
|
221
|
+
...flatten(inventory.sections.specdrive)
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
for (const item of all) {
|
|
225
|
+
if (item.present) {
|
|
226
|
+
inventory.present.push(item.path);
|
|
227
|
+
} else {
|
|
228
|
+
inventory.missing.push(item.path);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return inventory;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function buildReport(inventory) {
|
|
236
|
+
const lines = [];
|
|
237
|
+
|
|
238
|
+
lines.push("# Onboarding Report");
|
|
239
|
+
lines.push("");
|
|
240
|
+
lines.push(`Scanned at: ${inventory.scannedAt}`);
|
|
241
|
+
lines.push(`Project root: ${inventory.projectRoot}`);
|
|
242
|
+
lines.push("");
|
|
243
|
+
|
|
244
|
+
// External Spec Systems
|
|
245
|
+
const ext = inventory.detectedSystems || {};
|
|
246
|
+
if (ext.openspec?.detected || ext.speckit?.detected) {
|
|
247
|
+
lines.push("## Detected External Spec Systems");
|
|
248
|
+
if (ext.openspec?.detected) {
|
|
249
|
+
lines.push(`- ✅ OpenSpec`);
|
|
250
|
+
lines.push(` - Specs: ${ext.openspec.specs}`);
|
|
251
|
+
lines.push(` - Active changes: ${ext.openspec.changes}`);
|
|
252
|
+
lines.push(` - Archived: ${ext.openspec.archived}`);
|
|
253
|
+
}
|
|
254
|
+
if (ext.speckit?.detected) {
|
|
255
|
+
lines.push(`- ✅ Spec-Kit`);
|
|
256
|
+
lines.push(` - Specs: ${ext.speckit.specs}`);
|
|
257
|
+
lines.push(` - Constitution: ${ext.speckit.hasConstitution ? "found" : "missing"}`);
|
|
258
|
+
}
|
|
259
|
+
lines.push("");
|
|
260
|
+
lines.push("> These can be migrated into SpecDrive during onboarding via the Onboarding Agent.");
|
|
261
|
+
lines.push("");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
lines.push("## Present");
|
|
265
|
+
inventory.present.forEach((p) => lines.push(`- ✅ ${p}`));
|
|
266
|
+
lines.push("");
|
|
267
|
+
lines.push("## Missing");
|
|
268
|
+
inventory.missing.forEach((p) => lines.push(`- ❌ ${p}`));
|
|
269
|
+
lines.push("");
|
|
270
|
+
lines.push("## Deferred");
|
|
271
|
+
if (inventory.deferred.length === 0) {
|
|
272
|
+
lines.push("(none yet)");
|
|
273
|
+
} else {
|
|
274
|
+
inventory.deferred.forEach((p) => lines.push(`- ⚠️ ${p}`));
|
|
275
|
+
}
|
|
276
|
+
lines.push("");
|
|
277
|
+
|
|
278
|
+
return lines.join("\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function main() {
|
|
282
|
+
fs.mkdirSync(onboardingDir, { recursive: true });
|
|
283
|
+
|
|
284
|
+
const inventory = buildInventory();
|
|
285
|
+
fs.writeFileSync(inventoryPath, JSON.stringify(inventory, null, 2));
|
|
286
|
+
|
|
287
|
+
const report = buildReport(inventory);
|
|
288
|
+
fs.writeFileSync(reportPath, report);
|
|
289
|
+
|
|
290
|
+
console.log(`✅ Inventory written: ${path.relative(projectRoot, inventoryPath)}`);
|
|
291
|
+
console.log(`✅ Report written: ${path.relative(projectRoot, reportPath)}`);
|
|
292
|
+
console.log("");
|
|
293
|
+
console.log(`Present: ${inventory.present.length}`);
|
|
294
|
+
console.log(`Missing: ${inventory.missing.length}`);
|
|
295
|
+
console.log(`Deferred: ${inventory.deferred.length}`);
|
|
296
|
+
|
|
297
|
+
const ext = inventory.detectedSystems || {};
|
|
298
|
+
if (ext.openspec?.detected || ext.speckit?.detected) {
|
|
299
|
+
console.log("");
|
|
300
|
+
console.log("🔍 External Spec Systems Detected:");
|
|
301
|
+
if (ext.openspec?.detected) {
|
|
302
|
+
console.log(` - OpenSpec (${ext.openspec.specs} specs, ${ext.openspec.changes} changes)`);
|
|
303
|
+
}
|
|
304
|
+
if (ext.speckit?.detected) {
|
|
305
|
+
console.log(` - Spec-Kit (${ext.speckit.specs} specs)`);
|
|
306
|
+
}
|
|
307
|
+
console.log("");
|
|
308
|
+
console.log(" Run /sdrive:onboard to migrate them.");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
main();
|
package/scripts/pre-commit.js
CHANGED
|
@@ -1,20 +1,56 @@
|
|
|
1
|
-
const { execSync } = require("child_process");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
|
|
5
|
-
const projectRoot = process.cwd();
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
1
|
+
const { execSync } = require("child_process");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
|
|
5
|
+
const projectRoot = process.cwd();
|
|
6
|
+
|
|
7
|
+
const validatorPath = path.join(projectRoot, ".sdrive", "scripts", "validate-governance.js");
|
|
8
|
+
const verifyPath = path.join(projectRoot, ".sdrive", "scripts", "verify.js");
|
|
9
|
+
|
|
10
|
+
function getFeatureNames() {
|
|
11
|
+
const workflowStatePath = path.join(projectRoot, ".sdrive", "workflow-state.json");
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(workflowStatePath)) return [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const state = JSON.parse(fs.readFileSync(workflowStatePath, "utf8"));
|
|
17
|
+
return (state.features || []).map((f) => f.name);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 1. Run governance validator
|
|
24
|
+
if (fs.existsSync(validatorPath)) {
|
|
25
|
+
try {
|
|
26
|
+
execSync(`node "${validatorPath}"`, { stdio: "inherit" });
|
|
27
|
+
console.log("✅ Governance validation passed.");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error("❌ Governance validation failed. Fix errors before committing.");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
console.log("⚠️ Validator not found. Skipping governance validation.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Run spec-to-code verification for every active feature
|
|
37
|
+
const features = getFeatureNames();
|
|
38
|
+
|
|
39
|
+
if (features.length === 0) {
|
|
40
|
+
console.log("⚠️ No features found. Skipping spec-to-code verification.");
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const feature of features) {
|
|
45
|
+
try {
|
|
46
|
+
execSync(`node "${verifyPath}" "${feature}"`, { stdio: "pipe" });
|
|
47
|
+
console.log(`✅ Spec-to-code verification passed for ${feature}.`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(`❌ Verification failed for ${feature}.`);
|
|
50
|
+
console.error(error.stdout ? error.stdout.toString() : "");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log("✅ Pre-commit checks passed.");
|
|
56
|
+
process.exit(0);
|
package/scripts/team.js
CHANGED
|
@@ -1,114 +1,114 @@
|
|
|
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. Run `sdrive init` first.");
|
|
10
|
-
process.exit(1);
|
|
11
|
-
}
|
|
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));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getUserRole(username) {
|
|
26
|
-
const team = loadTeam();
|
|
27
|
-
const member = team.members?.find((m) => m.username === username);
|
|
28
|
-
return member ? member.role : null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function canApprove(username) {
|
|
32
|
-
const role = getUserRole(username);
|
|
33
|
-
if (!role) return false;
|
|
34
|
-
|
|
35
|
-
const team = loadTeam();
|
|
36
|
-
return team.rules?.approval_required?.includes(role) || false;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function canImplement(username) {
|
|
40
|
-
const role = getUserRole(username);
|
|
41
|
-
if (!role) return false;
|
|
42
|
-
|
|
43
|
-
const team = loadTeam();
|
|
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;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
module.exports = {
|
|
105
|
-
loadTeam,
|
|
106
|
-
saveTeam,
|
|
107
|
-
getUserRole,
|
|
108
|
-
canApprove,
|
|
109
|
-
canImplement,
|
|
110
|
-
listMembers,
|
|
111
|
-
addMember,
|
|
112
|
-
removeMember,
|
|
113
|
-
updateMember
|
|
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. Run `sdrive init` first.");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
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));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getUserRole(username) {
|
|
26
|
+
const team = loadTeam();
|
|
27
|
+
const member = team.members?.find((m) => m.username === username);
|
|
28
|
+
return member ? member.role : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function canApprove(username) {
|
|
32
|
+
const role = getUserRole(username);
|
|
33
|
+
if (!role) return false;
|
|
34
|
+
|
|
35
|
+
const team = loadTeam();
|
|
36
|
+
return team.rules?.approval_required?.includes(role) || false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function canImplement(username) {
|
|
40
|
+
const role = getUserRole(username);
|
|
41
|
+
if (!role) return false;
|
|
42
|
+
|
|
43
|
+
const team = loadTeam();
|
|
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;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
loadTeam,
|
|
106
|
+
saveTeam,
|
|
107
|
+
getUserRole,
|
|
108
|
+
canApprove,
|
|
109
|
+
canImplement,
|
|
110
|
+
listMembers,
|
|
111
|
+
addMember,
|
|
112
|
+
removeMember,
|
|
113
|
+
updateMember
|
|
114
114
|
};
|