contract-driven-delivery 1.0.1 → 1.6.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.
- package/README.md +96 -1
- package/assets/CLAUDE.template.md +59 -3
- package/assets/agents/backend-engineer.md +43 -0
- package/assets/agents/change-classifier.md +40 -0
- package/assets/agents/ci-cd-gatekeeper.md +53 -4
- package/assets/agents/contract-reviewer.md +49 -3
- package/assets/agents/dependency-security-reviewer.md +95 -0
- package/assets/agents/e2e-resilience-engineer.md +42 -1
- package/assets/agents/frontend-engineer.md +44 -1
- package/assets/agents/monkey-test-engineer.md +40 -1
- package/assets/agents/qa-reviewer.md +52 -0
- package/assets/agents/repo-context-scanner.md +40 -0
- package/assets/agents/spec-architect.md +77 -3
- package/assets/agents/spec-drift-auditor.md +40 -0
- package/assets/agents/stress-soak-engineer.md +42 -0
- package/assets/agents/test-strategist.md +44 -1
- package/assets/agents/ui-ux-reviewer.md +41 -1
- package/assets/agents/visual-reviewer.md +41 -1
- package/assets/ci/github-actions/contract-driven-gates.yml +50 -5
- package/assets/ci-templates/bun.yml +5 -0
- package/assets/ci-templates/conda.yml +11 -0
- package/assets/ci-templates/go.yml +12 -0
- package/assets/ci-templates/npm.yml +6 -0
- package/assets/ci-templates/pip.yml +10 -0
- package/assets/ci-templates/pnpm.yml +9 -0
- package/assets/ci-templates/poetry.yml +12 -0
- package/assets/ci-templates/rust.yml +12 -0
- package/assets/ci-templates/unknown.yml +4 -0
- package/assets/ci-templates/uv.yml +12 -0
- package/assets/ci-templates/yarn.yml +6 -0
- package/assets/contracts/CHANGELOG.md +27 -0
- package/assets/contracts/api/api-contract.md +7 -0
- package/assets/contracts/business/business-rules.md +7 -0
- package/assets/contracts/ci/ci-gate-contract.md +7 -0
- package/assets/contracts/css/css-contract.md +7 -0
- package/assets/contracts/data/data-shape-contract.md +7 -0
- package/assets/contracts/env/env-contract.md +7 -0
- package/assets/hooks/pre-commit +23 -0
- package/assets/skill/SKILL.md +20 -4
- package/assets/skill/scripts/detect_project_profile.py +68 -1
- package/assets/skill/scripts/generate_change_scaffold.py +2 -2
- package/assets/skill/scripts/validate_api_semantic.py +162 -0
- package/assets/skill/scripts/validate_ci_gates.py +34 -6
- package/assets/skill/scripts/validate_contract_versions.py +385 -0
- package/assets/skill/scripts/validate_contracts.py +25 -1
- package/assets/skill/scripts/validate_env_contract.py +3 -1
- package/assets/skill/scripts/validate_env_semantic.py +182 -0
- package/assets/skill/scripts/validate_spec_traceability.py +34 -8
- package/assets/tests-templates/soak/k6-example.js +19 -0
- package/assets/tests-templates/soak/locust-example.py +21 -0
- package/assets/tests-templates/soak/soak-profile.md +16 -0
- package/assets/tests-templates/stress/artillery-example.yml +27 -0
- package/assets/tests-templates/stress/k6-example.js +22 -0
- package/assets/tests-templates/stress/load-profile.md +14 -0
- package/assets/tests-templates/stress/locust-example.py +21 -0
- package/dist/cli/index.js +593 -106
- package/package.json +6 -3
- package/assets/skill/agents/openai.yaml +0 -2
package/dist/cli/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// src/cli/index.ts
|
|
2
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4
|
+
import { dirname as dirname3, join as join10 } from "path";
|
|
2
5
|
import { Command } from "commander";
|
|
3
6
|
|
|
4
7
|
// src/commands/init.ts
|
|
5
|
-
import { join as
|
|
8
|
+
import { join as join4 } from "path";
|
|
9
|
+
import { rmSync, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
|
|
6
10
|
|
|
7
11
|
// src/utils/paths.ts
|
|
8
12
|
import { join, dirname } from "path";
|
|
@@ -21,6 +25,7 @@ var ASSET = {
|
|
|
21
25
|
specsTemplates: join(ASSETS_DIR, "specs-templates"),
|
|
22
26
|
testsTemplates: join(ASSETS_DIR, "tests-templates"),
|
|
23
27
|
ci: join(ASSETS_DIR, "ci"),
|
|
28
|
+
hooks: join(ASSETS_DIR, "hooks"),
|
|
24
29
|
claudeTemplate: join(ASSETS_DIR, "CLAUDE.template.md"),
|
|
25
30
|
agentsTemplate: join(ASSETS_DIR, "AGENTS.template.md")
|
|
26
31
|
};
|
|
@@ -66,21 +71,25 @@ var log = {
|
|
|
66
71
|
function ensureDir(dir) {
|
|
67
72
|
mkdirSync(dir, { recursive: true });
|
|
68
73
|
}
|
|
69
|
-
function
|
|
74
|
+
function copyDirTracked(src, dest, opts = {}) {
|
|
70
75
|
const { overwrite = true, label } = opts;
|
|
71
76
|
if (!existsSync(src)) {
|
|
72
77
|
log.warn(`source not found, skipping: ${label ?? src}`);
|
|
73
|
-
return 0;
|
|
78
|
+
return { count: 0, created: [] };
|
|
74
79
|
}
|
|
75
80
|
ensureDir(dest);
|
|
76
81
|
let count = 0;
|
|
82
|
+
const created = [];
|
|
77
83
|
function walk(currentSrc, currentDest) {
|
|
78
84
|
const entries = readdirSync(currentSrc, { withFileTypes: true });
|
|
79
85
|
for (const entry of entries) {
|
|
80
86
|
const srcPath = join2(currentSrc, entry.name);
|
|
81
87
|
const destPath = join2(currentDest, entry.name);
|
|
82
88
|
if (entry.isDirectory()) {
|
|
89
|
+
const isNew = !existsSync(destPath);
|
|
83
90
|
ensureDir(destPath);
|
|
91
|
+
if (isNew)
|
|
92
|
+
created.push(destPath);
|
|
84
93
|
walk(srcPath, destPath);
|
|
85
94
|
} else {
|
|
86
95
|
if (!overwrite && existsSync(destPath)) {
|
|
@@ -88,17 +97,24 @@ function copyDir(src, dest, opts = {}) {
|
|
|
88
97
|
log.dim(`skip ${relPath}`);
|
|
89
98
|
continue;
|
|
90
99
|
}
|
|
100
|
+
const isNew = !existsSync(destPath);
|
|
91
101
|
ensureDir(dirname2(destPath));
|
|
92
102
|
copyFileSync(srcPath, destPath);
|
|
103
|
+
if (isNew)
|
|
104
|
+
created.push(destPath);
|
|
93
105
|
count += 1;
|
|
94
106
|
}
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
walk(src, dest);
|
|
98
|
-
return count;
|
|
110
|
+
return { count, created };
|
|
99
111
|
}
|
|
100
112
|
function copyFile(src, dest, opts = {}) {
|
|
101
113
|
const { overwrite = true, label } = opts;
|
|
114
|
+
if (!existsSync(src)) {
|
|
115
|
+
log.warn(`source not found, skipping: ${label ?? src}`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
102
118
|
if (!overwrite && existsSync(dest)) {
|
|
103
119
|
log.dim(`skip ${label ?? dest}`);
|
|
104
120
|
return false;
|
|
@@ -107,68 +123,258 @@ function copyFile(src, dest, opts = {}) {
|
|
|
107
123
|
copyFileSync(src, dest);
|
|
108
124
|
return true;
|
|
109
125
|
}
|
|
126
|
+
function copyFileTracked(src, dest, opts = {}) {
|
|
127
|
+
const { overwrite = true, label } = opts;
|
|
128
|
+
if (!existsSync(src)) {
|
|
129
|
+
log.warn(`source not found, skipping: ${label ?? src}`);
|
|
130
|
+
return { written: false, created: false };
|
|
131
|
+
}
|
|
132
|
+
if (!overwrite && existsSync(dest)) {
|
|
133
|
+
log.dim(`skip ${label ?? dest}`);
|
|
134
|
+
return { written: false, created: false };
|
|
135
|
+
}
|
|
136
|
+
const isNew = !existsSync(dest);
|
|
137
|
+
ensureDir(dirname2(dest));
|
|
138
|
+
copyFileSync(src, dest);
|
|
139
|
+
return { written: true, created: isNew };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/utils/stack-detect.ts
|
|
143
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
144
|
+
import { join as join3 } from "path";
|
|
145
|
+
function safeExists(filePath) {
|
|
146
|
+
try {
|
|
147
|
+
return existsSync2(filePath);
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function safeRead(filePath) {
|
|
153
|
+
try {
|
|
154
|
+
return readFileSync(filePath, "utf8");
|
|
155
|
+
} catch {
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function detectPython(repoRoot) {
|
|
160
|
+
if (safeExists(join3(repoRoot, "environment.yml")) || safeExists(join3(repoRoot, "conda-lock.yml")) || safeExists(join3(repoRoot, "meta.yaml"))) {
|
|
161
|
+
return "conda";
|
|
162
|
+
}
|
|
163
|
+
if (safeExists(join3(repoRoot, "pyproject.toml"))) {
|
|
164
|
+
const content = safeRead(join3(repoRoot, "pyproject.toml"));
|
|
165
|
+
if (content.includes("[tool.poetry]")) {
|
|
166
|
+
return "poetry";
|
|
167
|
+
}
|
|
168
|
+
return "uv";
|
|
169
|
+
}
|
|
170
|
+
if (safeExists(join3(repoRoot, "requirements.txt"))) {
|
|
171
|
+
return "pip";
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function detectJS(repoRoot) {
|
|
176
|
+
if (!safeExists(join3(repoRoot, "package.json"))) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (safeExists(join3(repoRoot, "pnpm-lock.yaml")))
|
|
180
|
+
return "pnpm";
|
|
181
|
+
if (safeExists(join3(repoRoot, "bun.lockb")))
|
|
182
|
+
return "bun";
|
|
183
|
+
if (safeExists(join3(repoRoot, "yarn.lock")))
|
|
184
|
+
return "yarn";
|
|
185
|
+
return "npm";
|
|
186
|
+
}
|
|
187
|
+
function detectGo(repoRoot) {
|
|
188
|
+
return safeExists(join3(repoRoot, "go.mod")) ? "go" : null;
|
|
189
|
+
}
|
|
190
|
+
function detectRust(repoRoot) {
|
|
191
|
+
return safeExists(join3(repoRoot, "Cargo.toml")) ? "rust" : null;
|
|
192
|
+
}
|
|
193
|
+
function detectStack(repoRoot) {
|
|
194
|
+
const candidates = [];
|
|
195
|
+
const python = detectPython(repoRoot);
|
|
196
|
+
if (python)
|
|
197
|
+
candidates.push(python);
|
|
198
|
+
const js = detectJS(repoRoot);
|
|
199
|
+
if (js)
|
|
200
|
+
candidates.push(js);
|
|
201
|
+
const go = detectGo(repoRoot);
|
|
202
|
+
if (go)
|
|
203
|
+
candidates.push(go);
|
|
204
|
+
const rust = detectRust(repoRoot);
|
|
205
|
+
if (rust)
|
|
206
|
+
candidates.push(rust);
|
|
207
|
+
if (candidates.length === 0) {
|
|
208
|
+
return { primary: "unknown", candidates: [], polyglot: false };
|
|
209
|
+
}
|
|
210
|
+
const PYTHON_STACKS = ["conda", "poetry", "uv", "pip"];
|
|
211
|
+
const JS_STACKS = ["pnpm", "bun", "yarn", "npm"];
|
|
212
|
+
const hasPython = candidates.some((c) => PYTHON_STACKS.includes(c));
|
|
213
|
+
const hasJS = candidates.some((c) => JS_STACKS.includes(c));
|
|
214
|
+
const hasGo = candidates.includes("go");
|
|
215
|
+
const hasRust = candidates.includes("rust");
|
|
216
|
+
const languageCount = [hasPython, hasJS, hasGo, hasRust].filter(Boolean).length;
|
|
217
|
+
const polyglot = languageCount > 1;
|
|
218
|
+
return {
|
|
219
|
+
primary: candidates[0],
|
|
220
|
+
candidates,
|
|
221
|
+
polyglot
|
|
222
|
+
};
|
|
223
|
+
}
|
|
110
224
|
|
|
111
225
|
// src/commands/init.ts
|
|
226
|
+
function loadCiTemplate(stack) {
|
|
227
|
+
const templatePath = join4(ASSETS_DIR, "ci-templates", `${stack}.yml`);
|
|
228
|
+
if (!existsSync3(templatePath))
|
|
229
|
+
return null;
|
|
230
|
+
try {
|
|
231
|
+
return readFileSync2(templatePath, "utf8");
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function patchFastGateYml(baseYml, fragment, stack) {
|
|
237
|
+
const lines = baseYml.split("\n");
|
|
238
|
+
const PLACEHOLDER_NAME = " - name: Repository-specific fast gate";
|
|
239
|
+
const startIdx = lines.findIndex((l) => l.startsWith(PLACEHOLDER_NAME));
|
|
240
|
+
if (startIdx === -1) {
|
|
241
|
+
return baseYml;
|
|
242
|
+
}
|
|
243
|
+
let endIdx = startIdx + 1;
|
|
244
|
+
while (endIdx < lines.length) {
|
|
245
|
+
const line = lines[endIdx];
|
|
246
|
+
if (line.startsWith(" - ") && endIdx > startIdx || line.match(/^ \S/) && !line.startsWith(" ")) {
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
endIdx++;
|
|
250
|
+
}
|
|
251
|
+
const fragmentLines = fragment.trimEnd().split("\n").map((l) => l === "" ? "" : " " + l);
|
|
252
|
+
const before = lines.slice(0, startIdx);
|
|
253
|
+
const after = lines.slice(endIdx);
|
|
254
|
+
return [...before, ...fragmentLines, ...after].join("\n");
|
|
255
|
+
}
|
|
112
256
|
async function init(opts) {
|
|
113
257
|
if (opts.globalOnly && opts.localOnly) {
|
|
114
258
|
log.error("--global-only and --local-only are mutually exclusive.");
|
|
115
259
|
process.exit(1);
|
|
116
260
|
}
|
|
117
261
|
const cwd = process.cwd();
|
|
262
|
+
const createdPaths = [];
|
|
263
|
+
function track(paths) {
|
|
264
|
+
createdPaths.push(...paths);
|
|
265
|
+
}
|
|
266
|
+
function rollback() {
|
|
267
|
+
log.warn("Rolling back created paths due to error\u2026");
|
|
268
|
+
for (const p of [...createdPaths].reverse()) {
|
|
269
|
+
try {
|
|
270
|
+
rmSync(p, { recursive: true, force: true });
|
|
271
|
+
log.dim(`rolled back: ${p}`);
|
|
272
|
+
} catch {
|
|
273
|
+
log.warn(`could not remove: ${p}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
118
277
|
log.blank();
|
|
119
278
|
log.info("Initialising contract-driven-delivery kit\u2026");
|
|
120
279
|
log.blank();
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
log.ok(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
280
|
+
try {
|
|
281
|
+
if (!opts.localOnly) {
|
|
282
|
+
log.info(`Installing agents \u2192 ${AGENTS_HOME}`);
|
|
283
|
+
const { count: agentCount, created: agentCreated } = copyDirTracked(ASSET.agents, AGENTS_HOME, { overwrite: true });
|
|
284
|
+
track(agentCreated);
|
|
285
|
+
log.ok(`${agentCount} agent file(s) installed.`);
|
|
286
|
+
const skillDest = join4(SKILLS_HOME, "contract-driven-delivery");
|
|
287
|
+
log.info(`Installing skill \u2192 ${skillDest}`);
|
|
288
|
+
const { count: skillCount, created: skillCreated } = copyDirTracked(ASSET.skill, skillDest, { overwrite: true });
|
|
289
|
+
track(skillCreated);
|
|
290
|
+
log.ok(`${skillCount} skill file(s) installed.`);
|
|
291
|
+
log.blank();
|
|
292
|
+
}
|
|
293
|
+
if (!opts.globalOnly) {
|
|
294
|
+
log.info(`Scaffolding project files in ${cwd}`);
|
|
295
|
+
const { count: contractsCount, created: contractsCreated } = copyDirTracked(
|
|
296
|
+
ASSET.contracts,
|
|
297
|
+
join4(cwd, "contracts"),
|
|
298
|
+
{ overwrite: opts.force, label: "contracts" }
|
|
299
|
+
);
|
|
300
|
+
track(contractsCreated);
|
|
301
|
+
log.ok(`contracts/ \u2014 ${contractsCount} file(s) written.`);
|
|
302
|
+
const { count: specsCount, created: specsCreated } = copyDirTracked(
|
|
303
|
+
ASSET.specsTemplates,
|
|
304
|
+
join4(cwd, "specs", "templates"),
|
|
305
|
+
{ overwrite: opts.force, label: "specs/templates" }
|
|
306
|
+
);
|
|
307
|
+
track(specsCreated);
|
|
308
|
+
log.ok(`specs/templates/ \u2014 ${specsCount} file(s) written.`);
|
|
309
|
+
const { count: testsCount, created: testsCreated } = copyDirTracked(
|
|
310
|
+
ASSET.testsTemplates,
|
|
311
|
+
join4(cwd, "tests", "templates"),
|
|
312
|
+
{ overwrite: opts.force, label: "tests/templates" }
|
|
313
|
+
);
|
|
314
|
+
track(testsCreated);
|
|
315
|
+
log.ok(`tests/templates/ \u2014 ${testsCount} file(s) written.`);
|
|
316
|
+
const { count: ciCount, created: ciCreated } = copyDirTracked(
|
|
317
|
+
ASSET.ci,
|
|
318
|
+
join4(cwd, "ci"),
|
|
319
|
+
{ overwrite: opts.force, label: "ci" }
|
|
320
|
+
);
|
|
321
|
+
track(ciCreated);
|
|
322
|
+
log.ok(`ci/ \u2014 ${ciCount} file(s) written.`);
|
|
323
|
+
const detection = detectStack(cwd);
|
|
324
|
+
if (detection.polyglot) {
|
|
325
|
+
const PYTHON_STACKS = ["conda", "poetry", "uv", "pip"];
|
|
326
|
+
const JS_STACKS = ["pnpm", "bun", "yarn", "npm"];
|
|
327
|
+
const other = detection.candidates.find((c) => {
|
|
328
|
+
if (PYTHON_STACKS.includes(detection.primary))
|
|
329
|
+
return !PYTHON_STACKS.includes(c);
|
|
330
|
+
if (JS_STACKS.includes(detection.primary))
|
|
331
|
+
return !JS_STACKS.includes(c);
|
|
332
|
+
return c !== detection.primary;
|
|
333
|
+
});
|
|
334
|
+
log.warn(
|
|
335
|
+
`Polyglot detected: ${detection.primary} and ${other ?? detection.candidates[1]}. Generated config for ${detection.primary}.`
|
|
336
|
+
);
|
|
337
|
+
} else if (detection.primary !== "unknown") {
|
|
338
|
+
log.info(`Detected stack: ${detection.primary}`);
|
|
339
|
+
} else {
|
|
340
|
+
log.warn("Could not detect stack \u2014 CI placeholder left in place.");
|
|
341
|
+
}
|
|
342
|
+
const ciYmlDest = join4(cwd, "ci", "github-actions", "contract-driven-gates.yml");
|
|
343
|
+
if (existsSync3(ciYmlDest)) {
|
|
344
|
+
const template = loadCiTemplate(detection.primary);
|
|
345
|
+
if (template) {
|
|
346
|
+
const original = readFileSync2(ciYmlDest, "utf8");
|
|
347
|
+
const patched = patchFastGateYml(original, template, detection.primary);
|
|
348
|
+
if (patched !== original) {
|
|
349
|
+
writeFileSync(ciYmlDest, patched, "utf8");
|
|
350
|
+
log.ok(`CI fast-gate patched for stack: ${detection.primary}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const { written: claudeWritten, created: claudeCreated } = copyFileTracked(
|
|
355
|
+
ASSET.claudeTemplate,
|
|
356
|
+
join4(cwd, "CLAUDE.md"),
|
|
357
|
+
{ overwrite: false, label: "CLAUDE.md" }
|
|
358
|
+
);
|
|
359
|
+
if (claudeCreated)
|
|
360
|
+
track([join4(cwd, "CLAUDE.md")]);
|
|
361
|
+
if (claudeWritten)
|
|
362
|
+
log.ok("CLAUDE.md created.");
|
|
363
|
+
const { written: agentsWritten, created: agentsCreated } = copyFileTracked(
|
|
364
|
+
ASSET.agentsTemplate,
|
|
365
|
+
join4(cwd, "AGENTS.md"),
|
|
366
|
+
{ overwrite: false, label: "AGENTS.md" }
|
|
367
|
+
);
|
|
368
|
+
if (agentsCreated)
|
|
369
|
+
track([join4(cwd, "AGENTS.md")]);
|
|
370
|
+
if (agentsWritten)
|
|
371
|
+
log.ok("AGENTS.md created.");
|
|
372
|
+
log.blank();
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
log.error(`Init failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
376
|
+
rollback();
|
|
377
|
+
process.exit(1);
|
|
172
378
|
}
|
|
173
379
|
log.ok("Done.");
|
|
174
380
|
log.blank();
|
|
@@ -177,27 +383,123 @@ async function init(opts) {
|
|
|
177
383
|
}
|
|
178
384
|
|
|
179
385
|
// src/commands/update.ts
|
|
180
|
-
import { join as
|
|
181
|
-
|
|
386
|
+
import { join as join5 } from "path";
|
|
387
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, copyFileSync as copyFileSync2, readFileSync as readFileSync3 } from "fs";
|
|
388
|
+
import { createHash } from "crypto";
|
|
389
|
+
import { homedir as homedir2 } from "os";
|
|
390
|
+
function fileHash(filePath) {
|
|
391
|
+
const buf = readFileSync3(filePath);
|
|
392
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
393
|
+
}
|
|
394
|
+
function diffDir(src, dest) {
|
|
395
|
+
const entries = [];
|
|
396
|
+
if (!existsSync4(src))
|
|
397
|
+
return entries;
|
|
398
|
+
function walk(currentSrc, currentDest) {
|
|
399
|
+
const items = readdirSync2(currentSrc, { withFileTypes: true });
|
|
400
|
+
for (const item of items) {
|
|
401
|
+
const srcPath = join5(currentSrc, item.name);
|
|
402
|
+
const destPath = join5(currentDest, item.name);
|
|
403
|
+
if (item.isDirectory()) {
|
|
404
|
+
walk(srcPath, destPath);
|
|
405
|
+
} else {
|
|
406
|
+
if (!existsSync4(destPath)) {
|
|
407
|
+
entries.push({ src: srcPath, dest: destPath, action: "add" });
|
|
408
|
+
} else if (fileHash(srcPath) !== fileHash(destPath)) {
|
|
409
|
+
entries.push({ src: srcPath, dest: destPath, action: "overwrite" });
|
|
410
|
+
} else {
|
|
411
|
+
entries.push({ src: srcPath, dest: destPath, action: "skip" });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
walk(src, dest);
|
|
417
|
+
return entries;
|
|
418
|
+
}
|
|
419
|
+
function applyDir(entries) {
|
|
420
|
+
let count = 0;
|
|
421
|
+
for (const e of entries) {
|
|
422
|
+
if (e.action === "skip")
|
|
423
|
+
continue;
|
|
424
|
+
mkdirSync2(join5(e.dest, ".."), { recursive: true });
|
|
425
|
+
copyFileSync2(e.src, e.dest);
|
|
426
|
+
count += 1;
|
|
427
|
+
}
|
|
428
|
+
return count;
|
|
429
|
+
}
|
|
430
|
+
function backupDir(dir, backupDest) {
|
|
431
|
+
if (!existsSync4(dir))
|
|
432
|
+
return;
|
|
433
|
+
mkdirSync2(backupDest, { recursive: true });
|
|
434
|
+
function walk(src, dst) {
|
|
435
|
+
const items = readdirSync2(src, { withFileTypes: true });
|
|
436
|
+
for (const item of items) {
|
|
437
|
+
const s = join5(src, item.name);
|
|
438
|
+
const d = join5(dst, item.name);
|
|
439
|
+
if (item.isDirectory()) {
|
|
440
|
+
mkdirSync2(d, { recursive: true });
|
|
441
|
+
walk(s, d);
|
|
442
|
+
} else
|
|
443
|
+
copyFileSync2(s, d);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
walk(dir, backupDest);
|
|
447
|
+
}
|
|
448
|
+
async function update(opts) {
|
|
449
|
+
log.blank();
|
|
450
|
+
const skillDest = join5(SKILLS_HOME, "contract-driven-delivery");
|
|
451
|
+
const agentDiff = diffDir(ASSET.agents, AGENTS_HOME);
|
|
452
|
+
const skillDiff = diffDir(ASSET.skill, skillDest);
|
|
453
|
+
const toWrite = [...agentDiff, ...skillDiff].filter((e) => e.action !== "skip");
|
|
454
|
+
const toAdd = toWrite.filter((e) => e.action === "add");
|
|
455
|
+
const toOver = toWrite.filter((e) => e.action === "overwrite");
|
|
456
|
+
const toSkip = [...agentDiff, ...skillDiff].filter((e) => e.action === "skip");
|
|
457
|
+
log.info(`Dry-run diff \u2014 agents: ${AGENTS_HOME}`);
|
|
458
|
+
log.info(`Dry-run diff \u2014 skill: ${skillDest}`);
|
|
459
|
+
log.blank();
|
|
460
|
+
if (toAdd.length)
|
|
461
|
+
log.info(` + ${toAdd.length} file(s) would be added`);
|
|
462
|
+
if (toOver.length)
|
|
463
|
+
log.warn(` ~ ${toOver.length} file(s) would be overwritten (user edits lost without backup)`);
|
|
464
|
+
if (toSkip.length)
|
|
465
|
+
log.dim(` ${toSkip.length} file(s) unchanged (skipped)`);
|
|
466
|
+
if (toWrite.length === 0) {
|
|
467
|
+
log.blank();
|
|
468
|
+
log.ok("Already up to date \u2014 nothing to write.");
|
|
469
|
+
log.blank();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (!opts.yes) {
|
|
473
|
+
log.blank();
|
|
474
|
+
log.info("Run with --yes to apply changes. Example:");
|
|
475
|
+
log.dim(" cdd-kit update --yes");
|
|
476
|
+
log.blank();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
480
|
+
const backupRoot = join5(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
|
|
182
481
|
log.blank();
|
|
183
|
-
log.info(
|
|
482
|
+
log.info(`Backing up to ${backupRoot} \u2026`);
|
|
483
|
+
backupDir(AGENTS_HOME, join5(backupRoot, "agents"));
|
|
484
|
+
backupDir(skillDest, join5(backupRoot, "skill"));
|
|
485
|
+
log.ok(`Backup complete: ${backupRoot}`);
|
|
184
486
|
log.blank();
|
|
185
487
|
log.info(`Updating agents \u2192 ${AGENTS_HOME}`);
|
|
186
|
-
const agentCount =
|
|
488
|
+
const agentCount = applyDir(agentDiff);
|
|
187
489
|
log.ok(`${agentCount} agent file(s) updated.`);
|
|
188
|
-
const skillDest = join4(SKILLS_HOME, "contract-driven-delivery");
|
|
189
490
|
log.info(`Updating skill \u2192 ${skillDest}`);
|
|
190
|
-
const skillCount =
|
|
491
|
+
const skillCount = applyDir(skillDiff);
|
|
191
492
|
log.ok(`${skillCount} skill file(s) updated.`);
|
|
192
493
|
log.blank();
|
|
193
494
|
log.info("Project files (contracts/, specs/, tests/, ci/) were not changed.");
|
|
194
495
|
log.ok("Update complete.");
|
|
496
|
+
log.info(`Backup saved to: ${backupRoot}`);
|
|
195
497
|
log.blank();
|
|
196
498
|
}
|
|
197
499
|
|
|
198
500
|
// src/commands/new-change.ts
|
|
199
|
-
import { join as
|
|
200
|
-
import { existsSync as
|
|
501
|
+
import { join as join6 } from "path";
|
|
502
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
|
|
201
503
|
var REQUIRED_TEMPLATES = [
|
|
202
504
|
"change-request.md",
|
|
203
505
|
"change-classification.md",
|
|
@@ -205,16 +507,14 @@ var REQUIRED_TEMPLATES = [
|
|
|
205
507
|
"ci-gates.md",
|
|
206
508
|
"tasks.md"
|
|
207
509
|
];
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"archive.md"
|
|
217
|
-
];
|
|
510
|
+
function listOptional() {
|
|
511
|
+
try {
|
|
512
|
+
const all = readdirSync3(ASSET.specsTemplates).filter((f) => f.endsWith(".md"));
|
|
513
|
+
return all.filter((f) => !REQUIRED_TEMPLATES.includes(f));
|
|
514
|
+
} catch {
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
}
|
|
218
518
|
var SAFE_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
219
519
|
async function newChange(name, opts) {
|
|
220
520
|
if (!SAFE_NAME.test(name)) {
|
|
@@ -222,25 +522,30 @@ async function newChange(name, opts) {
|
|
|
222
522
|
process.exit(1);
|
|
223
523
|
}
|
|
224
524
|
const cwd = process.cwd();
|
|
225
|
-
const changeDir =
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
525
|
+
const changeDir = join6(cwd, "specs", "changes", name);
|
|
526
|
+
if (existsSync5(changeDir)) {
|
|
527
|
+
if (opts.force) {
|
|
528
|
+
log.warn(`Forcing re-scaffold of existing change directory: ${changeDir}`);
|
|
529
|
+
log.warn("Existing files will NOT be deleted; only template files will be overwritten.");
|
|
530
|
+
} else {
|
|
531
|
+
log.warn(`Change directory already exists: ${changeDir}`);
|
|
532
|
+
log.warn("Aborting \u2014 remove or rename the directory to re-scaffold.");
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
230
535
|
}
|
|
231
536
|
log.blank();
|
|
232
537
|
log.info(`Creating change scaffold: specs/changes/${name}`);
|
|
233
538
|
ensureDir(changeDir);
|
|
234
|
-
const templates = opts.all ? [...REQUIRED_TEMPLATES, ...
|
|
539
|
+
const templates = opts.all ? [...REQUIRED_TEMPLATES, ...listOptional()] : [...REQUIRED_TEMPLATES];
|
|
235
540
|
let written = 0;
|
|
236
541
|
for (const tmpl of templates) {
|
|
237
|
-
const src =
|
|
238
|
-
const dest =
|
|
239
|
-
if (!
|
|
542
|
+
const src = join6(ASSET.specsTemplates, tmpl);
|
|
543
|
+
const dest = join6(changeDir, tmpl);
|
|
544
|
+
if (!existsSync5(src)) {
|
|
240
545
|
log.warn(`Template not found, skipping: ${tmpl}`);
|
|
241
546
|
continue;
|
|
242
547
|
}
|
|
243
|
-
copyFile(src, dest, { overwrite:
|
|
548
|
+
copyFile(src, dest, { overwrite: opts.force });
|
|
244
549
|
log.dim(tmpl);
|
|
245
550
|
written += 1;
|
|
246
551
|
}
|
|
@@ -250,22 +555,29 @@ async function newChange(name, opts) {
|
|
|
250
555
|
}
|
|
251
556
|
|
|
252
557
|
// src/commands/validate.ts
|
|
253
|
-
import { join as
|
|
254
|
-
import { existsSync as
|
|
255
|
-
import {
|
|
558
|
+
import { join as join7 } from "path";
|
|
559
|
+
import { existsSync as existsSync6 } from "fs";
|
|
560
|
+
import { spawnSync } from "child_process";
|
|
256
561
|
var VALIDATORS = [
|
|
257
|
-
{
|
|
562
|
+
{
|
|
563
|
+
flag: "contracts",
|
|
564
|
+
script: "validate_contracts.py",
|
|
565
|
+
label: "contracts",
|
|
566
|
+
chain: [
|
|
567
|
+
{ script: "validate_api_semantic.py", label: "API semantic" },
|
|
568
|
+
{ script: "validate_env_semantic.py", label: "Env semantic" }
|
|
569
|
+
]
|
|
570
|
+
},
|
|
258
571
|
{ flag: "env", script: "validate_env_contract.py", label: "env contract" },
|
|
259
572
|
{ flag: "ci", script: "validate_ci_gates.py", label: "CI gates" },
|
|
260
|
-
{ flag: "spec", script: "validate_spec_traceability.py", label: "spec traceability" }
|
|
573
|
+
{ flag: "spec", script: "validate_spec_traceability.py", label: "spec traceability" },
|
|
574
|
+
{ flag: "versions", script: "validate_contract_versions.py", label: "contract versions" }
|
|
261
575
|
];
|
|
262
576
|
function resolvePython() {
|
|
263
577
|
for (const cmd of ["python3", "python"]) {
|
|
264
|
-
|
|
265
|
-
|
|
578
|
+
const r = spawnSync(cmd, ["--version"], { stdio: "ignore" });
|
|
579
|
+
if (r.status === 0)
|
|
266
580
|
return cmd;
|
|
267
|
-
} catch {
|
|
268
|
-
}
|
|
269
581
|
}
|
|
270
582
|
throw new Error("Python not found. Install Python 3.8+ and ensure it is on PATH.");
|
|
271
583
|
}
|
|
@@ -277,28 +589,47 @@ async function validate(opts) {
|
|
|
277
589
|
log.error(e instanceof Error ? e.message : String(e));
|
|
278
590
|
process.exit(1);
|
|
279
591
|
}
|
|
280
|
-
const scriptsDir =
|
|
281
|
-
const runAll = !opts.contracts && !opts.env && !opts.ci && !opts.spec;
|
|
592
|
+
const scriptsDir = join7(ASSET.skill, "scripts");
|
|
593
|
+
const runAll = !opts.contracts && !opts.env && !opts.ci && !opts.spec && !opts.versions;
|
|
282
594
|
log.blank();
|
|
283
595
|
let failed = false;
|
|
284
596
|
for (const v of VALIDATORS) {
|
|
285
597
|
if (!runAll && !opts[v.flag])
|
|
286
598
|
continue;
|
|
287
|
-
const scriptPath =
|
|
288
|
-
if (!
|
|
599
|
+
const scriptPath = join7(scriptsDir, v.script);
|
|
600
|
+
if (!existsSync6(scriptPath)) {
|
|
289
601
|
log.warn(`${v.label}: script not found, skipping (${v.script})`);
|
|
290
602
|
log.blank();
|
|
291
603
|
continue;
|
|
292
604
|
}
|
|
293
605
|
log.info(`Validating ${v.label}\u2026`);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
log.ok(`${v.label} passed.`);
|
|
297
|
-
} catch {
|
|
606
|
+
const r = spawnSync(py, [scriptPath, ...v.args ?? []], { stdio: "inherit", cwd: process.cwd() });
|
|
607
|
+
if (r.status !== 0) {
|
|
298
608
|
log.error(`${v.label} validation failed.`);
|
|
299
609
|
failed = true;
|
|
610
|
+
} else {
|
|
611
|
+
log.ok(`${v.label} passed.`);
|
|
300
612
|
}
|
|
301
613
|
log.blank();
|
|
614
|
+
if (v.chain) {
|
|
615
|
+
for (const chained of v.chain) {
|
|
616
|
+
const chainedPath = join7(scriptsDir, chained.script);
|
|
617
|
+
if (!existsSync6(chainedPath)) {
|
|
618
|
+
log.warn(`${chained.label}: script not found, skipping (${chained.script})`);
|
|
619
|
+
log.blank();
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
log.info(`Validating ${chained.label}\u2026`);
|
|
623
|
+
const cr = spawnSync(py, [chainedPath], { stdio: "inherit", cwd: process.cwd() });
|
|
624
|
+
if (cr.status !== 0) {
|
|
625
|
+
log.error(`${chained.label} validation failed.`);
|
|
626
|
+
failed = true;
|
|
627
|
+
} else {
|
|
628
|
+
log.ok(`${chained.label} passed.`);
|
|
629
|
+
}
|
|
630
|
+
log.blank();
|
|
631
|
+
}
|
|
632
|
+
}
|
|
302
633
|
}
|
|
303
634
|
if (failed) {
|
|
304
635
|
log.error("One or more validations failed.");
|
|
@@ -309,9 +640,145 @@ async function validate(opts) {
|
|
|
309
640
|
}
|
|
310
641
|
}
|
|
311
642
|
|
|
643
|
+
// src/commands/gate.ts
|
|
644
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4, readdirSync as readdirSync4 } from "fs";
|
|
645
|
+
import { join as join8 } from "path";
|
|
646
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
647
|
+
var REQUIRED_FILES = [
|
|
648
|
+
"change-request.md",
|
|
649
|
+
"change-classification.md",
|
|
650
|
+
"test-plan.md",
|
|
651
|
+
"ci-gates.md",
|
|
652
|
+
"tasks.md"
|
|
653
|
+
];
|
|
654
|
+
var TIER_PATTERN = /\b(tier\s*[0-5]|low|medium|high|critical)\b/i;
|
|
655
|
+
function meaningfulChars(text) {
|
|
656
|
+
return text.split("\n").map((l) => l.trim()).filter((l) => l).filter((l) => !l.startsWith("#")).filter((l) => !/^[|\s\-:]+$/.test(l)).filter((l) => !l.startsWith("<!--")).join("").length;
|
|
657
|
+
}
|
|
658
|
+
async function gate(changeId) {
|
|
659
|
+
const cwd = process.cwd();
|
|
660
|
+
const changeDir = join8(cwd, "specs", "changes", changeId);
|
|
661
|
+
if (!existsSync7(changeDir)) {
|
|
662
|
+
log.error(`change not found: ${changeId} (looked in ${changeDir})`);
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
const errors = [];
|
|
666
|
+
for (const f of REQUIRED_FILES) {
|
|
667
|
+
if (!existsSync7(join8(changeDir, f))) {
|
|
668
|
+
errors.push(`missing required artifact: ${f}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (errors.length === 0) {
|
|
672
|
+
for (const f of REQUIRED_FILES) {
|
|
673
|
+
const content = readFileSync4(join8(changeDir, f), "utf8");
|
|
674
|
+
if (meaningfulChars(content) < 100) {
|
|
675
|
+
errors.push(`${f}: appears to be a stub (< 100 meaningful chars)`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const classifPath = join8(changeDir, "change-classification.md");
|
|
679
|
+
if (existsSync7(classifPath)) {
|
|
680
|
+
const text = readFileSync4(classifPath, "utf8");
|
|
681
|
+
if (!TIER_PATTERN.test(text)) {
|
|
682
|
+
errors.push("change-classification.md: missing tier/risk marker (Tier 0-5 or low/medium/high/critical)");
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const agentLogDir = join8(changeDir, "agent-log");
|
|
687
|
+
if (existsSync7(agentLogDir)) {
|
|
688
|
+
const logFiles = readdirSync4(agentLogDir).filter((f) => f.endsWith(".md"));
|
|
689
|
+
for (const f of logFiles) {
|
|
690
|
+
const content = readFileSync4(join8(agentLogDir, f), "utf8");
|
|
691
|
+
const statusMatch = content.match(/^\s*-\s*status:\s*(complete|needs-review|blocked)\s*$/m);
|
|
692
|
+
if (!statusMatch) {
|
|
693
|
+
errors.push(`agent-log/${f}: missing or invalid "status:" line (must be complete | needs-review | blocked)`);
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const status = statusMatch[1];
|
|
697
|
+
if (status === "blocked") {
|
|
698
|
+
const nextActionMatch = content.match(/^\s*-\s*next-action:\s*(.+)$/m);
|
|
699
|
+
if (!nextActionMatch || nextActionMatch[1].trim().toLowerCase() === "none" || nextActionMatch[1].trim().length < 10) {
|
|
700
|
+
errors.push(`agent-log/${f}: status=blocked requires concrete "next-action:" line (>= 10 chars, not "none")`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (errors.length > 0) {
|
|
706
|
+
log.error(`gate failed for change: ${changeId}`);
|
|
707
|
+
for (const e of errors) {
|
|
708
|
+
log.error(` ${e}`);
|
|
709
|
+
}
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
log.info(`gate: running contract validators for ${changeId}\u2026`);
|
|
713
|
+
const r = spawnSync2(process.execPath, [process.argv[1], "validate"], {
|
|
714
|
+
cwd,
|
|
715
|
+
stdio: "inherit"
|
|
716
|
+
});
|
|
717
|
+
if (r.status !== 0) {
|
|
718
|
+
log.error(`gate failed for change: ${changeId} (validators returned non-zero)`);
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
log.ok(`gate passed for change: ${changeId}`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/commands/install-hooks.ts
|
|
725
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync2, chmodSync, mkdirSync as mkdirSync3 } from "fs";
|
|
726
|
+
import { join as join9 } from "path";
|
|
727
|
+
var START_MARKER = "# cdd-kit-managed-block-start";
|
|
728
|
+
var END_MARKER = "# cdd-kit-managed-block-end";
|
|
729
|
+
async function installHooks() {
|
|
730
|
+
const cwd = process.cwd();
|
|
731
|
+
const gitDir = join9(cwd, ".git");
|
|
732
|
+
if (!existsSync8(gitDir)) {
|
|
733
|
+
log.error("not a git repository (no .git/ found in cwd)");
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
const hooksDir = join9(gitDir, "hooks");
|
|
737
|
+
mkdirSync3(hooksDir, { recursive: true });
|
|
738
|
+
const dest = join9(hooksDir, "pre-commit");
|
|
739
|
+
const ourHook = readFileSync5(join9(ASSET.hooks, "pre-commit"), "utf8");
|
|
740
|
+
let final;
|
|
741
|
+
if (!existsSync8(dest)) {
|
|
742
|
+
final = ourHook;
|
|
743
|
+
} else {
|
|
744
|
+
const existing = readFileSync5(dest, "utf8");
|
|
745
|
+
const startIdx = existing.indexOf(START_MARKER);
|
|
746
|
+
const endIdx = existing.indexOf(END_MARKER);
|
|
747
|
+
if (startIdx >= 0 && endIdx > startIdx) {
|
|
748
|
+
const before = existing.slice(0, startIdx);
|
|
749
|
+
const after = existing.slice(endIdx + END_MARKER.length);
|
|
750
|
+
const ourStart = ourHook.indexOf(START_MARKER);
|
|
751
|
+
const ourEnd = ourHook.indexOf(END_MARKER) + END_MARKER.length;
|
|
752
|
+
const ourBlock = ourHook.slice(ourStart, ourEnd);
|
|
753
|
+
final = before + ourBlock + after;
|
|
754
|
+
} else {
|
|
755
|
+
const ourStart = ourHook.indexOf(START_MARKER);
|
|
756
|
+
const ourEnd = ourHook.indexOf(END_MARKER) + END_MARKER.length;
|
|
757
|
+
const ourBlock = ourHook.slice(ourStart, ourEnd);
|
|
758
|
+
if (existing.startsWith("#!")) {
|
|
759
|
+
const firstNewline = existing.indexOf("\n");
|
|
760
|
+
const shebang = existing.slice(0, firstNewline + 1);
|
|
761
|
+
const rest = existing.slice(firstNewline + 1);
|
|
762
|
+
final = shebang + "\n" + ourBlock + "\n" + rest;
|
|
763
|
+
} else {
|
|
764
|
+
final = "#!/bin/sh\n" + ourBlock + "\n" + existing;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
writeFileSync2(dest, final, "utf8");
|
|
769
|
+
try {
|
|
770
|
+
chmodSync(dest, 493);
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
773
|
+
log.ok(`pre-commit hook installed at ${dest}`);
|
|
774
|
+
log.info("cdd-kit gate will now run automatically before each commit affecting specs/changes/");
|
|
775
|
+
}
|
|
776
|
+
|
|
312
777
|
// src/cli/index.ts
|
|
778
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
779
|
+
var pkg = JSON.parse(readFileSync6(join10(__dirname2, "..", "..", "package.json"), "utf8"));
|
|
313
780
|
var program = new Command();
|
|
314
|
-
program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(
|
|
781
|
+
program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
|
|
315
782
|
program.command("init").description(
|
|
316
783
|
"Install agents/skill into ~/.claude and scaffold project files in cwd"
|
|
317
784
|
).option("--global-only", "Only install into ~/.claude, skip project files", false).option("--local-only", "Only scaffold project files, skip ~/.claude", false).option("--force", "Overwrite existing project files", false).action(
|
|
@@ -321,16 +788,36 @@ program.command("init").description(
|
|
|
321
788
|
force: opts.force
|
|
322
789
|
})
|
|
323
790
|
);
|
|
324
|
-
program.command("update").description("Update ~/.claude agents and skill (does not touch project files)").action(() => update());
|
|
325
|
-
program.command("new <name>").description("Scaffold a new change directory under specs/changes/<name>").option("--all", "Include optional templates in addition to required ones", false).action(
|
|
326
|
-
(name, opts) => newChange(name, { all: opts.all })
|
|
791
|
+
program.command("update").description("Update ~/.claude agents and skill (does not touch project files)").option("--yes", "Apply changes (default is dry-run)", false).action((opts) => update({ yes: opts.yes }));
|
|
792
|
+
program.command("new <name>").description("Scaffold a new change directory under specs/changes/<name>").option("--all", "Include optional templates in addition to required ones", false).option("--force", "Overwrite existing template files in the change folder", false).action(
|
|
793
|
+
(name, opts) => newChange(name, { all: opts.all, force: opts.force })
|
|
327
794
|
);
|
|
328
|
-
program.command("validate").description("Run validation scripts (defaults to all)").option("--contracts", "Validate API/data/CSS contracts (use --env separately for env)", false).option("--env", "Validate env contract", false).option("--ci", "Validate CI gate policy", false).option("--spec", "Validate spec traceability", false).action(
|
|
795
|
+
program.command("validate").description("Run validation scripts (defaults to all)").option("--contracts", "Validate API/data/CSS contracts (use --env separately for env)", false).option("--env", "Validate env contract", false).option("--ci", "Validate CI gate policy", false).option("--spec", "Validate spec traceability", false).option("--versions", "Validate contract frontmatter and version bumps", false).action(
|
|
329
796
|
(opts) => validate({
|
|
330
797
|
contracts: opts.contracts,
|
|
331
798
|
env: opts.env,
|
|
332
799
|
ci: opts.ci,
|
|
333
|
-
spec: opts.spec
|
|
800
|
+
spec: opts.spec,
|
|
801
|
+
versions: opts.versions
|
|
334
802
|
})
|
|
335
803
|
);
|
|
804
|
+
program.command("gate <change-id>").description("Run full orchestration gate for a change (required artifacts, content, tier, contracts)").action(async (id) => {
|
|
805
|
+
await gate(id);
|
|
806
|
+
});
|
|
807
|
+
program.command("install-hooks").description("Install pre-commit hook that runs cdd-kit gate on staged changes").action(async () => {
|
|
808
|
+
await installHooks();
|
|
809
|
+
});
|
|
810
|
+
program.command("detect-stack").description("Detect the project tech stack and print the result").action(() => {
|
|
811
|
+
const cwd = process.cwd();
|
|
812
|
+
const result = detectStack(cwd);
|
|
813
|
+
console.log(`Detected stack: ${result.primary}`);
|
|
814
|
+
if (result.candidates.length > 1) {
|
|
815
|
+
console.log(`Candidates (in order): ${result.candidates.join(", ")}`);
|
|
816
|
+
}
|
|
817
|
+
if (result.polyglot) {
|
|
818
|
+
console.log(
|
|
819
|
+
`Polyglot: yes (config will be generated for ${result.primary})`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
336
823
|
program.parse();
|