@duyquangnvx/create-agent-cli 0.30.0 → 0.30.1

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/bin.js +198 -184
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ What changed in each release of `@duyquangnvx/agent-cli` (the Runtime) and `@duy
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## v0.30.1 (2026-11-09)
8
+
9
+ ### Fixed
10
+
11
+ - `ctx.spawn` kills a child that ignores SIGTERM with SIGKILL 2 s after stopping it, so it no longer outlives the CLI and the Command's cleanup runs within the grace.
12
+ - Nothing of a run reaches stdout or stderr after its output, so the error a caller reads last stays last: a Side task's own progress, and progress from a Command still running past its grace, such as `finishing <name>` from a `runToEnd` it starts late, were printed after the output while the run waited for its Side task. A Side task's failure under `<NAME>_DEBUG=1` still prints after it.
13
+
7
14
  ## v0.30.0 (2026-11-09)
8
15
 
9
16
  ### Added
package/dist/bin.js CHANGED
@@ -5740,6 +5740,40 @@ function untilAborted(signal) {
5740
5740
  });
5741
5741
  }
5742
5742
  //#endregion
5743
+ //#region ../agent-cli/dist/clock.js
5744
+ const realClock = {
5745
+ now: () => Date.now(),
5746
+ sleep: (ms, signal) => new Promise((resolve, reject) => {
5747
+ if (signal?.aborted === true) {
5748
+ reject(signal.reason);
5749
+ return;
5750
+ }
5751
+ const onAbort = () => {
5752
+ clearTimeout(timer);
5753
+ reject(signal?.reason);
5754
+ };
5755
+ const timer = setTimeout(() => {
5756
+ signal?.removeEventListener("abort", onAbort);
5757
+ resolve();
5758
+ }, ms);
5759
+ signal?.addEventListener("abort", onAbort, { once: true });
5760
+ })
5761
+ };
5762
+ /** Runs `then` once `ms` have passed on `clock`; aborting `cancel` drops it. */
5763
+ function after(clock, ms, cancel, then) {
5764
+ clock.sleep(ms, cancel).then(then, (error) => {
5765
+ if (!cancel.aborted) throw error;
5766
+ });
5767
+ }
5768
+ async function poll(clock, check, intervalMs, signal) {
5769
+ for (;;) {
5770
+ signal.throwIfAborted();
5771
+ const value = await check();
5772
+ if (value !== void 0) return value;
5773
+ await clock.sleep(intervalMs, signal);
5774
+ }
5775
+ }
5776
+ //#endregion
5743
5777
  //#region ../agent-cli/dist/command-line/tokenize.js
5744
5778
  const NEGATIVE_NUMBER = /^-(\d+|\d*\.\d+)(e[+-]?\d+)?$/;
5745
5779
  function isOptionLike(word) {
@@ -6064,6 +6098,149 @@ function lastLines(output, count) {
6064
6098
  return output.trim().split("\n").slice(-count).join("\n");
6065
6099
  }
6066
6100
  //#endregion
6101
+ //#region ../agent-cli/dist/context/interrupts.js
6102
+ const INTERRUPTED = "INTERRUPTED";
6103
+ const DEADLINE_EXCEEDED = "DEADLINE_EXCEEDED";
6104
+ /** How long an interrupted Command may take to settle before the run ends without it. */
6105
+ const GRACE_MS = 3e3;
6106
+ const GRACE_OVER = Symbol("grace over");
6107
+ function interruptedError(rerun) {
6108
+ return new CliError({
6109
+ code: INTERRUPTED,
6110
+ message: "interrupted before it finished",
6111
+ hint: "to finish it, run the Next step, unless it was stopped on purpose; if a tool timeout stopped it, give it longer",
6112
+ next: [rerun]
6113
+ });
6114
+ }
6115
+ const BOUND_WORDS = {
6116
+ deadline: "deadline",
6117
+ ignoreStop: "timeout"
6118
+ };
6119
+ function waitHint(bound) {
6120
+ return bound === "ignoreStop" ? "the call may have been billed: ask your user before running the Next step" : "run the Next step later, when the service it waits on may answer faster";
6121
+ }
6122
+ function deadlineError(commandLine, ms, advice, bound = "deadline") {
6123
+ return new CliError({
6124
+ code: DEADLINE_EXCEEDED,
6125
+ message: `${commandLine} ran past its ${ms} ms ${BOUND_WORDS[bound]}`,
6126
+ ...advice
6127
+ });
6128
+ }
6129
+ /** One Command run's cancellation: its signal, its deadlines and the steps it must finish. */
6130
+ var RunControl = class {
6131
+ signal;
6132
+ #controller = new AbortController();
6133
+ #commandLine;
6134
+ #rerun;
6135
+ #outer;
6136
+ #onOuterAbort = () => this.#controller.abort(interruptedError(this.#rerun));
6137
+ /** Each running must-finish step, tracked until it settles, with its name. */
6138
+ #steps = /* @__PURE__ */ new Map();
6139
+ #progress;
6140
+ #clock;
6141
+ #disposed = new AbortController();
6142
+ /** The reasons this run's deadlines aborted with, to tell them in a Command's error. */
6143
+ #abortReasons = /* @__PURE__ */ new WeakSet();
6144
+ #graceCancel = new AbortController();
6145
+ #endGrace = () => {};
6146
+ #graceOver = new Promise((resolve) => {
6147
+ this.#endGrace = resolve;
6148
+ });
6149
+ constructor({ commandLine, rerun, outer, clock, progress, runDeadline }) {
6150
+ this.#commandLine = commandLine;
6151
+ this.#rerun = rerun;
6152
+ this.#outer = outer;
6153
+ this.#clock = clock;
6154
+ this.#progress = progress;
6155
+ this.signal = this.#controller.signal;
6156
+ this.signal.addEventListener("abort", () => this.#onStop(), { once: true });
6157
+ if (outer?.aborted === true) this.#onOuterAbort();
6158
+ else outer?.addEventListener("abort", this.#onOuterAbort, { once: true });
6159
+ if (runDeadline !== void 0) {
6160
+ const { ms, advice } = runDeadline;
6161
+ after(clock, ms, this.#disposed.signal, () => this.#controller.abort(deadlineError(commandLine, ms, advice())));
6162
+ }
6163
+ }
6164
+ deadline(ms, options = {}) {
6165
+ return options.ignoreStop === true ? this.#abortsAfter(ms, "ignoreStop") : AbortSignal.any([this.signal, this.#abortsAfter(ms, "deadline")]);
6166
+ }
6167
+ #abortsAfter(ms, bound) {
6168
+ const controller = new AbortController();
6169
+ after(this.#clock, ms, this.#disposed.signal, () => {
6170
+ const advice = {
6171
+ hint: waitHint(bound),
6172
+ next: [this.#rerun]
6173
+ };
6174
+ const reason = deadlineError(this.#commandLine, ms, advice, bound);
6175
+ this.#abortReasons.add(reason);
6176
+ controller.abort(reason);
6177
+ });
6178
+ return controller.signal;
6179
+ }
6180
+ async runToEnd(name, work) {
6181
+ this.#graceCancel.abort();
6182
+ if (this.signal.aborted) this.#finishing(name);
6183
+ const step = work();
6184
+ const settled = step.then(() => {}, () => {});
6185
+ this.#steps.set(settled, name);
6186
+ try {
6187
+ return await step;
6188
+ } finally {
6189
+ this.#steps.delete(settled);
6190
+ this.#startGrace();
6191
+ }
6192
+ }
6193
+ /** Waits for the Command, its must-finish steps, and no longer than the grace once interrupted. */
6194
+ async settle(work) {
6195
+ try {
6196
+ const value = await Promise.race([work, this.#graceOver]);
6197
+ if (this.signal.aborted || value === GRACE_OVER) throw this.signal.reason;
6198
+ return value;
6199
+ } finally {
6200
+ await this.#stepsDone();
6201
+ this.close();
6202
+ }
6203
+ }
6204
+ /**
6205
+ * The error a run ends with: an interrupt or the run deadline outranks what the Command threw,
6206
+ * unless the Command threw its own `INTERRUPTED`, saying what it finished before it stopped.
6207
+ * Otherwise each leaf that is not a `CliError` and whose cause chain holds a deadline's abort is
6208
+ * reported as that abort, so a declared error keeps its own code.
6209
+ */
6210
+ errorAfter(error) {
6211
+ if (this.signal.aborted) return error instanceof CliError && error.code === "INTERRUPTED" ? error : this.signal.reason;
6212
+ const leaves = errorsOf(error).map((leaf) => this.#abortIn(leaf) ?? leaf);
6213
+ return leaves.length === 1 ? leaves[0] : new AggregateError(leaves);
6214
+ }
6215
+ #abortIn(error) {
6216
+ if (error instanceof CliError) return void 0;
6217
+ return causesOf(error).find((each) => each instanceof CliError && this.#abortReasons.has(each));
6218
+ }
6219
+ #onStop() {
6220
+ for (const name of this.#steps.values()) this.#finishing(name);
6221
+ this.#startGrace();
6222
+ }
6223
+ #finishing(name) {
6224
+ this.#progress(`finishing ${name}`);
6225
+ }
6226
+ #startGrace() {
6227
+ if (this.signal.aborted && this.#steps.size === 0) {
6228
+ this.#graceCancel.abort();
6229
+ this.#graceCancel = new AbortController();
6230
+ after(this.#clock, GRACE_MS, this.#graceCancel.signal, () => this.#endGrace(GRACE_OVER));
6231
+ }
6232
+ }
6233
+ async #stepsDone() {
6234
+ while (this.#steps.size > 0) await Promise.all(this.#steps.keys());
6235
+ }
6236
+ /** Stops the run's timers and its hold on the caller's signal, for a run that never settles. */
6237
+ close() {
6238
+ this.#graceCancel.abort();
6239
+ this.#disposed.abort();
6240
+ this.#outer?.removeEventListener("abort", this.#onOuterAbort);
6241
+ }
6242
+ };
6243
+ //#endregion
6067
6244
  //#region ../agent-cli/dist/define.js
6068
6245
  function isCommand(node) {
6069
6246
  return typeof node.effect === "string";
@@ -6146,6 +6323,7 @@ function toolRows(tools, commands, platform) {
6146
6323
  const TOOL_FAILED = "TOOL_FAILED";
6147
6324
  const STDERR_TAIL_LINES$1 = 20;
6148
6325
  const SIGNAL_EXIT_BASE = 128;
6326
+ const KILL_AFTER_MS = 2e3;
6149
6327
  const BATCH_FILE = /\.(?:cmd|bat)$/i;
6150
6328
  const CMD_SHIM = /\.cmd$/i;
6151
6329
  const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
@@ -6182,9 +6360,10 @@ async function windowsLaunch(command, args, env) {
6182
6360
  shell: true
6183
6361
  };
6184
6362
  }
6185
- function stop(child) {
6363
+ function stop(child, exited) {
6186
6364
  if (process.platform !== "win32" || child.pid === void 0) {
6187
6365
  child.kill();
6366
+ after(realClock, KILL_AFTER_MS, exited, () => child.kill("SIGKILL"));
6188
6367
  return Promise.resolve();
6189
6368
  }
6190
6369
  const pid = String(child.pid);
@@ -6280,8 +6459,9 @@ async function spawnChild(command, args, { cwd, env, signal, onLine, stdin, deta
6280
6459
  detached: detached && process.platform !== "win32"
6281
6460
  });
6282
6461
  let stopped = Promise.resolve();
6462
+ const exited = new AbortController();
6283
6463
  const kill = () => {
6284
- stopped = stop(child);
6464
+ stopped = stop(child, exited.signal);
6285
6465
  };
6286
6466
  signal.addEventListener("abort", kill, { once: true });
6287
6467
  let callerFailure;
@@ -6304,7 +6484,9 @@ async function spawnChild(command, args, { cwd, env, signal, onLine, stdin, deta
6304
6484
  stderrLines.push(chunk);
6305
6485
  });
6306
6486
  if (stdin !== void 0 && child.stdin !== null) feed(child.stdin, stdin, signal).catch(fail);
6487
+ child.once("exit", () => exited.abort());
6307
6488
  child.on("error", (error) => {
6489
+ exited.abort();
6308
6490
  signal.removeEventListener("abort", kill);
6309
6491
  reject(error);
6310
6492
  });
@@ -6453,40 +6635,6 @@ function contextSpawnDetached(run, defaults, scope) {
6453
6635
  };
6454
6636
  }
6455
6637
  //#endregion
6456
- //#region ../agent-cli/dist/clock.js
6457
- const realClock = {
6458
- now: () => Date.now(),
6459
- sleep: (ms, signal) => new Promise((resolve, reject) => {
6460
- if (signal?.aborted === true) {
6461
- reject(signal.reason);
6462
- return;
6463
- }
6464
- const onAbort = () => {
6465
- clearTimeout(timer);
6466
- reject(signal?.reason);
6467
- };
6468
- const timer = setTimeout(() => {
6469
- signal?.removeEventListener("abort", onAbort);
6470
- resolve();
6471
- }, ms);
6472
- signal?.addEventListener("abort", onAbort, { once: true });
6473
- })
6474
- };
6475
- /** Runs `then` once `ms` have passed on `clock`; aborting `cancel` drops it. */
6476
- function after(clock, ms, cancel, then) {
6477
- clock.sleep(ms, cancel).then(then, (error) => {
6478
- if (!cancel.aborted) throw error;
6479
- });
6480
- }
6481
- async function poll(clock, check, intervalMs, signal) {
6482
- for (;;) {
6483
- signal.throwIfAborted();
6484
- const value = await check();
6485
- if (value !== void 0) return value;
6486
- await clock.sleep(intervalMs, signal);
6487
- }
6488
- }
6489
- //#endregion
6490
6638
  //#region ../agent-cli/dist/context/stdin.js
6491
6639
  const STDIN_META = "stdin";
6492
6640
  function isStdinSchema(schema) {
@@ -7067,149 +7215,6 @@ function classifyErrno(error, context) {
7067
7215
  }
7068
7216
  }
7069
7217
  //#endregion
7070
- //#region ../agent-cli/dist/context/interrupts.js
7071
- const INTERRUPTED = "INTERRUPTED";
7072
- const DEADLINE_EXCEEDED = "DEADLINE_EXCEEDED";
7073
- /** How long an interrupted Command may take to settle before the run ends without it. */
7074
- const GRACE_MS = 3e3;
7075
- const GRACE_OVER = Symbol("grace over");
7076
- function interruptedError(rerun) {
7077
- return new CliError({
7078
- code: INTERRUPTED,
7079
- message: "interrupted before it finished",
7080
- hint: "to finish it, run the Next step, unless it was stopped on purpose; if a tool timeout stopped it, give it longer",
7081
- next: [rerun]
7082
- });
7083
- }
7084
- const BOUND_WORDS = {
7085
- deadline: "deadline",
7086
- ignoreStop: "timeout"
7087
- };
7088
- function waitHint(bound) {
7089
- return bound === "ignoreStop" ? "the call may have been billed: ask your user before running the Next step" : "run the Next step later, when the service it waits on may answer faster";
7090
- }
7091
- function deadlineError(commandLine, ms, advice, bound = "deadline") {
7092
- return new CliError({
7093
- code: DEADLINE_EXCEEDED,
7094
- message: `${commandLine} ran past its ${ms} ms ${BOUND_WORDS[bound]}`,
7095
- ...advice
7096
- });
7097
- }
7098
- /** One Command run's cancellation: its signal, its deadlines and the steps it must finish. */
7099
- var RunControl = class {
7100
- signal;
7101
- #controller = new AbortController();
7102
- #commandLine;
7103
- #rerun;
7104
- #outer;
7105
- #onOuterAbort = () => this.#controller.abort(interruptedError(this.#rerun));
7106
- /** Each running must-finish step, tracked until it settles, with its name. */
7107
- #steps = /* @__PURE__ */ new Map();
7108
- #progress;
7109
- #clock;
7110
- #disposed = new AbortController();
7111
- /** The reasons this run's deadlines aborted with, to tell them in a Command's error. */
7112
- #abortReasons = /* @__PURE__ */ new WeakSet();
7113
- #graceCancel = new AbortController();
7114
- #endGrace = () => {};
7115
- #graceOver = new Promise((resolve) => {
7116
- this.#endGrace = resolve;
7117
- });
7118
- constructor({ commandLine, rerun, outer, clock, progress, runDeadline }) {
7119
- this.#commandLine = commandLine;
7120
- this.#rerun = rerun;
7121
- this.#outer = outer;
7122
- this.#clock = clock;
7123
- this.#progress = progress;
7124
- this.signal = this.#controller.signal;
7125
- this.signal.addEventListener("abort", () => this.#onStop(), { once: true });
7126
- if (outer?.aborted === true) this.#onOuterAbort();
7127
- else outer?.addEventListener("abort", this.#onOuterAbort, { once: true });
7128
- if (runDeadline !== void 0) {
7129
- const { ms, advice } = runDeadline;
7130
- after(clock, ms, this.#disposed.signal, () => this.#controller.abort(deadlineError(commandLine, ms, advice())));
7131
- }
7132
- }
7133
- deadline(ms, options = {}) {
7134
- return options.ignoreStop === true ? this.#abortsAfter(ms, "ignoreStop") : AbortSignal.any([this.signal, this.#abortsAfter(ms, "deadline")]);
7135
- }
7136
- #abortsAfter(ms, bound) {
7137
- const controller = new AbortController();
7138
- after(this.#clock, ms, this.#disposed.signal, () => {
7139
- const advice = {
7140
- hint: waitHint(bound),
7141
- next: [this.#rerun]
7142
- };
7143
- const reason = deadlineError(this.#commandLine, ms, advice, bound);
7144
- this.#abortReasons.add(reason);
7145
- controller.abort(reason);
7146
- });
7147
- return controller.signal;
7148
- }
7149
- async runToEnd(name, work) {
7150
- this.#graceCancel.abort();
7151
- if (this.signal.aborted) this.#finishing(name);
7152
- const step = work();
7153
- const settled = step.then(() => {}, () => {});
7154
- this.#steps.set(settled, name);
7155
- try {
7156
- return await step;
7157
- } finally {
7158
- this.#steps.delete(settled);
7159
- this.#startGrace();
7160
- }
7161
- }
7162
- /** Waits for the Command, its must-finish steps, and no longer than the grace once interrupted. */
7163
- async settle(work) {
7164
- try {
7165
- const value = await Promise.race([work, this.#graceOver]);
7166
- if (this.signal.aborted || value === GRACE_OVER) throw this.signal.reason;
7167
- return value;
7168
- } finally {
7169
- await this.#stepsDone();
7170
- this.close();
7171
- }
7172
- }
7173
- /**
7174
- * The error a run ends with: an interrupt or the run deadline outranks what the Command threw,
7175
- * unless the Command threw its own `INTERRUPTED`, saying what it finished before it stopped.
7176
- * Otherwise each leaf that is not a `CliError` and whose cause chain holds a deadline's abort is
7177
- * reported as that abort, so a declared error keeps its own code.
7178
- */
7179
- errorAfter(error) {
7180
- if (this.signal.aborted) return error instanceof CliError && error.code === "INTERRUPTED" ? error : this.signal.reason;
7181
- const leaves = errorsOf(error).map((leaf) => this.#abortIn(leaf) ?? leaf);
7182
- return leaves.length === 1 ? leaves[0] : new AggregateError(leaves);
7183
- }
7184
- #abortIn(error) {
7185
- if (error instanceof CliError) return void 0;
7186
- return causesOf(error).find((each) => each instanceof CliError && this.#abortReasons.has(each));
7187
- }
7188
- #onStop() {
7189
- for (const name of this.#steps.values()) this.#finishing(name);
7190
- this.#startGrace();
7191
- }
7192
- #finishing(name) {
7193
- this.#progress(`finishing ${name}`);
7194
- }
7195
- #startGrace() {
7196
- if (this.signal.aborted && this.#steps.size === 0) {
7197
- this.#graceCancel.abort();
7198
- this.#graceCancel = new AbortController();
7199
- after(this.#clock, GRACE_MS, this.#graceCancel.signal, () => this.#endGrace(GRACE_OVER));
7200
- }
7201
- }
7202
- async #stepsDone() {
7203
- while (this.#steps.size > 0) await Promise.all(this.#steps.keys());
7204
- }
7205
- /** Stops the run's timers and its hold on the caller's signal, for a run that never settles. */
7206
- close() {
7207
- this.#graceCancel.abort();
7208
- this.#disposed.abort();
7209
- this.#outer?.removeEventListener("abort", this.#onOuterAbort);
7210
- }
7211
- };
7212
- //#endregion
7213
7218
  //#region ../agent-cli/dist/context/operator.js
7214
7219
  const NEEDS_OPERATOR = "NEEDS_OPERATOR";
7215
7220
  /** Fails with `NEEDS_OPERATOR` when a Coding agent runs an Operator Command. */
@@ -7743,9 +7748,9 @@ function cliErrorOf(code, init, options) {
7743
7748
  /** The bundler's stamp, absent when the CLI runs unbundled. Read per call, so a test can define it. */
7744
7749
  function buildStamp() {
7745
7750
  return {
7746
- "sha": "3c4069a",
7751
+ "sha": "7f79c40",
7747
7752
  "dirty": false,
7748
- "builtAt": "2026-11-09T06:02:49.000Z"
7753
+ "builtAt": "2026-11-09T07:03:37.000Z"
7749
7754
  };
7750
7755
  }
7751
7756
  /** The first line of `--version`: the version, and the commit and time of a stamped build. */
@@ -11599,7 +11604,7 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11599
11604
  }, {
11600
11605
  clock,
11601
11606
  failed: (error) => {
11602
- if (isDebug(baseFormat)) progress(`side task failed: ${traceOf(error)}`);
11607
+ if (isDebug(baseFormat)) write.debug(progressLine(`side task failed: ${traceOf(error)}`, routed.json));
11603
11608
  }
11604
11609
  });
11605
11610
  const ran = ({ result, end }, sideTask) => {
@@ -11739,7 +11744,16 @@ async function whileStopping(signal, onStop, work) {
11739
11744
  }
11740
11745
  /** Runs the CLI, writes its output, then waits for the Side task it started. */
11741
11746
  async function runWithProgress(cli, argv, options, write, stopNotice) {
11742
- const { result, finishSideTask } = await routeAndRun(cli, argv, options, write, stopNotice);
11747
+ let resultWritten = false;
11748
+ const gated = (writeTo) => (text) => {
11749
+ if (!resultWritten) writeTo(text);
11750
+ };
11751
+ const { result, finishSideTask } = await routeAndRun(cli, argv, options, {
11752
+ stdout: gated(write.stdout),
11753
+ stderr: gated(write.stderr),
11754
+ debug: write.stderr
11755
+ }, stopNotice);
11756
+ resultWritten = true;
11743
11757
  await write.result(result);
11744
11758
  await finishSideTask();
11745
11759
  return result;
@@ -11842,7 +11856,7 @@ async function runProcess(cli) {
11842
11856
  //#region package.json
11843
11857
  var package_default = {
11844
11858
  name: "@duyquangnvx/create-agent-cli",
11845
- version: "0.30.0",
11859
+ version: "0.30.1",
11846
11860
  description: "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
11847
11861
  type: "module",
11848
11862
  engines: { "node": ">=22.12.0" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duyquangnvx/create-agent-cli",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "description": "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -21,7 +21,7 @@
21
21
  "access": "public"
22
22
  },
23
23
  "devDependencies": {
24
- "@duyquangnvx/agent-cli": "0.30.0",
24
+ "@duyquangnvx/agent-cli": "0.30.1",
25
25
  "tsdown": "0.23.0",
26
26
  "zod": "4.6.7"
27
27
  },