@intentius/chant 0.60.0 → 0.62.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/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +0 -18
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/codegen/json-schema.d.ts +5 -2
- package/dist/codegen/json-schema.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +70 -2
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/lexicon.d.ts +51 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/assert-live.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +117 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/observation.d.ts +21 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/activities/lexicon-upgrade.d.ts +19 -1
- package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
- package/dist/op/activities/reconcile.d.ts +225 -14
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +17 -2
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +8 -0
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/components.ts +1 -1
- package/src/cli/handlers/lifecycle.ts +1 -0
- package/src/cli/handlers/operator.test.ts +102 -1
- package/src/cli/handlers/operator.ts +75 -5
- package/src/codegen/json-schema.test.ts +159 -0
- package/src/codegen/json-schema.ts +106 -8
- package/src/graph-ir.test.ts +63 -0
- package/src/graph-ir.ts +125 -8
- package/src/lexicon.ts +51 -0
- package/src/lifecycle/assert-live.ts +1 -0
- package/src/lifecycle/git.test.ts +49 -5
- package/src/lifecycle/git.ts +312 -11
- package/src/lifecycle/observe.ts +3 -0
- package/src/observation.test.ts +21 -9
- package/src/observation.ts +31 -2
- package/src/op/activities/lexicon-upgrade.test.ts +122 -39
- package/src/op/activities/lexicon-upgrade.ts +55 -9
- package/src/op/activities/reconcile.test.ts +527 -1
- package/src/op/activities/reconcile.ts +446 -23
- package/src/op/gate.test.ts +504 -0
- package/src/op/gate.ts +9 -1
- package/src/op/local-executor.test.ts +115 -0
- package/src/op/local-executor.ts +130 -21
- package/src/op/operator.test.ts +20 -0
- package/src/op/operator.ts +39 -1
- package/src/op/runtimes/local.ts +11 -0
- package/src/runtime-adapter.ts +17 -3
package/src/op/local-executor.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { resolveActivity, type ActivityFn, type ActivityProfile } from "./activi
|
|
|
25
25
|
import type { ReceiptReadResult } from "./receipt-store";
|
|
26
26
|
import { isStepOutputRef } from "./step-output-ref";
|
|
27
27
|
import { parseDuration } from "./duration";
|
|
28
|
-
import { evaluateGate, gitGateLedgerPort, type GateLedgerPort } from "./gate";
|
|
28
|
+
import { evaluateGate, gitGateLedgerPort, type GateCheck, type GateLedgerPort } from "./gate";
|
|
29
29
|
import { gateName } from "./gate-name";
|
|
30
30
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
31
31
|
import { appendRunRecord, buildRunRecord } from "../lifecycle/run-ledger";
|
|
@@ -87,11 +87,31 @@ export interface OpRunResult {
|
|
|
87
87
|
|
|
88
88
|
// ── Errors ────────────────────────────────────────────────────────────────��─
|
|
89
89
|
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Thrown on terminal Op failure; carries the partial run result for rendering.
|
|
92
|
+
*
|
|
93
|
+
* `cause` is what actually went wrong (#2301). Before it, this class was the
|
|
94
|
+
* end of the line for any error that was not a `PhaseFailure` — the executor
|
|
95
|
+
* caught it, built an `OpRunFailure` from the records it had, and never
|
|
96
|
+
* referenced the error again. Anything thrown outside a step's own
|
|
97
|
+
* error-capturing path therefore reached the operator as `Op "x" failed` and
|
|
98
|
+
* nothing else: no error line, no failing step, exit 1. That is how a gate
|
|
99
|
+
* whose ledger write died on a CI checkout with no `user.email` produced no
|
|
100
|
+
* text naming git, the ledger, or the identity, on INTENTIUS/choudoufu#1026.
|
|
101
|
+
*/
|
|
91
102
|
export class OpRunFailure extends Error {
|
|
92
|
-
|
|
103
|
+
/** The error the run actually died on, when it was not a step's own failure. */
|
|
104
|
+
public readonly cause?: unknown;
|
|
105
|
+
|
|
106
|
+
constructor(
|
|
107
|
+
public readonly result: OpRunResult,
|
|
108
|
+
options?: { cause?: unknown },
|
|
109
|
+
) {
|
|
93
110
|
super(`Op "${result.op}" failed`);
|
|
94
111
|
this.name = "OpRunFailure";
|
|
112
|
+
// Assigned rather than passed to `super` — the lib this package compiles
|
|
113
|
+
// against predates Error's `options` parameter.
|
|
114
|
+
if (options && "cause" in options) this.cause = options.cause;
|
|
95
115
|
}
|
|
96
116
|
}
|
|
97
117
|
|
|
@@ -118,6 +138,14 @@ class GateStop extends Error {
|
|
|
118
138
|
// ── Helpers ───────────────────────────────────────────────────────────────��─
|
|
119
139
|
|
|
120
140
|
const DEFAULT_PROFILE = "fastIdempotent";
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The phase a synthetic failure record is filed under when the error did not
|
|
144
|
+
* come from a step (#2301) — a ledger write, an abort, a malformed phase.
|
|
145
|
+
* Named rather than blank so a reader of `--json` or of the run ledger can
|
|
146
|
+
* tell it apart from an authored phase.
|
|
147
|
+
*/
|
|
148
|
+
const UNATTRIBUTED_PHASE = "run";
|
|
121
149
|
const FALLBACK_TIMEOUT_MS = 5 * 60_000;
|
|
122
150
|
|
|
123
151
|
const isActivity = (s: StepDefinition): s is ActivityStep => s.kind === "activity";
|
|
@@ -358,14 +386,36 @@ async function runGateStep(
|
|
|
358
386
|
gates: GateContext,
|
|
359
387
|
): Promise<{ record: StepRecord; pending?: PendingGateRecord }> {
|
|
360
388
|
const start = Date.now();
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
389
|
+
let check: GateCheck;
|
|
390
|
+
try {
|
|
391
|
+
check = await evaluateGate(gates.port, {
|
|
392
|
+
op: gates.op,
|
|
393
|
+
gate: gateName(step),
|
|
394
|
+
...(step.description ? { description: step.description } : {}),
|
|
395
|
+
...(step.timeout ? { timeout: step.timeout } : {}),
|
|
396
|
+
...(gates.runId ? { runId: gates.runId } : {}),
|
|
397
|
+
...(gates.now ? { now: gates.now } : {}),
|
|
398
|
+
});
|
|
399
|
+
} catch (err) {
|
|
400
|
+
// Deciding a gate reads and writes the `chant/lifecycle` branch, so it
|
|
401
|
+
// fails for all the ordinary git reasons — no committer identity, an
|
|
402
|
+
// unfetched ledger, a rejected ref update. `runStep` has always turned an
|
|
403
|
+
// activity's throw into a failing `StepRecord` carrying the message
|
|
404
|
+
// ("Never throws — returns a record"); `runGateStep` did not, which made
|
|
405
|
+
// the gate the one step kind whose failure produced no record and so no
|
|
406
|
+
// rendered output at all (#2301). It does now, and the phase treats it
|
|
407
|
+
// like any other failing step.
|
|
408
|
+
return {
|
|
409
|
+
record: {
|
|
410
|
+
phase: phaseName,
|
|
411
|
+
fn: gateFn(step),
|
|
412
|
+
args: {},
|
|
413
|
+
status: "fail",
|
|
414
|
+
durationMs: Date.now() - start,
|
|
415
|
+
error: errMessage(err),
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
369
419
|
|
|
370
420
|
if (check.satisfied) {
|
|
371
421
|
const { resolution } = check;
|
|
@@ -462,6 +512,12 @@ async function runEffectStep(
|
|
|
462
512
|
if (isGate(nested)) {
|
|
463
513
|
const { record, pending } = await runGateStep(nested, phaseName, gates);
|
|
464
514
|
pushRecord(records, gates, record);
|
|
515
|
+
if (record.status === "fail") {
|
|
516
|
+
// Receipt left untouched, as for any other failing nested step
|
|
517
|
+
// (#2301) — the effect is not applied and the next run re-proposes it.
|
|
518
|
+
skipRest(i + 1);
|
|
519
|
+
return { records, failed: true };
|
|
520
|
+
}
|
|
465
521
|
if (pending) {
|
|
466
522
|
// Receipt left untouched — the next run re-proposes the effect and
|
|
467
523
|
// re-evaluates the gate against whatever the ledger says by then.
|
|
@@ -523,6 +579,14 @@ async function runPhase(
|
|
|
523
579
|
for (const step of phase.steps.filter(isGate)) {
|
|
524
580
|
const { record, pending } = await runGateStep(step, phase.name, gates);
|
|
525
581
|
pushRecord(gateRecords, gates, record);
|
|
582
|
+
if (record.status === "fail") {
|
|
583
|
+
// The gate could not be decided at all (#2301). Same treatment as a
|
|
584
|
+
// pending one: nothing fans out, the activities are recorded skipped.
|
|
585
|
+
for (const skipped of phase.steps.filter(isActivity)) {
|
|
586
|
+
pushRecord(gateRecords, gates, skippedRecord(phase.name, skipped.fn, skipped.args));
|
|
587
|
+
}
|
|
588
|
+
throw new PhaseFailure(gateRecords);
|
|
589
|
+
}
|
|
526
590
|
if (pending) {
|
|
527
591
|
for (const skipped of phase.steps.filter(isActivity)) {
|
|
528
592
|
pushRecord(gateRecords, gates, skippedRecord(phase.name, skipped.fn, skipped.args));
|
|
@@ -560,6 +624,12 @@ async function runPhase(
|
|
|
560
624
|
if (isGate(step)) {
|
|
561
625
|
const { record, pending } = await runGateStep(step, phase.name, gates);
|
|
562
626
|
pushRecord(records, gates, record);
|
|
627
|
+
if (record.status === "fail") {
|
|
628
|
+
// A gate that could not be decided is a failed step, not a pending
|
|
629
|
+
// one (#2301) — the run failed, and the record says why.
|
|
630
|
+
skipRemaining(i + 1);
|
|
631
|
+
throw new PhaseFailure(records);
|
|
632
|
+
}
|
|
563
633
|
if (pending) {
|
|
564
634
|
skipRemaining(i + 1);
|
|
565
635
|
throw new GateStop(records, pending, phase.name);
|
|
@@ -747,7 +817,30 @@ export async function runOpLocally(
|
|
|
747
817
|
};
|
|
748
818
|
}
|
|
749
819
|
|
|
750
|
-
if (err instanceof PhaseFailure)
|
|
820
|
+
if (err instanceof PhaseFailure) {
|
|
821
|
+
records.push(...err.records);
|
|
822
|
+
} else {
|
|
823
|
+
// Everything else the phase loop can throw (#2301). `PhaseFailure` and
|
|
824
|
+
// `GateStop` are the two shapes that carry their own records; anything
|
|
825
|
+
// else — a git failure inside a ledger write, a bad phase definition, a
|
|
826
|
+
// bug — used to be dropped here without being read, leaving a result
|
|
827
|
+
// whose `records` hold nothing marked failed and nothing carrying a
|
|
828
|
+
// message. The renderer prints text only from records, so the operator
|
|
829
|
+
// got `Op "x" failed after 43.8s` and no cause at all.
|
|
830
|
+
//
|
|
831
|
+
// A synthetic record is what makes it visible: it renders as a failing
|
|
832
|
+
// step like any other, lands in the run ledger, and appears in
|
|
833
|
+
// `--json`. `cause` on the thrown `OpRunFailure` carries the original
|
|
834
|
+
// error for a caller that wants the object rather than the text.
|
|
835
|
+
records.push({
|
|
836
|
+
phase: UNATTRIBUTED_PHASE,
|
|
837
|
+
fn: "opFailure",
|
|
838
|
+
args: {},
|
|
839
|
+
status: "fail",
|
|
840
|
+
durationMs: 0,
|
|
841
|
+
error: errMessage(err),
|
|
842
|
+
});
|
|
843
|
+
}
|
|
751
844
|
|
|
752
845
|
// Compensation: run onFailure phases in reverse order (best-effort). Skipped
|
|
753
846
|
// on abort (Ctrl-C) — the user asked to stop, so don't start new work.
|
|
@@ -756,19 +849,35 @@ export async function runOpLocally(
|
|
|
756
849
|
try {
|
|
757
850
|
records.push(...(await runPhase(phase, activities, profiles, resultsById, gates, signal)));
|
|
758
851
|
} catch (compErr) {
|
|
759
|
-
if (compErr instanceof PhaseFailure || compErr instanceof GateStop)
|
|
852
|
+
if (compErr instanceof PhaseFailure || compErr instanceof GateStop) {
|
|
853
|
+
records.push(...compErr.records);
|
|
854
|
+
} else {
|
|
855
|
+
// The same swallow, one level down (#2301): a compensation phase
|
|
856
|
+
// that threw something else left no trace whatsoever.
|
|
857
|
+
records.push({
|
|
858
|
+
phase: phase.name,
|
|
859
|
+
fn: "onFailure",
|
|
860
|
+
args: {},
|
|
861
|
+
status: "fail",
|
|
862
|
+
durationMs: 0,
|
|
863
|
+
error: errMessage(compErr),
|
|
864
|
+
});
|
|
865
|
+
}
|
|
760
866
|
}
|
|
761
867
|
}
|
|
762
868
|
}
|
|
763
869
|
|
|
764
|
-
throw new OpRunFailure(
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
870
|
+
throw new OpRunFailure(
|
|
871
|
+
{
|
|
872
|
+
op: config.name,
|
|
873
|
+
records,
|
|
874
|
+
totalMs: Date.now() - start,
|
|
875
|
+
status: "fail",
|
|
876
|
+
startedAt,
|
|
877
|
+
record: await settle(records, "fail"),
|
|
878
|
+
},
|
|
879
|
+
{ cause: err },
|
|
880
|
+
);
|
|
772
881
|
}
|
|
773
882
|
|
|
774
883
|
return {
|
package/src/op/operator.test.ts
CHANGED
|
@@ -293,6 +293,26 @@ describe("runOperatorRound — lease + tick execution over a fixture ConvergeOp
|
|
|
293
293
|
});
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* #2309 review, finding 3. `chant operator` renders its own events rather
|
|
298
|
+
* than going through `renderHuman`, so fixing the executor's swallow (#2301)
|
|
299
|
+
* did nothing for this path: the message was the fixed string "see its
|
|
300
|
+
* ledger record for step-level detail", which sends the reader to an
|
|
301
|
+
* artifact that a failed ledger append means is not there.
|
|
302
|
+
*/
|
|
303
|
+
test("a failing tick names the failing step, not just the ledger record", async () => {
|
|
304
|
+
await withTestDir(async (dir) => {
|
|
305
|
+
await initRepo(dir);
|
|
306
|
+
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
307
|
+
const activities = fakeTickActivities(dir, "staging", "staging-converge", { throws: true });
|
|
308
|
+
|
|
309
|
+
const events = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES });
|
|
310
|
+
const failed = events[0] as Extract<OperatorTickEvent, { kind: "tick-failed" }>;
|
|
311
|
+
expect(failed.error).not.toMatch(/see its ledger record/);
|
|
312
|
+
expect(failed.error).toMatch(/^Op "staging-converge" failed: /);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
296
316
|
test("ticks every discovered ConvergeOp across environments in one round", async () => {
|
|
297
317
|
await withTestDir(async (dir) => {
|
|
298
318
|
await initRepo(dir);
|
package/src/op/operator.ts
CHANGED
|
@@ -180,13 +180,31 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
|
|
|
180
180
|
try {
|
|
181
181
|
const result = await runOpLocally(config, opts.activities, opts.profiles, opts.signal, {
|
|
182
182
|
ledger: { cwd: opts.cwd },
|
|
183
|
+
// #2301: without a sink, `settle` catches a failed ledger append and
|
|
184
|
+
// drops it. That is the failure this tick can least afford to lose —
|
|
185
|
+
// the message below points the reader at the ledger record, which is
|
|
186
|
+
// exactly the artifact a failed append means is not there.
|
|
187
|
+
onLedgerError: (err) =>
|
|
188
|
+
events.push({
|
|
189
|
+
kind: "tick-failed",
|
|
190
|
+
op: config.name,
|
|
191
|
+
env,
|
|
192
|
+
error:
|
|
193
|
+
`Op "${config.name}" ran, but its record could not be appended to the run ledger: ` +
|
|
194
|
+
`${err instanceof Error ? err.message : String(err)}`,
|
|
195
|
+
}),
|
|
183
196
|
});
|
|
184
197
|
const held = await stillHoldsLease(config.name, holder, lease.token, { cwd: opts.cwd });
|
|
185
198
|
events.push(held ? { kind: "ticked", op: config.name, env, result } : { kind: "fenced", op: config.name, env });
|
|
186
199
|
} catch (err) {
|
|
200
|
+
// #2301: "see its ledger record" was the whole message, and it sent the
|
|
201
|
+
// reader to an artifact that a failed ledger append means is missing —
|
|
202
|
+
// while `err.result` was carrying the failing steps all along. Name the
|
|
203
|
+
// failing steps here, and fall back to `cause` for a failure that
|
|
204
|
+
// produced no step record at all.
|
|
187
205
|
const message =
|
|
188
206
|
err instanceof OpRunFailure
|
|
189
|
-
?
|
|
207
|
+
? failureDetail(config.name, err)
|
|
190
208
|
: err instanceof Error
|
|
191
209
|
? err.message
|
|
192
210
|
: String(err);
|
|
@@ -197,6 +215,26 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
|
|
|
197
215
|
return events;
|
|
198
216
|
}
|
|
199
217
|
|
|
218
|
+
/**
|
|
219
|
+
* What a failed tick says about itself (#2301).
|
|
220
|
+
*
|
|
221
|
+
* `chant operator` renders its own events rather than going through
|
|
222
|
+
* `renderHuman`, so the executor's step records reached nobody here even after
|
|
223
|
+
* the executor stopped dropping them. Every failing step's message, in order,
|
|
224
|
+
* or the underlying error when the run failed without producing one.
|
|
225
|
+
*/
|
|
226
|
+
function failureDetail(op: string, err: OpRunFailure): string {
|
|
227
|
+
const failed = err.result.records.filter((r) => r.status === "fail" && r.error);
|
|
228
|
+
if (failed.length > 0) {
|
|
229
|
+
return `Op "${op}" failed: ${failed.map((r) => `${r.fn}: ${r.error}`).join("; ")}`;
|
|
230
|
+
}
|
|
231
|
+
const cause = err.cause;
|
|
232
|
+
if (cause !== undefined) {
|
|
233
|
+
return `Op "${op}" failed: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
234
|
+
}
|
|
235
|
+
return `Op "${op}" failed — see its ledger record for step-level detail`;
|
|
236
|
+
}
|
|
237
|
+
|
|
200
238
|
/** Abortable sleep — resolves early (without throwing) if `signal` fires mid-wait, so the operator loop can stop promptly on Ctrl-C rather than finishing out a long interval. */
|
|
201
239
|
function sleepAbortable(ms: number, signal?: AbortSignal): Promise<void> {
|
|
202
240
|
return new Promise((resolve) => {
|
package/src/op/runtimes/local.ts
CHANGED
|
@@ -153,6 +153,17 @@ export function createLocalOpRuntime(opts: { projectPath?: string } = {}): OpRun
|
|
|
153
153
|
// appends it here, at the one seam every local run passes
|
|
154
154
|
// through, rather than in the CLI handler above it.
|
|
155
155
|
ledger: { cwd: projectPath },
|
|
156
|
+
// #2301: the run-ledger append goes through the same
|
|
157
|
+
// `chant/lifecycle` write as the gate does, so it dies for the
|
|
158
|
+
// same reasons — and this runtime declared no sink for it, so
|
|
159
|
+
// the failure was caught by `settle` and dropped on the floor.
|
|
160
|
+
// A run whose outcome never reached the ledger says so now;
|
|
161
|
+
// `chant run status` and `chant run log` will not have it.
|
|
162
|
+
onLedgerError: (err) =>
|
|
163
|
+
process.stderr.write(
|
|
164
|
+
`warning: run "${runId}" of "${op.name}" finished, but its record could not be ` +
|
|
165
|
+
`appended to the run ledger: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
166
|
+
),
|
|
156
167
|
...(startOpts.progress ? { onRecord: startOpts.progress } : {}),
|
|
157
168
|
},
|
|
158
169
|
);
|
package/src/runtime-adapter.ts
CHANGED
|
@@ -40,7 +40,14 @@ export interface RuntimeAdapter {
|
|
|
40
40
|
* needs to round-trip content through a shell (`sh -c "echo … | cmd"`) just
|
|
41
41
|
* to feed a command that reads from stdin (e.g. `git hash-object --stdin`).
|
|
42
42
|
*/
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* `opts.env` merges over the parent environment rather than replacing it —
|
|
45
|
+
* a caller pinning `LC_ALL=C` to parse a command's output must not also
|
|
46
|
+
* have to reconstruct `PATH`, `HOME` and everything else the child needs.
|
|
47
|
+
* An entry whose value is `""` is passed through as empty, which is how a
|
|
48
|
+
* gettext override like `LANGUAGE` is neutralised.
|
|
49
|
+
*/
|
|
50
|
+
spawn(cmd: string[], opts?: { cwd?: string; stdin?: string; env?: Record<string, string> }): Promise<SpawnResult>;
|
|
44
51
|
/** Commands to use when spawning package manager / executor */
|
|
45
52
|
readonly commands: RuntimeCommands;
|
|
46
53
|
}
|
|
@@ -64,12 +71,19 @@ class NodeRuntimeAdapter implements RuntimeAdapter {
|
|
|
64
71
|
return picomatch(pattern)(filePath);
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
async spawn(
|
|
74
|
+
async spawn(
|
|
75
|
+
cmd: string[],
|
|
76
|
+
opts?: { cwd?: string; stdin?: string; env?: Record<string, string> },
|
|
77
|
+
): Promise<SpawnResult> {
|
|
68
78
|
return new Promise((resolve) => {
|
|
69
79
|
const child = execFile(
|
|
70
80
|
cmd[0],
|
|
71
81
|
cmd.slice(1),
|
|
72
|
-
{
|
|
82
|
+
{
|
|
83
|
+
cwd: opts?.cwd,
|
|
84
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
85
|
+
...(opts?.env ? { env: { ...process.env, ...opts.env } } : {}),
|
|
86
|
+
},
|
|
73
87
|
(err, stdout, stderr) => {
|
|
74
88
|
resolve({
|
|
75
89
|
stdout: stdout ?? "",
|