everything-claude-code 1.4.3
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/LICENSE +21 -0
- package/README.md +739 -0
- package/README.zh-CN.md +523 -0
- package/crates/ecc-kernel/Cargo.lock +160 -0
- package/crates/ecc-kernel/Cargo.toml +15 -0
- package/crates/ecc-kernel/src/main.rs +710 -0
- package/docs/ecc.md +117 -0
- package/package.json +45 -0
- package/packs/blueprint.json +8 -0
- package/packs/forge.json +16 -0
- package/packs/instinct.json +16 -0
- package/packs/orchestra.json +15 -0
- package/packs/proof.json +8 -0
- package/packs/sentinel.json +8 -0
- package/prompts/ecc/patch.md +25 -0
- package/prompts/ecc/plan.md +28 -0
- package/schemas/ecc.apply.schema.json +35 -0
- package/schemas/ecc.config.schema.json +37 -0
- package/schemas/ecc.lock.schema.json +34 -0
- package/schemas/ecc.patch.schema.json +25 -0
- package/schemas/ecc.plan.schema.json +32 -0
- package/schemas/ecc.run.schema.json +67 -0
- package/schemas/ecc.verify.schema.json +27 -0
- package/schemas/hooks.schema.json +81 -0
- package/schemas/package-manager.schema.json +17 -0
- package/schemas/plugin.schema.json +13 -0
- package/scripts/ecc/catalog.js +82 -0
- package/scripts/ecc/config.js +43 -0
- package/scripts/ecc/diff.js +113 -0
- package/scripts/ecc/exec.js +121 -0
- package/scripts/ecc/fixtures/basic/patches/impl-core.diff +8 -0
- package/scripts/ecc/fixtures/basic/patches/tests.diff +8 -0
- package/scripts/ecc/fixtures/basic/plan.json +23 -0
- package/scripts/ecc/fixtures/unauthorized/patches/impl-core.diff +8 -0
- package/scripts/ecc/fixtures/unauthorized/plan.json +15 -0
- package/scripts/ecc/git.js +139 -0
- package/scripts/ecc/id.js +37 -0
- package/scripts/ecc/install-kernel.js +344 -0
- package/scripts/ecc/json-extract.js +301 -0
- package/scripts/ecc/json.js +26 -0
- package/scripts/ecc/kernel.js +144 -0
- package/scripts/ecc/lock.js +36 -0
- package/scripts/ecc/paths.js +28 -0
- package/scripts/ecc/plan.js +57 -0
- package/scripts/ecc/project.js +37 -0
- package/scripts/ecc/providers/codex.js +168 -0
- package/scripts/ecc/providers/index.js +23 -0
- package/scripts/ecc/providers/mock.js +49 -0
- package/scripts/ecc/report.js +127 -0
- package/scripts/ecc/run.js +105 -0
- package/scripts/ecc/validate.js +325 -0
- package/scripts/ecc/verify.js +125 -0
- package/scripts/ecc.js +532 -0
- package/scripts/lib/package-manager.js +390 -0
- package/scripts/lib/session-aliases.js +432 -0
- package/scripts/lib/session-manager.js +396 -0
- package/scripts/lib/utils.js +426 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const { writeJson, readJson, writeText } = require('./json');
|
|
5
|
+
const { runsDir } = require('./project');
|
|
6
|
+
const { validateRun, throwIfErrors } = require('./validate');
|
|
7
|
+
|
|
8
|
+
function nowIso() {
|
|
9
|
+
return new Date().toISOString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function runRoot(projectRoot, runId) {
|
|
13
|
+
return path.join(runsDir(projectRoot), runId);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runPaths(projectRoot, runId) {
|
|
17
|
+
const root = runRoot(projectRoot, runId);
|
|
18
|
+
return {
|
|
19
|
+
root,
|
|
20
|
+
intentTxt: path.join(root, 'intent.txt'),
|
|
21
|
+
runJson: path.join(root, 'run.json'),
|
|
22
|
+
planJson: path.join(root, 'plan.json'),
|
|
23
|
+
planMd: path.join(root, 'plan.md'),
|
|
24
|
+
patchesDir: path.join(root, 'patches'),
|
|
25
|
+
applyDir: path.join(root, 'apply'),
|
|
26
|
+
applyJson: path.join(root, 'apply', 'applied.json'),
|
|
27
|
+
verifyDir: path.join(root, 'verify'),
|
|
28
|
+
verifySummaryJson: path.join(root, 'verify', 'summary.json'),
|
|
29
|
+
reportMd: path.join(root, 'report.md')
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function initRun({ projectRoot, runId, intent, backend, packs, base }) {
|
|
34
|
+
const paths = runPaths(projectRoot, runId);
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(paths.runJson)) {
|
|
37
|
+
throw new Error(`Run already exists: ${runId}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fs.mkdirSync(paths.root, { recursive: true });
|
|
41
|
+
fs.mkdirSync(paths.patchesDir, { recursive: true });
|
|
42
|
+
fs.mkdirSync(paths.applyDir, { recursive: true });
|
|
43
|
+
fs.mkdirSync(paths.verifyDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
writeText(paths.intentTxt, intent + '\n');
|
|
46
|
+
|
|
47
|
+
const run = {
|
|
48
|
+
version: 1,
|
|
49
|
+
runId,
|
|
50
|
+
intent,
|
|
51
|
+
backend,
|
|
52
|
+
packs,
|
|
53
|
+
status: 'planned',
|
|
54
|
+
base: {
|
|
55
|
+
repoRoot: base.repoRoot,
|
|
56
|
+
branch: base.branch,
|
|
57
|
+
sha: base.sha
|
|
58
|
+
},
|
|
59
|
+
worktree: {
|
|
60
|
+
path: '',
|
|
61
|
+
branch: `ecc/${runId}`
|
|
62
|
+
},
|
|
63
|
+
artifacts: {
|
|
64
|
+
planJson: paths.planJson,
|
|
65
|
+
planMd: paths.planMd,
|
|
66
|
+
patchesDir: paths.patchesDir,
|
|
67
|
+
applyJson: paths.applyJson,
|
|
68
|
+
verifyDir: paths.verifyDir,
|
|
69
|
+
reportMd: paths.reportMd
|
|
70
|
+
},
|
|
71
|
+
startedAt: nowIso()
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
throwIfErrors(validateRun(run), 'run');
|
|
75
|
+
writeJson(paths.runJson, run);
|
|
76
|
+
return { run, paths };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function loadRun(projectRoot, runId) {
|
|
80
|
+
const p = runPaths(projectRoot, runId).runJson;
|
|
81
|
+
if (!fs.existsSync(p)) return null;
|
|
82
|
+
const run = readJson(p);
|
|
83
|
+
throwIfErrors(validateRun(run), 'run');
|
|
84
|
+
return run;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function saveRun(projectRoot, runId, run) {
|
|
88
|
+
throwIfErrors(validateRun(run), 'run');
|
|
89
|
+
writeJson(runPaths(projectRoot, runId).runJson, run);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function markRunEnded(run, status) {
|
|
93
|
+
run.status = status;
|
|
94
|
+
run.endedAt = nowIso();
|
|
95
|
+
return run;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
runPaths,
|
|
100
|
+
initRun,
|
|
101
|
+
loadRun,
|
|
102
|
+
saveRun,
|
|
103
|
+
markRunEnded
|
|
104
|
+
};
|
|
105
|
+
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECC validators (dependency-free)
|
|
3
|
+
*
|
|
4
|
+
* These validators are intentionally strict on required fields/types and
|
|
5
|
+
* provide a stable internal contract independent of external JSON Schema libs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function isObj(v) {
|
|
9
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isStr(v) {
|
|
13
|
+
return typeof v === 'string';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isBool(v) {
|
|
17
|
+
return typeof v === 'boolean';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isInt(v) {
|
|
21
|
+
return Number.isInteger(v);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pushErr(errors, path, message) {
|
|
25
|
+
errors.push({ path, message });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateString(errors, path, v, { minLength = 1 } = {}) {
|
|
29
|
+
if (!isStr(v)) {
|
|
30
|
+
pushErr(errors, path, 'expected string');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (v.length < minLength) pushErr(errors, path, `expected string length >= ${minLength}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validateStringArray(errors, path, v, { minItems = 0 } = {}) {
|
|
37
|
+
if (!Array.isArray(v)) {
|
|
38
|
+
pushErr(errors, path, 'expected array');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (v.length < minItems) pushErr(errors, path, `expected array length >= ${minItems}`);
|
|
42
|
+
for (let i = 0; i < v.length; i++) validateString(errors, `${path}[${i}]`, v[i]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validateConfig(cfg) {
|
|
46
|
+
const errors = [];
|
|
47
|
+
if (!isObj(cfg)) {
|
|
48
|
+
pushErr(errors, '$', 'expected object');
|
|
49
|
+
return errors;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (cfg.version !== 1) pushErr(errors, '$.version', 'expected 1');
|
|
53
|
+
if (!['codex', 'claude'].includes(cfg.backend)) {
|
|
54
|
+
pushErr(errors, '$.backend', 'expected "codex" or "claude"');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
validateStringArray(errors, '$.packs', cfg.packs, { minItems: 1 });
|
|
58
|
+
validateString(errors, '$.createdAt', cfg.createdAt);
|
|
59
|
+
|
|
60
|
+
if (!isObj(cfg.verify)) {
|
|
61
|
+
pushErr(errors, '$.verify', 'expected object');
|
|
62
|
+
} else {
|
|
63
|
+
if (!['auto', 'manual'].includes(cfg.verify.mode)) {
|
|
64
|
+
pushErr(errors, '$.verify.mode', 'expected "auto" or "manual"');
|
|
65
|
+
}
|
|
66
|
+
if (cfg.verify.commands !== undefined) {
|
|
67
|
+
if (!Array.isArray(cfg.verify.commands)) {
|
|
68
|
+
pushErr(errors, '$.verify.commands', 'expected array');
|
|
69
|
+
} else {
|
|
70
|
+
for (let i = 0; i < cfg.verify.commands.length; i++) {
|
|
71
|
+
const c = cfg.verify.commands[i];
|
|
72
|
+
const base = `$.verify.commands[${i}]`;
|
|
73
|
+
if (!isObj(c)) {
|
|
74
|
+
pushErr(errors, base, 'expected object');
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
validateString(errors, `${base}.name`, c.name);
|
|
78
|
+
validateString(errors, `${base}.command`, c.command);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return errors;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateLock(lock) {
|
|
88
|
+
const errors = [];
|
|
89
|
+
if (!isObj(lock)) {
|
|
90
|
+
pushErr(errors, '$', 'expected object');
|
|
91
|
+
return errors;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (lock.version !== 1) pushErr(errors, '$.version', 'expected 1');
|
|
95
|
+
validateString(errors, '$.lockedAt', lock.lockedAt);
|
|
96
|
+
validateStringArray(errors, '$.packs', lock.packs, { minItems: 1 });
|
|
97
|
+
|
|
98
|
+
if (!isObj(lock.engine)) {
|
|
99
|
+
pushErr(errors, '$.engine', 'expected object');
|
|
100
|
+
} else {
|
|
101
|
+
if (lock.engine.name !== 'ecc') pushErr(errors, '$.engine.name', 'expected "ecc"');
|
|
102
|
+
if (lock.engine.version !== undefined) validateString(errors, '$.engine.version', lock.engine.version);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!isObj(lock.catalog)) {
|
|
106
|
+
pushErr(errors, '$.catalog', 'expected object');
|
|
107
|
+
} else {
|
|
108
|
+
if (lock.catalog.type !== 'embedded') pushErr(errors, '$.catalog.type', 'expected "embedded"');
|
|
109
|
+
validateString(errors, '$.catalog.digest', lock.catalog.digest);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return errors;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validatePlan(plan) {
|
|
116
|
+
const errors = [];
|
|
117
|
+
if (!isObj(plan)) {
|
|
118
|
+
pushErr(errors, '$', 'expected object');
|
|
119
|
+
return errors;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (plan.version !== 1) pushErr(errors, '$.version', 'expected 1');
|
|
123
|
+
validateString(errors, '$.intent', plan.intent);
|
|
124
|
+
|
|
125
|
+
if (!Array.isArray(plan.tasks)) {
|
|
126
|
+
pushErr(errors, '$.tasks', 'expected array');
|
|
127
|
+
return errors;
|
|
128
|
+
}
|
|
129
|
+
if (plan.tasks.length < 1) pushErr(errors, '$.tasks', 'expected at least 1 task');
|
|
130
|
+
|
|
131
|
+
const ids = new Set();
|
|
132
|
+
for (let i = 0; i < plan.tasks.length; i++) {
|
|
133
|
+
const t = plan.tasks[i];
|
|
134
|
+
const base = `$.tasks[${i}]`;
|
|
135
|
+
if (!isObj(t)) {
|
|
136
|
+
pushErr(errors, base, 'expected object');
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
validateString(errors, `${base}.id`, t.id);
|
|
140
|
+
validateString(errors, `${base}.title`, t.title);
|
|
141
|
+
if (t.kind !== 'patch') pushErr(errors, `${base}.kind`, 'expected "patch"');
|
|
142
|
+
validateStringArray(errors, `${base}.dependsOn`, t.dependsOn || []);
|
|
143
|
+
validateStringArray(errors, `${base}.allowedPathPrefixes`, t.allowedPathPrefixes, { minItems: 1 });
|
|
144
|
+
validateString(errors, `${base}.prompt`, t.prompt);
|
|
145
|
+
|
|
146
|
+
if (isStr(t.id)) {
|
|
147
|
+
if (ids.has(t.id)) pushErr(errors, `${base}.id`, 'duplicate task id');
|
|
148
|
+
ids.add(t.id);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Validate dependsOn references and DAG (only if basic task objects passed)
|
|
153
|
+
const tasksById = new Map();
|
|
154
|
+
for (const t of plan.tasks) {
|
|
155
|
+
if (isObj(t) && isStr(t.id)) tasksById.set(t.id, t);
|
|
156
|
+
}
|
|
157
|
+
for (let i = 0; i < plan.tasks.length; i++) {
|
|
158
|
+
const t = plan.tasks[i];
|
|
159
|
+
if (!isObj(t) || !isStr(t.id)) continue;
|
|
160
|
+
const deps = Array.isArray(t.dependsOn) ? t.dependsOn : [];
|
|
161
|
+
for (let j = 0; j < deps.length; j++) {
|
|
162
|
+
const dep = deps[j];
|
|
163
|
+
if (!tasksById.has(dep)) pushErr(errors, `$.tasks[${i}].dependsOn[${j}]`, 'unknown task id');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Cycle detection
|
|
168
|
+
const visiting = new Set();
|
|
169
|
+
const visited = new Set();
|
|
170
|
+
function dfs(id) {
|
|
171
|
+
if (visited.has(id)) return;
|
|
172
|
+
if (visiting.has(id)) {
|
|
173
|
+
pushErr(errors, '$.tasks', `cycle detected at task "${id}"`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
visiting.add(id);
|
|
177
|
+
const t = tasksById.get(id);
|
|
178
|
+
if (t) {
|
|
179
|
+
const deps = Array.isArray(t.dependsOn) ? t.dependsOn : [];
|
|
180
|
+
for (const dep of deps) dfs(dep);
|
|
181
|
+
}
|
|
182
|
+
visiting.delete(id);
|
|
183
|
+
visited.add(id);
|
|
184
|
+
}
|
|
185
|
+
for (const id of tasksById.keys()) dfs(id);
|
|
186
|
+
|
|
187
|
+
return errors;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function validateRun(run) {
|
|
191
|
+
const errors = [];
|
|
192
|
+
if (!isObj(run)) {
|
|
193
|
+
pushErr(errors, '$', 'expected object');
|
|
194
|
+
return errors;
|
|
195
|
+
}
|
|
196
|
+
if (run.version !== 1) pushErr(errors, '$.version', 'expected 1');
|
|
197
|
+
validateString(errors, '$.runId', run.runId);
|
|
198
|
+
validateString(errors, '$.intent', run.intent);
|
|
199
|
+
if (!['codex', 'claude'].includes(run.backend)) pushErr(errors, '$.backend', 'expected "codex" or "claude"');
|
|
200
|
+
validateStringArray(errors, '$.packs', run.packs, { minItems: 1 });
|
|
201
|
+
if (!['planned', 'executing', 'verifying', 'succeeded', 'failed'].includes(run.status)) {
|
|
202
|
+
pushErr(errors, '$.status', 'invalid status');
|
|
203
|
+
}
|
|
204
|
+
validateString(errors, '$.startedAt', run.startedAt);
|
|
205
|
+
if (run.endedAt !== undefined) validateString(errors, '$.endedAt', run.endedAt);
|
|
206
|
+
|
|
207
|
+
if (!isObj(run.base)) {
|
|
208
|
+
pushErr(errors, '$.base', 'expected object');
|
|
209
|
+
} else {
|
|
210
|
+
validateString(errors, '$.base.repoRoot', run.base.repoRoot);
|
|
211
|
+
if (run.base.branch === undefined) pushErr(errors, '$.base.branch', 'missing');
|
|
212
|
+
else if (!isStr(run.base.branch)) pushErr(errors, '$.base.branch', 'expected string');
|
|
213
|
+
if (run.base.sha === undefined) pushErr(errors, '$.base.sha', 'missing');
|
|
214
|
+
else if (!isStr(run.base.sha)) pushErr(errors, '$.base.sha', 'expected string');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!isObj(run.worktree)) {
|
|
218
|
+
pushErr(errors, '$.worktree', 'expected object');
|
|
219
|
+
} else {
|
|
220
|
+
if (run.worktree.path === undefined) pushErr(errors, '$.worktree.path', 'missing');
|
|
221
|
+
else if (!isStr(run.worktree.path)) pushErr(errors, '$.worktree.path', 'expected string');
|
|
222
|
+
if (run.worktree.branch === undefined) pushErr(errors, '$.worktree.branch', 'missing');
|
|
223
|
+
else if (!isStr(run.worktree.branch)) pushErr(errors, '$.worktree.branch', 'expected string');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!isObj(run.artifacts)) {
|
|
227
|
+
pushErr(errors, '$.artifacts', 'expected object');
|
|
228
|
+
} else {
|
|
229
|
+
validateString(errors, '$.artifacts.planJson', run.artifacts.planJson);
|
|
230
|
+
validateString(errors, '$.artifacts.planMd', run.artifacts.planMd);
|
|
231
|
+
validateString(errors, '$.artifacts.patchesDir', run.artifacts.patchesDir);
|
|
232
|
+
validateString(errors, '$.artifacts.applyJson', run.artifacts.applyJson);
|
|
233
|
+
validateString(errors, '$.artifacts.verifyDir', run.artifacts.verifyDir);
|
|
234
|
+
validateString(errors, '$.artifacts.reportMd', run.artifacts.reportMd);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return errors;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function validateApplyResult(applyResult) {
|
|
241
|
+
const errors = [];
|
|
242
|
+
if (!isObj(applyResult)) {
|
|
243
|
+
pushErr(errors, '$', 'expected object');
|
|
244
|
+
return errors;
|
|
245
|
+
}
|
|
246
|
+
if (applyResult.version !== 1) pushErr(errors, '$.version', 'expected 1');
|
|
247
|
+
validateString(errors, '$.appliedAt', applyResult.appliedAt);
|
|
248
|
+
validateString(errors, '$.baseSha', applyResult.baseSha);
|
|
249
|
+
|
|
250
|
+
if (!Array.isArray(applyResult.tasks)) {
|
|
251
|
+
pushErr(errors, '$.tasks', 'expected array');
|
|
252
|
+
} else {
|
|
253
|
+
for (let i = 0; i < applyResult.tasks.length; i++) {
|
|
254
|
+
const t = applyResult.tasks[i];
|
|
255
|
+
const base = `$.tasks[${i}]`;
|
|
256
|
+
if (!isObj(t)) {
|
|
257
|
+
pushErr(errors, base, 'expected object');
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
validateString(errors, `${base}.id`, t.id);
|
|
261
|
+
validateString(errors, `${base}.patchPath`, t.patchPath);
|
|
262
|
+
if (!isBool(t.ok)) pushErr(errors, `${base}.ok`, 'expected boolean');
|
|
263
|
+
if (t.error !== undefined) validateString(errors, `${base}.error`, t.error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (applyResult.commit !== undefined) {
|
|
268
|
+
if (!isObj(applyResult.commit)) {
|
|
269
|
+
pushErr(errors, '$.commit', 'expected object');
|
|
270
|
+
} else {
|
|
271
|
+
validateString(errors, '$.commit.sha', applyResult.commit.sha);
|
|
272
|
+
validateString(errors, '$.commit.message', applyResult.commit.message);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return errors;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function validateVerifySummary(summary) {
|
|
280
|
+
const errors = [];
|
|
281
|
+
if (!isObj(summary)) {
|
|
282
|
+
pushErr(errors, '$', 'expected object');
|
|
283
|
+
return errors;
|
|
284
|
+
}
|
|
285
|
+
if (summary.version !== 1) pushErr(errors, '$.version', 'expected 1');
|
|
286
|
+
validateString(errors, '$.ranAt', summary.ranAt);
|
|
287
|
+
if (!isBool(summary.ok)) pushErr(errors, '$.ok', 'expected boolean');
|
|
288
|
+
|
|
289
|
+
if (!Array.isArray(summary.commands)) {
|
|
290
|
+
pushErr(errors, '$.commands', 'expected array');
|
|
291
|
+
} else {
|
|
292
|
+
for (let i = 0; i < summary.commands.length; i++) {
|
|
293
|
+
const c = summary.commands[i];
|
|
294
|
+
const base = `$.commands[${i}]`;
|
|
295
|
+
if (!isObj(c)) {
|
|
296
|
+
pushErr(errors, base, 'expected object');
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
validateString(errors, `${base}.name`, c.name);
|
|
300
|
+
validateString(errors, `${base}.command`, c.command);
|
|
301
|
+
if (!isBool(c.ok)) pushErr(errors, `${base}.ok`, 'expected boolean');
|
|
302
|
+
if (!isInt(c.exitCode)) pushErr(errors, `${base}.exitCode`, 'expected integer');
|
|
303
|
+
validateString(errors, `${base}.outputPath`, c.outputPath);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return errors;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function throwIfErrors(errors, label = 'validation') {
|
|
311
|
+
if (!errors.length) return;
|
|
312
|
+
const lines = errors.map(e => `- ${e.path}: ${e.message}`).join('\n');
|
|
313
|
+
throw new Error(`${label} failed:\n${lines}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = {
|
|
317
|
+
validateConfig,
|
|
318
|
+
validateLock,
|
|
319
|
+
validatePlan,
|
|
320
|
+
validateRun,
|
|
321
|
+
validateApplyResult,
|
|
322
|
+
validateVerifySummary,
|
|
323
|
+
throwIfErrors
|
|
324
|
+
};
|
|
325
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const { ensureDir } = require('../lib/utils');
|
|
6
|
+
const { writeJson, writeText } = require('./json');
|
|
7
|
+
const { validateVerifySummary, throwIfErrors } = require('./validate');
|
|
8
|
+
const { runKernel } = require('./kernel');
|
|
9
|
+
|
|
10
|
+
const { getPackageManager } = require('../lib/package-manager');
|
|
11
|
+
|
|
12
|
+
function nowIso() {
|
|
13
|
+
return new Date().toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function safeName(name) {
|
|
17
|
+
return String(name || 'command')
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
21
|
+
.replace(/^-+|-+$/g, '') || 'command';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function runCommand(command, { cwd }) {
|
|
25
|
+
const res = spawnSync(command, {
|
|
26
|
+
cwd,
|
|
27
|
+
shell: true,
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const stdout = res.stdout || '';
|
|
33
|
+
const stderr = res.stderr || '';
|
|
34
|
+
const status = typeof res.status === 'number' ? res.status : 1;
|
|
35
|
+
return { status, stdout, stderr };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectAutoCommands(worktreePath) {
|
|
39
|
+
const pkgPath = path.join(worktreePath, 'package.json');
|
|
40
|
+
if (!fs.existsSync(pkgPath)) return [];
|
|
41
|
+
|
|
42
|
+
let pkg;
|
|
43
|
+
try {
|
|
44
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
45
|
+
} catch (_err) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const scripts = (pkg && pkg.scripts && typeof pkg.scripts === 'object') ? pkg.scripts : {};
|
|
50
|
+
const wanted = ['lint', 'test', 'build'].filter(k => typeof scripts[k] === 'string' && scripts[k].trim());
|
|
51
|
+
if (!wanted.length) return [];
|
|
52
|
+
|
|
53
|
+
const pm = getPackageManager({ projectDir: worktreePath });
|
|
54
|
+
const runCmd = pm && pm.config && pm.config.runCmd ? pm.config.runCmd : 'npm run';
|
|
55
|
+
|
|
56
|
+
return wanted.map(name => ({ name, command: `${runCmd} ${name}` }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getVerifyCommands(verifyConfig, worktreePath) {
|
|
60
|
+
if (verifyConfig && verifyConfig.mode === 'manual') {
|
|
61
|
+
return Array.isArray(verifyConfig.commands) ? verifyConfig.commands : [];
|
|
62
|
+
}
|
|
63
|
+
return detectAutoCommands(worktreePath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runVerify({ worktreePath, verifyConfig, outDir }) {
|
|
67
|
+
ensureDir(outDir);
|
|
68
|
+
|
|
69
|
+
const commands = getVerifyCommands(verifyConfig || { mode: 'auto' }, worktreePath);
|
|
70
|
+
|
|
71
|
+
const kernelOut = runKernel('verify.run', {
|
|
72
|
+
worktreePath,
|
|
73
|
+
outDir,
|
|
74
|
+
commands: commands.map(c => ({ name: String(c.name), command: String(c.command) }))
|
|
75
|
+
});
|
|
76
|
+
if (kernelOut !== null) {
|
|
77
|
+
if (!kernelOut || kernelOut.version !== 1) {
|
|
78
|
+
throw new Error('ecc-kernel verify.run returned invalid output');
|
|
79
|
+
}
|
|
80
|
+
throwIfErrors(validateVerifySummary(kernelOut), 'verify summary');
|
|
81
|
+
const sumPath = path.join(outDir, 'summary.json');
|
|
82
|
+
if (!fs.existsSync(sumPath)) writeJson(sumPath, kernelOut);
|
|
83
|
+
return kernelOut;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const results = [];
|
|
87
|
+
|
|
88
|
+
let ok = true;
|
|
89
|
+
|
|
90
|
+
for (const c of commands) {
|
|
91
|
+
const name = safeName(c.name);
|
|
92
|
+
const cmd = String(c.command || '');
|
|
93
|
+
const outputPath = path.join(outDir, `${name}.txt`);
|
|
94
|
+
|
|
95
|
+
const { status, stdout, stderr } = runCommand(cmd, { cwd: worktreePath });
|
|
96
|
+
const entryOk = status === 0;
|
|
97
|
+
if (!entryOk) ok = false;
|
|
98
|
+
|
|
99
|
+
writeText(outputPath, stdout + (stderr ? (stdout.endsWith('\n') ? '' : '\n') + stderr : ''));
|
|
100
|
+
|
|
101
|
+
results.push({
|
|
102
|
+
name: String(c.name),
|
|
103
|
+
command: cmd,
|
|
104
|
+
ok: entryOk,
|
|
105
|
+
exitCode: status,
|
|
106
|
+
outputPath
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const summary = {
|
|
111
|
+
version: 1,
|
|
112
|
+
ranAt: nowIso(),
|
|
113
|
+
commands: results,
|
|
114
|
+
ok
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
throwIfErrors(validateVerifySummary(summary), 'verify summary');
|
|
118
|
+
writeJson(path.join(outDir, 'summary.json'), summary);
|
|
119
|
+
|
|
120
|
+
return summary;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
runVerify
|
|
125
|
+
};
|