iterate-plugin 2.10.0 → 2.12.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 +42 -2
- package/README.zh-CN.md +40 -2
- package/dist/approval-gate.js +92 -0
- package/dist/config-loader.js +18 -3
- package/dist/config-write.js +7 -4
- package/dist/evidence.js +67 -1
- package/dist/git-scope.js +35 -6
- package/dist/index.js +15 -5
- package/dist/live.js +155 -0
- package/dist/meta-review.js +19 -5
- package/dist/method-scope.js +5 -1
- package/dist/paths.js +4 -0
- package/dist/review-scope.js +12 -8
- package/dist/review.js +76 -24
- package/dist/session-hooks.js +89 -0
- package/dist/skill-prompt.js +101 -19
- package/dist/tools/checkpoint.js +10 -3
- package/dist/tools/context.js +16 -4
- package/dist/tools/decision-log.js +29 -9
- package/dist/tools/fix.js +120 -3
- package/dist/tools/prune.js +16 -9
- package/dist/tools/review.js +4 -1
- package/dist/tools/transcript.js +324 -0
- package/dist/tools/triage.js +9 -6
- package/dist/tools/validate.js +5 -2
- package/dist/transcript.js +421 -0
- package/lib/client.js +966 -80
- package/lib/parse.js +302 -17
- package/package.json +1 -1
- package/src/approval-gate.ts +119 -0
- package/src/client/index.ts +807 -62
- package/src/config-loader.ts +16 -2
- package/src/config-write.ts +6 -4
- package/src/evidence.ts +69 -1
- package/src/git-scope.ts +34 -6
- package/src/index.ts +17 -6
- package/src/live.ts +185 -0
- package/src/meta-review.ts +24 -10
- package/src/method-scope.ts +5 -1
- package/src/paths.ts +5 -0
- package/src/review-scope.ts +11 -7
- package/src/review.ts +82 -25
- package/src/session-hooks.ts +90 -0
- package/src/skill-prompt.ts +101 -19
- package/src/tools/checkpoint.ts +10 -3
- package/src/tools/context.ts +14 -3
- package/src/tools/decision-log.ts +27 -10
- package/src/tools/fix.ts +114 -3
- package/src/tools/prune.ts +14 -11
- package/src/tools/review.ts +5 -2
- package/src/tools/transcript.ts +334 -0
- package/src/tools/triage.ts +9 -6
- package/src/tools/validate.ts +5 -2
- package/src/transcript.ts +475 -0
- package/src/types.ts +129 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/transcript.ts — `iterate_transcript` tool.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the runtime-observatory manifest to the model (and, via its persisted
|
|
5
|
+
* on-disk copy, to the client observatory panel). Purely local, deterministic,
|
|
6
|
+
* and safe:
|
|
7
|
+
*
|
|
8
|
+
* - `read` — return the persisted transcript manifest (or a structured
|
|
9
|
+
* "not found" empty view). Used each round by the workflow to
|
|
10
|
+
* pick up steering nudges, and polled by tool-reading agents.
|
|
11
|
+
* - `capture` — build a fresh transcript from the review `rounds` + `report`
|
|
12
|
+
* and persist it. Called by the canonical scripts after the
|
|
13
|
+
* final aggregate so the client always sees the latest run.
|
|
14
|
+
* - `nudge` — set (`text`) or clear (`text: null`) steering text persisted
|
|
15
|
+
* for the next round's reviewers to read.
|
|
16
|
+
*
|
|
17
|
+
* All writes are persisted to `.iterate/transcript.json` via an atomic
|
|
18
|
+
* tmp+rename so a crashed writer never leaves a corrupt manifest.
|
|
19
|
+
*/
|
|
20
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
21
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
22
|
+
import { existsSync } from 'node:fs';
|
|
23
|
+
import { dirname } from 'node:path';
|
|
24
|
+
import { loadEffectiveConfig, resolveProjectRootForExec, } from "../config-loader.js";
|
|
25
|
+
import { transcriptPath } from "../paths.js";
|
|
26
|
+
import { ReviewTranscriptBuilder } from "../transcript.js";
|
|
27
|
+
import { readLive } from "../live.js";
|
|
28
|
+
/** Build per-dimension threads for one round from its (dimension-tagged) findings. */
|
|
29
|
+
function captureRound(builder, round) {
|
|
30
|
+
if (!round || typeof round !== 'object')
|
|
31
|
+
return;
|
|
32
|
+
const r = round;
|
|
33
|
+
const roundNo = typeof r.round === 'number' ? Math.floor(r.round) : 0;
|
|
34
|
+
if (roundNo <= 0)
|
|
35
|
+
return;
|
|
36
|
+
builder.roundStart(roundNo);
|
|
37
|
+
const findings = Array.isArray(r.findings) ? r.findings : [];
|
|
38
|
+
const readFiles = Array.isArray(r.readFiles) ? r.readFiles : [];
|
|
39
|
+
// Group the round's findings by dimension → one reviewer thread each.
|
|
40
|
+
const byDim = new Map();
|
|
41
|
+
for (const f of findings) {
|
|
42
|
+
if (!f || typeof f !== 'object')
|
|
43
|
+
continue;
|
|
44
|
+
const rec = f;
|
|
45
|
+
const dim = typeof rec.dimension === 'string' && rec.dimension ? rec.dimension : 'review';
|
|
46
|
+
const list = byDim.get(dim) ?? [];
|
|
47
|
+
list.push(f);
|
|
48
|
+
byDim.set(dim, list);
|
|
49
|
+
}
|
|
50
|
+
if (byDim.size === 0) {
|
|
51
|
+
builder.reviewerSnapshot('review', [], readFiles);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
for (const [dim, list] of byDim)
|
|
55
|
+
builder.reviewerSnapshot(dim, list, readFiles);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Normalize the checkpoint shape if present. */
|
|
59
|
+
function normalizeCheckpoint(input) {
|
|
60
|
+
if (!input || typeof input !== 'object')
|
|
61
|
+
return null;
|
|
62
|
+
const c = input;
|
|
63
|
+
const round = typeof c.round === 'number' ? c.round : 0;
|
|
64
|
+
if (round <= 0)
|
|
65
|
+
return null;
|
|
66
|
+
return {
|
|
67
|
+
mode: c.mode === 'dry-run' || c.mode === 'normal' ? c.mode : 'normal',
|
|
68
|
+
round,
|
|
69
|
+
maxRounds: typeof c.maxRounds === 'number' ? c.maxRounds : 0,
|
|
70
|
+
fixedCount: typeof c.fixedCount === 'number' ? c.fixedCount : 0,
|
|
71
|
+
resumeCount: typeof c.resumeCount === 'number' ? c.resumeCount : 0,
|
|
72
|
+
updatedAt: typeof c.updatedAt === 'string' ? c.updatedAt : new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Normalize a fix record. */
|
|
76
|
+
function normalizeFix(input) {
|
|
77
|
+
if (!input || typeof input !== 'object')
|
|
78
|
+
return null;
|
|
79
|
+
const f = input;
|
|
80
|
+
const id = typeof f.id === 'string' ? f.id : '';
|
|
81
|
+
const file = typeof f.file === 'string' ? f.file : '';
|
|
82
|
+
if (!id || !file)
|
|
83
|
+
return null;
|
|
84
|
+
return {
|
|
85
|
+
id,
|
|
86
|
+
timestamp: typeof f.timestamp === 'string' ? f.timestamp : new Date().toISOString(),
|
|
87
|
+
round: typeof f.round === 'number' ? f.round : 0,
|
|
88
|
+
file,
|
|
89
|
+
summary: typeof f.summary === 'string' ? f.summary : '',
|
|
90
|
+
linesAdded: typeof f.linesAdded === 'number' ? f.linesAdded : 0,
|
|
91
|
+
linesRemoved: typeof f.linesRemoved === 'number' ? f.linesRemoved : 0,
|
|
92
|
+
success: f.success !== false,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Register the `iterate_transcript` tool. */
|
|
96
|
+
export function registerTranscriptTool(ctx) {
|
|
97
|
+
ctx.tools.register(defineTool({
|
|
98
|
+
name: 'iterate_transcript',
|
|
99
|
+
description: 'Runtime-observatory transcript for the iterate workflow. ' +
|
|
100
|
+
'`read` returns the current persisted transcript manifest (per-reviewer threads, ' +
|
|
101
|
+
'convergence series, findings, fixes, checkpoint, timeline, and any steering nudge ' +
|
|
102
|
+
'written for the next round). ' +
|
|
103
|
+
'`capture` builds a fresh transcript from the review `rounds` + `report` and persists it ' +
|
|
104
|
+
'(call once after the final aggregate so the UI reflects the run). ' +
|
|
105
|
+
'`nudge` sets (text) or clears (text:null) steering text the next round\'s reviewers read. ' +
|
|
106
|
+
'Purely local and deterministic — never touches source files.',
|
|
107
|
+
parameters: {
|
|
108
|
+
operation: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
required: true,
|
|
111
|
+
description: '"read" to fetch the manifest, "capture" to persist one, "nudge" to set steering text.',
|
|
112
|
+
enum: ['read', 'capture', 'nudge'],
|
|
113
|
+
},
|
|
114
|
+
rounds: {
|
|
115
|
+
type: 'json',
|
|
116
|
+
description: 'For `capture`: per-round findings, each [{round, findings:[{dimension,file,line?,severity,summary,…}], readFiles:[…]}].',
|
|
117
|
+
},
|
|
118
|
+
report: {
|
|
119
|
+
type: 'json',
|
|
120
|
+
description: 'For `capture`: the ReviewReport (convergence.findingsByRound used for the trend).',
|
|
121
|
+
},
|
|
122
|
+
mode: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'For `capture`: run mode ("dry-run" | "normal"). Default dry-run.',
|
|
125
|
+
enum: ['dry-run', 'normal'],
|
|
126
|
+
},
|
|
127
|
+
goal: { type: 'string', description: 'For `capture`: run goal.' },
|
|
128
|
+
maxRounds: { type: 'integer', description: 'For `capture`: round cap.' },
|
|
129
|
+
roundsExecuted: { type: 'integer', description: 'For `capture`: number of rounds actually executed.' },
|
|
130
|
+
findingsByRound: { type: 'json', description: 'For `capture`: the per-round new-findings count series (report.convergence.findingsByRound). Preferred over passing the whole report.' },
|
|
131
|
+
checkpoint: { type: 'json', description: 'For `capture`: checkpoint summary (optional).' },
|
|
132
|
+
fixes: {
|
|
133
|
+
type: 'json',
|
|
134
|
+
description: 'For `capture`: array of applied fixes [{id, file, round, summary, linesAdded, linesRemoved, success}].',
|
|
135
|
+
},
|
|
136
|
+
refReadFiles: { type: 'json', description: 'For `capture`: flat array of all read files across rounds (optional).' },
|
|
137
|
+
text: { type: 'string', description: 'For `nudge`: steering text to set (or null to clear).' },
|
|
138
|
+
path: { type: 'string', description: 'Project root directory (default: current working directory).' },
|
|
139
|
+
},
|
|
140
|
+
output: {
|
|
141
|
+
schema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
properties: {
|
|
145
|
+
operation: { type: 'string', required: true },
|
|
146
|
+
found: { type: 'boolean' },
|
|
147
|
+
transcript: { type: 'json' },
|
|
148
|
+
live: { type: 'json', description: 'Recent live reviewer-activity entries (newest first).' },
|
|
149
|
+
updated: { type: 'boolean' },
|
|
150
|
+
error: { type: 'string' },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
154
|
+
},
|
|
155
|
+
async execute(args, exec) {
|
|
156
|
+
const resolved = resolveProjectRootForExec(exec, args.path);
|
|
157
|
+
if (!resolved.ok)
|
|
158
|
+
return { operation: args.operation, error: resolved.reason };
|
|
159
|
+
const projectRoot = resolved.root;
|
|
160
|
+
const file = transcriptPath(projectRoot);
|
|
161
|
+
const { config } = loadEffectiveConfig(projectRoot);
|
|
162
|
+
const approval = config.observatory?.approval ?? 'ask';
|
|
163
|
+
if (args.operation === 'read') {
|
|
164
|
+
const live = await readLive(projectRoot);
|
|
165
|
+
if (!existsSync(file)) {
|
|
166
|
+
return {
|
|
167
|
+
operation: 'read',
|
|
168
|
+
found: false,
|
|
169
|
+
live: live,
|
|
170
|
+
transcript: new ReviewTranscriptBuilder({
|
|
171
|
+
project: projectRoot,
|
|
172
|
+
approval,
|
|
173
|
+
}).serialize(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const raw = await readFile(file, 'utf-8');
|
|
178
|
+
const parsed = JSON.parse(raw);
|
|
179
|
+
return {
|
|
180
|
+
operation: 'read',
|
|
181
|
+
found: true,
|
|
182
|
+
live: live,
|
|
183
|
+
transcript: parsed,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
return {
|
|
188
|
+
operation: 'read',
|
|
189
|
+
found: false,
|
|
190
|
+
error: `Failed to read transcript: ${err instanceof Error ? err.message : String(err)}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (args.operation === 'nudge') {
|
|
195
|
+
let manifest = null;
|
|
196
|
+
if (existsSync(file)) {
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(await readFile(file, 'utf-8'));
|
|
199
|
+
manifest = parsed;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
manifest = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const builder = manifest
|
|
206
|
+
? rehydrateBuilder(manifest, approval)
|
|
207
|
+
: new ReviewTranscriptBuilder({ project: projectRoot, mode: 'normal', approval });
|
|
208
|
+
builder.setNudge(typeof args.text === 'string' && args.text.trim() ? args.text : null);
|
|
209
|
+
await persist(file, builder.serialize());
|
|
210
|
+
return {
|
|
211
|
+
operation: 'nudge',
|
|
212
|
+
updated: true,
|
|
213
|
+
transcript: builder.serialize(),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// capture
|
|
217
|
+
const mode = args.mode === 'normal' ? 'normal' : 'dry-run';
|
|
218
|
+
const goal = typeof args.goal === 'string' ? args.goal : '';
|
|
219
|
+
const maxRounds = typeof args.maxRounds === 'number' ? Math.floor(args.maxRounds) : 0;
|
|
220
|
+
const builder = new ReviewTranscriptBuilder({ project: projectRoot, mode, approval, goal, maxRounds });
|
|
221
|
+
const report = args.report;
|
|
222
|
+
const reportFindings = report && typeof report === 'object' && Array.isArray(report.findings)
|
|
223
|
+
? report.findings
|
|
224
|
+
: [];
|
|
225
|
+
const convergence = Array.isArray(args.findingsByRound) ? args.findingsByRound
|
|
226
|
+
: report && typeof report === 'object' && report.convergence
|
|
227
|
+
? report.convergence.findingsByRound ?? []
|
|
228
|
+
: [];
|
|
229
|
+
const rounds = Array.isArray(args.rounds) ? args.rounds : [];
|
|
230
|
+
for (const r of rounds)
|
|
231
|
+
captureRound(builder, r);
|
|
232
|
+
if (rounds.length === 0) {
|
|
233
|
+
// No pre-grouped rounds: fall back to the report's flattened findings.
|
|
234
|
+
const readFiles = Array.isArray(args.refReadFiles) ? args.refReadFiles : [];
|
|
235
|
+
const byDim = new Map();
|
|
236
|
+
for (const f of reportFindings) {
|
|
237
|
+
if (!f || typeof f !== 'object')
|
|
238
|
+
continue;
|
|
239
|
+
const rec = f;
|
|
240
|
+
const dim = typeof rec.dimension === 'string' && rec.dimension ? rec.dimension : 'review';
|
|
241
|
+
const list = byDim.get(dim) ?? [];
|
|
242
|
+
list.push(f);
|
|
243
|
+
byDim.set(dim, list);
|
|
244
|
+
}
|
|
245
|
+
for (const [dim, list] of byDim)
|
|
246
|
+
builder.reviewerSnapshot(dim, list, readFiles);
|
|
247
|
+
}
|
|
248
|
+
// Convergence series from the report (position per round).
|
|
249
|
+
for (let i = 0; i < convergence.length; i += 1) {
|
|
250
|
+
const n = convergence[i];
|
|
251
|
+
if (typeof n === 'number')
|
|
252
|
+
builder.snapshotConvergence(i + 1, n);
|
|
253
|
+
}
|
|
254
|
+
const roundsExecuted = typeof args.roundsExecuted === 'number' ? Math.floor(args.roundsExecuted) : rounds.length;
|
|
255
|
+
if (roundsExecuted > 0)
|
|
256
|
+
builder.roundStart(roundsExecuted, maxRounds);
|
|
257
|
+
builder.recordCheckpoint(normalizeCheckpoint(args.checkpoint));
|
|
258
|
+
if (Array.isArray(args.fixes)) {
|
|
259
|
+
for (const fx of args.fixes) {
|
|
260
|
+
const record = normalizeFix(fx);
|
|
261
|
+
if (record)
|
|
262
|
+
builder.fix(record);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Convergence "found nothing → settled" marker when the trend ends on 0.
|
|
266
|
+
const last = convergence[convergence.length - 1];
|
|
267
|
+
if (convergence.length > 0 && last === 0)
|
|
268
|
+
builder.finish();
|
|
269
|
+
await persist(file, builder.serialize());
|
|
270
|
+
const live = await readLive(projectRoot);
|
|
271
|
+
return {
|
|
272
|
+
operation: 'capture',
|
|
273
|
+
found: true,
|
|
274
|
+
updated: true,
|
|
275
|
+
live: live,
|
|
276
|
+
transcript: builder.serialize(),
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
/** Rebuild a builder from a persisted manifest so nudge edits preserve history. */
|
|
282
|
+
function rehydrateBuilder(manifest, approval) {
|
|
283
|
+
const builder = new ReviewTranscriptBuilder({
|
|
284
|
+
project: manifest.project,
|
|
285
|
+
mode: manifest.mode ?? null,
|
|
286
|
+
approval,
|
|
287
|
+
goal: manifest.goal,
|
|
288
|
+
maxRounds: manifest.maxRounds,
|
|
289
|
+
});
|
|
290
|
+
for (const r of Array.isArray(manifest.rounds) ? manifest.rounds : []) {
|
|
291
|
+
builder.roundStart(r.round, manifest.maxRounds);
|
|
292
|
+
for (const t of Array.isArray(r.threads) ? r.threads : []) {
|
|
293
|
+
builder.reviewerStart(t.dimension || 'review', t.attempt || 1);
|
|
294
|
+
builder.reviewerMessage((t.messages ?? []).join('\n'));
|
|
295
|
+
builder.reviewerRead(t.readFiles ?? []);
|
|
296
|
+
for (const f of t.findings ?? [])
|
|
297
|
+
builder.reviewerFindings([f]);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (let idx = 0; idx < (manifest.convergence ?? []).length; idx += 1) {
|
|
301
|
+
const n = manifest.convergence[idx];
|
|
302
|
+
if (typeof n === 'number' && n >= 0)
|
|
303
|
+
builder.snapshotConvergence(idx + 1, n);
|
|
304
|
+
}
|
|
305
|
+
if (manifest.checkpoint)
|
|
306
|
+
builder.recordCheckpoint(manifest.checkpoint);
|
|
307
|
+
if (Array.isArray(manifest.fixes))
|
|
308
|
+
for (const fx of manifest.fixes)
|
|
309
|
+
builder.fix(fx);
|
|
310
|
+
if (Array.isArray(manifest.timeline))
|
|
311
|
+
for (const e of manifest.timeline)
|
|
312
|
+
builder.decision(e);
|
|
313
|
+
builder.setNudge(manifest.nudge?.text ?? null);
|
|
314
|
+
if (!manifest.active)
|
|
315
|
+
builder.finish();
|
|
316
|
+
return builder;
|
|
317
|
+
}
|
|
318
|
+
/** Atomically persist a manifest (tmp + rename) under `.iterate/`. */
|
|
319
|
+
async function persist(file, manifest) {
|
|
320
|
+
await mkdir(dirname(file), { recursive: true });
|
|
321
|
+
const tmp = `${file}.tmp`;
|
|
322
|
+
await writeFile(tmp, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
323
|
+
await rename(tmp, file);
|
|
324
|
+
}
|
package/dist/tools/triage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { copyFileSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
4
4
|
import yaml from 'js-yaml';
|
|
@@ -194,19 +194,22 @@ function applyEntries(projectRoot, incoming) {
|
|
|
194
194
|
writeFileSync(configPath, yamlText, 'utf-8');
|
|
195
195
|
}
|
|
196
196
|
catch (err) {
|
|
197
|
-
// Rollback: restore the backup
|
|
197
|
+
// Rollback: restore the backup, or REMOVE the file we just created when
|
|
198
|
+
// there was no prior config — an empty file left behind would poison all
|
|
199
|
+
// future config reads (empty YAML is not a valid mapping).
|
|
200
|
+
let rollbackError = '';
|
|
198
201
|
try {
|
|
199
202
|
if (backupPath)
|
|
200
203
|
copyFileSync(backupPath, configPath);
|
|
201
204
|
else if (existsSync(configPath))
|
|
202
|
-
|
|
205
|
+
rmSync(configPath, { force: true });
|
|
203
206
|
}
|
|
204
|
-
catch {
|
|
205
|
-
|
|
207
|
+
catch (rbErr) {
|
|
208
|
+
rollbackError = `; rollback also failed: ${String(rbErr)}`;
|
|
206
209
|
}
|
|
207
210
|
return {
|
|
208
211
|
ok: false,
|
|
209
|
-
error: `Failed to write config: ${String(err)}`,
|
|
212
|
+
error: `Failed to write config: ${String(err)}${rollbackError}`,
|
|
210
213
|
};
|
|
211
214
|
}
|
|
212
215
|
return { ok: true, added, skipped, count: merged.length, configPath, backupPath };
|
package/dist/tools/validate.js
CHANGED
|
@@ -30,10 +30,13 @@ async function runCommand(command, cwd, timeoutMs) {
|
|
|
30
30
|
env: { ...process.env, PAGER: 'cat' },
|
|
31
31
|
}, (error, stdout, stderr) => {
|
|
32
32
|
const durationMs = Math.round(performance.now() - start);
|
|
33
|
-
// error.code is the exit code when the command ran;
|
|
33
|
+
// error.code is the exit code when the command ran; when the binary
|
|
34
|
+
// cannot be spawned Node sets error.code to a STRING ('ENOENT' etc).
|
|
35
|
+
// Coerce to a number so the integer output schema is never violated.
|
|
36
|
+
const exitCode = typeof error?.code === 'number' ? error.code : (error ? 1 : 0);
|
|
34
37
|
resolve({
|
|
35
38
|
command,
|
|
36
|
-
exitCode
|
|
39
|
+
exitCode,
|
|
37
40
|
stdout: stdout ?? '',
|
|
38
41
|
stderr: stderr ?? '',
|
|
39
42
|
timedOut: error?.killed === true,
|