@duyquangnvx/create-agent-cli 0.29.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 +22 -0
  2. package/dist/bin.js +365 -260
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ 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
+
14
+ ## v0.30.0 (2026-11-09)
15
+
16
+ ### Added
17
+
18
+ - A Side task learns how its run ended: `SideTaskRun` has `ended`, a promise of `RunEnd`, `{ exitCode, errors: [{ code, message }] }` with no errors on success, as the caller sees it, the Runtime's codes such as `INTERRUPTED` included. `RunEnd` and `RunEndError` are exported.
19
+ - `defineCli({ skill })` of a Global install takes `add`, the whole line that installs the skill from its Skill Hub, so the Hub sees who installed it. `setup` then copies no skill and ends with `run: <add>`, its `--json` `skill` being `{ add }`, on every run; `update` leaves the Skill copy alone, its `skill` being `null`; and `doctor`'s `skill` row passes on any one home copy and gives `add` as the fix when there is none. `defineCli` throws on an empty or multi-line `add`, and on `add` beside any install but a Global install. Users whose copy came from the package move to the Hub when they run `setup` again, so a CLI that adds `add` asks them to.
20
+
21
+ ### Changed
22
+
23
+ - **Breaking:** `defineCli({ sideTask })` also runs for a Command whose input is rejected, such as a missing arg or an unknown option after its name, with `dryRun` false, so a usage count also counts those runs.
24
+
25
+ ### Upgrade from v0.29.0
26
+
27
+ - A Side task that must not count a run whose input was rejected awaits `ended` and skips that run by its error code, such as `INVALID_INPUT`.
28
+
7
29
  ## v0.29.0 (2026-11-09)
8
30
 
9
31
  ### 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) {
@@ -5972,10 +6006,19 @@ function bugHint(format) {
5972
6006
  const rerun = `rerun with ${debugVariable(format.cliName)}=1 to see the stack trace`;
5973
6007
  return bugs === void 0 ? rerun : `${rerun}, then report it at ${bugs}`;
5974
6008
  }
6009
+ /** The code and message the caller sees for one error a run failed with. */
6010
+ function runEndErrorOf(error) {
6011
+ return error instanceof CliError ? {
6012
+ code: error.code,
6013
+ message: error.message
6014
+ } : {
6015
+ code: INTERNAL,
6016
+ message: messageOf(error)
6017
+ };
6018
+ }
5975
6019
  function fieldsOf$1({ error, exitClass }, format) {
5976
6020
  if (!(error instanceof CliError)) return {
5977
- code: INTERNAL,
5978
- message: messageOf(error),
6021
+ ...runEndErrorOf(error),
5979
6022
  hint: bugHint(format)
5980
6023
  };
5981
6024
  const { code, message, hint, details, retryAt, next } = error;
@@ -6055,6 +6098,149 @@ function lastLines(output, count) {
6055
6098
  return output.trim().split("\n").slice(-count).join("\n");
6056
6099
  }
6057
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
6058
6244
  //#region ../agent-cli/dist/define.js
6059
6245
  function isCommand(node) {
6060
6246
  return typeof node.effect === "string";
@@ -6137,6 +6323,7 @@ function toolRows(tools, commands, platform) {
6137
6323
  const TOOL_FAILED = "TOOL_FAILED";
6138
6324
  const STDERR_TAIL_LINES$1 = 20;
6139
6325
  const SIGNAL_EXIT_BASE = 128;
6326
+ const KILL_AFTER_MS = 2e3;
6140
6327
  const BATCH_FILE = /\.(?:cmd|bat)$/i;
6141
6328
  const CMD_SHIM = /\.cmd$/i;
6142
6329
  const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
@@ -6173,9 +6360,10 @@ async function windowsLaunch(command, args, env) {
6173
6360
  shell: true
6174
6361
  };
6175
6362
  }
6176
- function stop(child) {
6363
+ function stop(child, exited) {
6177
6364
  if (process.platform !== "win32" || child.pid === void 0) {
6178
6365
  child.kill();
6366
+ after(realClock, KILL_AFTER_MS, exited, () => child.kill("SIGKILL"));
6179
6367
  return Promise.resolve();
6180
6368
  }
6181
6369
  const pid = String(child.pid);
@@ -6271,8 +6459,9 @@ async function spawnChild(command, args, { cwd, env, signal, onLine, stdin, deta
6271
6459
  detached: detached && process.platform !== "win32"
6272
6460
  });
6273
6461
  let stopped = Promise.resolve();
6462
+ const exited = new AbortController();
6274
6463
  const kill = () => {
6275
- stopped = stop(child);
6464
+ stopped = stop(child, exited.signal);
6276
6465
  };
6277
6466
  signal.addEventListener("abort", kill, { once: true });
6278
6467
  let callerFailure;
@@ -6295,7 +6484,9 @@ async function spawnChild(command, args, { cwd, env, signal, onLine, stdin, deta
6295
6484
  stderrLines.push(chunk);
6296
6485
  });
6297
6486
  if (stdin !== void 0 && child.stdin !== null) feed(child.stdin, stdin, signal).catch(fail);
6487
+ child.once("exit", () => exited.abort());
6298
6488
  child.on("error", (error) => {
6489
+ exited.abort();
6299
6490
  signal.removeEventListener("abort", kill);
6300
6491
  reject(error);
6301
6492
  });
@@ -6444,40 +6635,6 @@ function contextSpawnDetached(run, defaults, scope) {
6444
6635
  };
6445
6636
  }
6446
6637
  //#endregion
6447
- //#region ../agent-cli/dist/clock.js
6448
- const realClock = {
6449
- now: () => Date.now(),
6450
- sleep: (ms, signal) => new Promise((resolve, reject) => {
6451
- if (signal?.aborted === true) {
6452
- reject(signal.reason);
6453
- return;
6454
- }
6455
- const onAbort = () => {
6456
- clearTimeout(timer);
6457
- reject(signal?.reason);
6458
- };
6459
- const timer = setTimeout(() => {
6460
- signal?.removeEventListener("abort", onAbort);
6461
- resolve();
6462
- }, ms);
6463
- signal?.addEventListener("abort", onAbort, { once: true });
6464
- })
6465
- };
6466
- /** Runs `then` once `ms` have passed on `clock`; aborting `cancel` drops it. */
6467
- function after(clock, ms, cancel, then) {
6468
- clock.sleep(ms, cancel).then(then, (error) => {
6469
- if (!cancel.aborted) throw error;
6470
- });
6471
- }
6472
- async function poll(clock, check, intervalMs, signal) {
6473
- for (;;) {
6474
- signal.throwIfAborted();
6475
- const value = await check();
6476
- if (value !== void 0) return value;
6477
- await clock.sleep(intervalMs, signal);
6478
- }
6479
- }
6480
- //#endregion
6481
6638
  //#region ../agent-cli/dist/context/stdin.js
6482
6639
  const STDIN_META = "stdin";
6483
6640
  function isStdinSchema(schema) {
@@ -7058,149 +7215,6 @@ function classifyErrno(error, context) {
7058
7215
  }
7059
7216
  }
7060
7217
  //#endregion
7061
- //#region ../agent-cli/dist/context/interrupts.js
7062
- const INTERRUPTED = "INTERRUPTED";
7063
- const DEADLINE_EXCEEDED = "DEADLINE_EXCEEDED";
7064
- /** How long an interrupted Command may take to settle before the run ends without it. */
7065
- const GRACE_MS = 3e3;
7066
- const GRACE_OVER = Symbol("grace over");
7067
- function interruptedError(rerun) {
7068
- return new CliError({
7069
- code: INTERRUPTED,
7070
- message: "interrupted before it finished",
7071
- hint: "to finish it, run the Next step, unless it was stopped on purpose; if a tool timeout stopped it, give it longer",
7072
- next: [rerun]
7073
- });
7074
- }
7075
- const BOUND_WORDS = {
7076
- deadline: "deadline",
7077
- ignoreStop: "timeout"
7078
- };
7079
- function waitHint(bound) {
7080
- 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";
7081
- }
7082
- function deadlineError(commandLine, ms, advice, bound = "deadline") {
7083
- return new CliError({
7084
- code: DEADLINE_EXCEEDED,
7085
- message: `${commandLine} ran past its ${ms} ms ${BOUND_WORDS[bound]}`,
7086
- ...advice
7087
- });
7088
- }
7089
- /** One Command run's cancellation: its signal, its deadlines and the steps it must finish. */
7090
- var RunControl = class {
7091
- signal;
7092
- #controller = new AbortController();
7093
- #commandLine;
7094
- #rerun;
7095
- #outer;
7096
- #onOuterAbort = () => this.#controller.abort(interruptedError(this.#rerun));
7097
- /** Each running must-finish step, tracked until it settles, with its name. */
7098
- #steps = /* @__PURE__ */ new Map();
7099
- #progress;
7100
- #clock;
7101
- #disposed = new AbortController();
7102
- /** The reasons this run's deadlines aborted with, to tell them in a Command's error. */
7103
- #abortReasons = /* @__PURE__ */ new WeakSet();
7104
- #graceCancel = new AbortController();
7105
- #endGrace = () => {};
7106
- #graceOver = new Promise((resolve) => {
7107
- this.#endGrace = resolve;
7108
- });
7109
- constructor({ commandLine, rerun, outer, clock, progress, runDeadline }) {
7110
- this.#commandLine = commandLine;
7111
- this.#rerun = rerun;
7112
- this.#outer = outer;
7113
- this.#clock = clock;
7114
- this.#progress = progress;
7115
- this.signal = this.#controller.signal;
7116
- this.signal.addEventListener("abort", () => this.#onStop(), { once: true });
7117
- if (outer?.aborted === true) this.#onOuterAbort();
7118
- else outer?.addEventListener("abort", this.#onOuterAbort, { once: true });
7119
- if (runDeadline !== void 0) {
7120
- const { ms, advice } = runDeadline;
7121
- after(clock, ms, this.#disposed.signal, () => this.#controller.abort(deadlineError(commandLine, ms, advice())));
7122
- }
7123
- }
7124
- deadline(ms, options = {}) {
7125
- return options.ignoreStop === true ? this.#abortsAfter(ms, "ignoreStop") : AbortSignal.any([this.signal, this.#abortsAfter(ms, "deadline")]);
7126
- }
7127
- #abortsAfter(ms, bound) {
7128
- const controller = new AbortController();
7129
- after(this.#clock, ms, this.#disposed.signal, () => {
7130
- const advice = {
7131
- hint: waitHint(bound),
7132
- next: [this.#rerun]
7133
- };
7134
- const reason = deadlineError(this.#commandLine, ms, advice, bound);
7135
- this.#abortReasons.add(reason);
7136
- controller.abort(reason);
7137
- });
7138
- return controller.signal;
7139
- }
7140
- async runToEnd(name, work) {
7141
- this.#graceCancel.abort();
7142
- if (this.signal.aborted) this.#finishing(name);
7143
- const step = work();
7144
- const settled = step.then(() => {}, () => {});
7145
- this.#steps.set(settled, name);
7146
- try {
7147
- return await step;
7148
- } finally {
7149
- this.#steps.delete(settled);
7150
- this.#startGrace();
7151
- }
7152
- }
7153
- /** Waits for the Command, its must-finish steps, and no longer than the grace once interrupted. */
7154
- async settle(work) {
7155
- try {
7156
- const value = await Promise.race([work, this.#graceOver]);
7157
- if (this.signal.aborted || value === GRACE_OVER) throw this.signal.reason;
7158
- return value;
7159
- } finally {
7160
- await this.#stepsDone();
7161
- this.close();
7162
- }
7163
- }
7164
- /**
7165
- * The error a run ends with: an interrupt or the run deadline outranks what the Command threw,
7166
- * unless the Command threw its own `INTERRUPTED`, saying what it finished before it stopped.
7167
- * Otherwise each leaf that is not a `CliError` and whose cause chain holds a deadline's abort is
7168
- * reported as that abort, so a declared error keeps its own code.
7169
- */
7170
- errorAfter(error) {
7171
- if (this.signal.aborted) return error instanceof CliError && error.code === "INTERRUPTED" ? error : this.signal.reason;
7172
- const leaves = errorsOf(error).map((leaf) => this.#abortIn(leaf) ?? leaf);
7173
- return leaves.length === 1 ? leaves[0] : new AggregateError(leaves);
7174
- }
7175
- #abortIn(error) {
7176
- if (error instanceof CliError) return void 0;
7177
- return causesOf(error).find((each) => each instanceof CliError && this.#abortReasons.has(each));
7178
- }
7179
- #onStop() {
7180
- for (const name of this.#steps.values()) this.#finishing(name);
7181
- this.#startGrace();
7182
- }
7183
- #finishing(name) {
7184
- this.#progress(`finishing ${name}`);
7185
- }
7186
- #startGrace() {
7187
- if (this.signal.aborted && this.#steps.size === 0) {
7188
- this.#graceCancel.abort();
7189
- this.#graceCancel = new AbortController();
7190
- after(this.#clock, GRACE_MS, this.#graceCancel.signal, () => this.#endGrace(GRACE_OVER));
7191
- }
7192
- }
7193
- async #stepsDone() {
7194
- while (this.#steps.size > 0) await Promise.all(this.#steps.keys());
7195
- }
7196
- /** Stops the run's timers and its hold on the caller's signal, for a run that never settles. */
7197
- close() {
7198
- this.#graceCancel.abort();
7199
- this.#disposed.abort();
7200
- this.#outer?.removeEventListener("abort", this.#onOuterAbort);
7201
- }
7202
- };
7203
- //#endregion
7204
7218
  //#region ../agent-cli/dist/context/operator.js
7205
7219
  const NEEDS_OPERATOR = "NEEDS_OPERATOR";
7206
7220
  /** Fails with `NEEDS_OPERATOR` when a Coding agent runs an Operator Command. */
@@ -7734,9 +7748,9 @@ function cliErrorOf(code, init, options) {
7734
7748
  /** The bundler's stamp, absent when the CLI runs unbundled. Read per call, so a test can define it. */
7735
7749
  function buildStamp() {
7736
7750
  return {
7737
- "sha": "0478338",
7751
+ "sha": "7f79c40",
7738
7752
  "dirty": false,
7739
- "builtAt": "2026-11-09T05:16:34.000Z"
7753
+ "builtAt": "2026-11-09T07:03:37.000Z"
7740
7754
  };
7741
7755
  }
7742
7756
  /** The first line of `--version`: the version, and the commit and time of a stamped build. */
@@ -8398,12 +8412,19 @@ const homeSyncSchema = object({
8398
8412
  written: array(string()),
8399
8413
  removed: array(string())
8400
8414
  });
8415
+ function copiedFromPackage(skill) {
8416
+ return skill !== void 0 && skill.add === void 0;
8417
+ }
8418
+ /** The home Skill copies `setup` and `update` write: none when the skill comes from its Skill Hub. */
8419
+ async function syncedCopiesOf(cli, ctx) {
8420
+ return copiedFromPackage(cli.skill) ? homeCopiesOf(ctx.files, cli.name, ctx.env) : [];
8421
+ }
8401
8422
  /**
8402
8423
  * Makes each home Skill copy match the folder in the package at `packageDir`. Null when the CLI
8403
- * declares no skill, or that package ships none, which must not empty a copy.
8424
+ * declares no skill, its Skill Hub installs it, or that package ships none, which must not empty a copy.
8404
8425
  */
8405
8426
  async function syncHome(cli, packageDir, ctx) {
8406
- if (cli.skill === void 0) return null;
8427
+ if (!copiedFromPackage(cli.skill)) return null;
8407
8428
  const source = join(packageDir, cli.skill.folder);
8408
8429
  if ((await filesIn(ctx.files, source)).length === 0) return null;
8409
8430
  const copies = await homeCopiesOf(ctx.files, cli.name, ctx.env);
@@ -8463,14 +8484,27 @@ async function findingOf$1(cli, folder, install, ctx) {
8463
8484
  fix: `${install.runAs} setup`
8464
8485
  };
8465
8486
  }
8466
- /** A skill the package ships, which `setup` and `update` copy under home. */
8467
- function homeSkill(cli, { folder }, install) {
8487
+ async function hubFindingOf(cli, add, ctx) {
8488
+ const paths = await homeCopiesOf(ctx.files, cli.name, ctx.env);
8489
+ const copies = await Promise.all(paths.map((path) => homeCopyAt(ctx.files, path)));
8490
+ const found = copies.filter((copy) => copy.state !== "missing");
8491
+ return found.length === 0 ? {
8492
+ state: "problem",
8493
+ message: copies.map(describeCopy).join("; "),
8494
+ fix: add
8495
+ } : {
8496
+ state: "ok",
8497
+ message: found.map((copy) => copy.path).join("; ")
8498
+ };
8499
+ }
8500
+ /** A skill the package ships, which `setup` and `update` copy under home unless its Skill Hub installs it. */
8501
+ function homeSkill(cli, { folder, add }, install) {
8468
8502
  return {
8469
8503
  builtIns: {},
8470
8504
  rows: [{
8471
8505
  name: "skill",
8472
8506
  advisory: true,
8473
- run: (ctx) => findingOf$1(cli, folder, install, ctx)
8507
+ run: (ctx) => add === void 0 ? findingOf$1(cli, folder, install, ctx) : hubFindingOf(cli, add, ctx)
8474
8508
  }]
8475
8509
  };
8476
8510
  }
@@ -8543,7 +8577,7 @@ const setupSchema = object({
8543
8577
  version: string(),
8544
8578
  prefix: string(),
8545
8579
  installed: boolean(),
8546
- skill: homeSyncSchema.nullable(),
8580
+ skill: union([homeSyncSchema, object({ add: string() })]).nullable(),
8547
8581
  upgrades: array(upgradeSchema),
8548
8582
  docs: string().nullable()
8549
8583
  });
@@ -8603,13 +8637,23 @@ function withSetupLine(error, install) {
8603
8637
  next: [wholeLine(setupLineOf(install))]
8604
8638
  }, { cause: error });
8605
8639
  }
8640
+ function renderNext(name, docs, skill) {
8641
+ const read = docs === null ? [] : [`docs ${docs}`];
8642
+ if (skill === null || !("add" in skill)) return docs === null ? read : [...read, `Read that folder before using ${name}.`];
8643
+ const then = docs === null ? "" : `, then read that folder before using ${name}`;
8644
+ return [
8645
+ ...read,
8646
+ `Install the ${name} skill from its Skill Hub${then}.`,
8647
+ `run: ${skill.add}`
8648
+ ];
8649
+ }
8606
8650
  function renderSetup(cli, install, setup) {
8607
- const { version, prefix, docs } = setup;
8651
+ const { version, prefix, docs, skill } = setup;
8608
8652
  return [
8609
8653
  setup.installed ? `installed ${install.package}@${version} with the npm under ${prefix}` : `${install.package}@${version} is already installed under ${prefix}`,
8610
- ...renderHomeSync(cli.name, setup.skill),
8654
+ ...skill !== null && "copies" in skill ? renderHomeSync(cli.name, skill) : [],
8611
8655
  ...setup.upgrades.map(renderUpgrade),
8612
- ...docs === null ? [] : [`docs ${docs}`, `Read that folder before using ${cli.name}.`]
8656
+ ...renderNext(cli.name, docs, skill)
8613
8657
  ].join("\n");
8614
8658
  }
8615
8659
  /** The built-in `setup`, which runs from any copy, such as npx's, so it never asks to be the global one. */
@@ -8645,7 +8689,7 @@ function setupCommand(cli, install, launch) {
8645
8689
  latest: await latestVersion(npm),
8646
8690
  prefix,
8647
8691
  packageDir,
8648
- copies: cli.skill === void 0 ? [] : await homeCopiesOf(ctx.files, cli.name, ctx.env)
8692
+ copies: await syncedCopiesOf(cli, ctx)
8649
8693
  };
8650
8694
  },
8651
8695
  apply: async (plan, ctx) => {
@@ -8664,16 +8708,25 @@ function setupCommand(cli, install, launch) {
8664
8708
  version: installs || from === null ? latest : from,
8665
8709
  prefix,
8666
8710
  installed: installs,
8667
- skill,
8711
+ skill: cli.skill?.add === void 0 ? skill : { add: cli.skill.add },
8668
8712
  upgrades: installs && from !== null ? await upgradesOf(ctx.files, packageDir, from, latest) : [],
8669
8713
  docs: cli.docs === void 0 ? null : join(packageDir, cli.docs)
8670
8714
  };
8671
8715
  ctx.deliver(setup);
8672
- if (ctx.signal.aborted) throw new CliError({
8673
- code: INTERRUPTED,
8674
- message: `interrupted, but ${plan.package}@${setup.version} and its skill are already in place`,
8675
- hint: "nothing to run again"
8676
- });
8716
+ if (ctx.signal.aborted) {
8717
+ const add = cli.skill?.add;
8718
+ if (add === void 0) throw new CliError({
8719
+ code: INTERRUPTED,
8720
+ message: `interrupted, but ${plan.package}@${setup.version} and its skill are already in place`,
8721
+ hint: "nothing to run again"
8722
+ });
8723
+ throw runtimeError({
8724
+ code: INTERRUPTED,
8725
+ message: `interrupted, but ${plan.package}@${setup.version} is already installed`,
8726
+ hint: "run the Next step to install its skill from its Skill Hub",
8727
+ next: [wholeLine(add)]
8728
+ });
8729
+ }
8677
8730
  await assertOnPath(cli, prefix, launch, ctx);
8678
8731
  return setup;
8679
8732
  },
@@ -8734,7 +8787,7 @@ function updateCommand(cli, install, launch) {
8734
8787
  latest: await latestVersion(npm),
8735
8788
  prefix: copy.prefix,
8736
8789
  packageDir: copy.packageDir,
8737
- copies: cli.skill === void 0 ? [] : await homeCopiesOf(ctx.files, cli.name, ctx.env)
8790
+ copies: await syncedCopiesOf(cli, ctx)
8738
8791
  };
8739
8792
  },
8740
8793
  apply: async (plan, ctx) => {
@@ -9635,6 +9688,11 @@ function checkTree(commands, path, table) {
9635
9688
  }
9636
9689
  function checkSkill({ name, skill, install }) {
9637
9690
  if (skill?.agents !== void 0 && install?.mode === "global") throw new Error(`${name} declares an Agents section on a Global install, which has no repo to write it to`);
9691
+ const add = skill?.add;
9692
+ if (add === void 0) return;
9693
+ if (add.trim() === "") throw new Error(`${name} declares an empty Skill Hub install line`);
9694
+ if (/[\r\n]/.test(add)) throw new Error(`${name} declares a Skill Hub install line holding a line break`);
9695
+ if (install?.mode !== "global") throw new Error(`${name} declares a Skill Hub install line, which only a Global install uses: any other's Skill copy goes in the project`);
9638
9696
  }
9639
9697
  function checkDocs({ name, docs }) {
9640
9698
  if (docs === void 0) return;
@@ -9732,17 +9790,25 @@ function agentCli(values = {}) {
9732
9790
  /** How long a run waits for its Side task once its output is written. */
9733
9791
  const SIDE_TASK_WAIT_MS = 1e3;
9734
9792
  const noSideTask = async () => {};
9793
+ const NO_SIDE_TASK = {
9794
+ end: () => {},
9795
+ finish: noSideTask
9796
+ };
9735
9797
  function waitOver() {
9736
9798
  return new DOMException(`the side task ran past its ${SIDE_TASK_WAIT_MS} ms wait`, "TimeoutError");
9737
9799
  }
9738
9800
  function startSideTask(sideTask, runWith, { clock, failed }) {
9739
- if (sideTask === void 0) return noSideTask;
9801
+ if (sideTask === void 0) return NO_SIDE_TASK;
9802
+ let end = () => {};
9803
+ const ended = new Promise((resolve) => {
9804
+ end = resolve;
9805
+ });
9740
9806
  const controller = new AbortController();
9741
9807
  const { signal } = controller;
9742
- const running = (async () => sideTask(runWith(signal)))().catch((error) => {
9808
+ const running = (async () => sideTask(runWith(signal, ended)))().catch((error) => {
9743
9809
  if (!(signal.aborted && error === signal.reason)) failed(error);
9744
9810
  });
9745
- return async () => {
9811
+ const finish = async () => {
9746
9812
  const waited = new AbortController();
9747
9813
  const capped = new Promise((resolve) => after(clock, SIDE_TASK_WAIT_MS, waited.signal, () => {
9748
9814
  const over = waitOver();
@@ -9753,6 +9819,10 @@ function startSideTask(sideTask, runWith, { clock, failed }) {
9753
9819
  await Promise.race([running, capped]);
9754
9820
  waited.abort();
9755
9821
  };
9822
+ return {
9823
+ end,
9824
+ finish
9825
+ };
9756
9826
  }
9757
9827
  //#endregion
9758
9828
  //#region ../agent-cli/dist/command-line/suggest.js
@@ -10492,11 +10562,24 @@ function errorText({ leaves }, format) {
10492
10562
  const { usageHelp, json } = format;
10493
10563
  return usageHelp !== void 0 && !json && leaves.some((leaf) => leaf.error instanceof UsageError) ? `${text}\n${usageHelp}` : text;
10494
10564
  }
10565
+ function endOf(failure) {
10566
+ return {
10567
+ exitCode: failure.exitCode,
10568
+ errors: failure.leaves.map((leaf) => runEndErrorOf(leaf.error))
10569
+ };
10570
+ }
10571
+ const SUCCEEDED = {
10572
+ exitCode: SUCCESS_EXIT_CODE,
10573
+ errors: []
10574
+ };
10495
10575
  function failedResult(failure, format) {
10496
10576
  return {
10497
- stdout: "",
10498
- stderr: errorText(failure, format),
10499
- exitCode: failure.exitCode
10577
+ result: {
10578
+ stdout: "",
10579
+ stderr: errorText(failure, format),
10580
+ exitCode: failure.exitCode
10581
+ },
10582
+ end: endOf(failure)
10500
10583
  };
10501
10584
  }
10502
10585
  function stdoutOf({ value, render }, format, spill) {
@@ -10511,10 +10594,13 @@ function startedOutput(outcome, format, spill) {
10511
10594
  }, spill);
10512
10595
  }
10513
10596
  /** A serve Command stopped, or ended itself, after it started: that is how it is meant to end. */
10514
- const STOPPED_RESULT = {
10515
- stdout: "",
10516
- stderr: "",
10517
- exitCode: SUCCESS_EXIT_CODE
10597
+ const STOPPED = {
10598
+ result: {
10599
+ stdout: "",
10600
+ stderr: "",
10601
+ exitCode: SUCCESS_EXIT_CODE
10602
+ },
10603
+ end: SUCCEEDED
10518
10604
  };
10519
10605
  async function errorResult(error, format) {
10520
10606
  return failedResult(await failureOf(error, format), format);
@@ -10522,9 +10608,12 @@ async function errorResult(error, format) {
10522
10608
  async function commandResult(settled, format, spill) {
10523
10609
  if (settled.ok) try {
10524
10610
  return {
10525
- stdout: await stdoutOf(settled.outcome, format, spill),
10526
- stderr: "",
10527
- exitCode: SUCCESS_EXIT_CODE
10611
+ result: {
10612
+ stdout: await stdoutOf(settled.outcome, format, spill),
10613
+ stderr: "",
10614
+ exitCode: SUCCESS_EXIT_CODE
10615
+ },
10616
+ end: SUCCEEDED
10528
10617
  };
10529
10618
  } catch (spillError) {
10530
10619
  return errorResult(spillError, format);
@@ -10538,16 +10627,26 @@ async function commandResult(settled, format, spill) {
10538
10627
  } catch (rejected) {
10539
10628
  return failedResult(withLeaves(failure, rejected, format), format);
10540
10629
  }
10541
- const result = failedResult(failure, format);
10630
+ const { result, end } = failedResult(failure, format);
10542
10631
  try {
10543
10632
  return {
10544
- ...result,
10545
- stdout: await stdoutOf(outcome, format, spill)
10633
+ result: {
10634
+ ...result,
10635
+ stdout: await stdoutOf(outcome, format, spill)
10636
+ },
10637
+ end
10546
10638
  };
10547
10639
  } catch (spillError) {
10640
+ const lost = withLeaves(failure, spillError, format);
10548
10641
  return {
10549
- ...result,
10550
- stderr: errorText(withLeaves(failure, spillError, format), format)
10642
+ result: {
10643
+ ...result,
10644
+ stderr: errorText(lost, format)
10645
+ },
10646
+ end: {
10647
+ ...endOf(lost),
10648
+ exitCode: result.exitCode
10649
+ }
10551
10650
  };
10552
10651
  }
10553
10652
  }
@@ -11490,13 +11589,35 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11490
11589
  json: routed.json,
11491
11590
  invocation
11492
11591
  };
11493
- const failed = async (error, finishSideTask) => {
11494
- control.close();
11592
+ const sideTaskOf = (dryRun) => startSideTask(builtIn ? void 0 : cli.sideTask?.bind(cli), (sideSignal, ended) => {
11593
+ const sideBase = scope.portsBase(sideSignal);
11594
+ const sidePorts = once(() => scope.ports(sideBase));
11595
+ return {
11596
+ ...sideBase,
11597
+ command: routed.spec.path,
11598
+ dryRun,
11599
+ ended,
11600
+ get ports() {
11601
+ return sidePorts();
11602
+ }
11603
+ };
11604
+ }, {
11605
+ clock,
11606
+ failed: (error) => {
11607
+ if (isDebug(baseFormat)) write.debug(progressLine(`side task failed: ${traceOf(error)}`, routed.json));
11608
+ }
11609
+ });
11610
+ const ran = ({ result, end }, sideTask) => {
11611
+ sideTask.end(end);
11495
11612
  return {
11496
- result: await errorResult(error, format),
11497
- finishSideTask
11613
+ result,
11614
+ finishSideTask: sideTask.finish
11498
11615
  };
11499
11616
  };
11617
+ const failed = (error) => {
11618
+ control.close();
11619
+ return errorResult(error, format);
11620
+ };
11500
11621
  let parsed;
11501
11622
  try {
11502
11623
  checkOperator(routed.spec, env, rerun);
@@ -11508,27 +11629,11 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11508
11629
  });
11509
11630
  });
11510
11631
  } catch (error) {
11511
- return failed(error, noSideTask);
11632
+ return ran(await failed(error), sideTaskOf(false));
11512
11633
  }
11513
11634
  const { input, flags } = parsed;
11514
11635
  resumed = command.deadline?.resume(input, rerunOf);
11515
- const finishSideTask = startSideTask(builtIn ? void 0 : cli.sideTask?.bind(cli), (sideSignal) => {
11516
- const sideBase = scope.portsBase(sideSignal);
11517
- const sidePorts = once(() => scope.ports(sideBase));
11518
- return {
11519
- ...sideBase,
11520
- command: routed.spec.path,
11521
- dryRun: flags.dryRun,
11522
- get ports() {
11523
- return sidePorts();
11524
- }
11525
- };
11526
- }, {
11527
- clock,
11528
- failed: (error) => {
11529
- if (isDebug(baseFormat)) progress(`side task failed: ${traceOf(error)}`);
11530
- }
11531
- });
11636
+ const sideTask = sideTaskOf(flags.dryRun);
11532
11637
  let project;
11533
11638
  try {
11534
11639
  project = builtIn || cli.project === void 0 ? void 0 : projectOf(cli.project, cwd, {
@@ -11536,7 +11641,7 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11536
11641
  rerun
11537
11642
  });
11538
11643
  } catch (error) {
11539
- return failed(error, finishSideTask);
11644
+ return ran(await failed(error), sideTask);
11540
11645
  }
11541
11646
  let delivered;
11542
11647
  const ctx = contextOf(scope, control, {
@@ -11572,32 +11677,23 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11572
11677
  },
11573
11678
  outputs
11574
11679
  })));
11575
- if (served) return {
11576
- result: STOPPED_RESULT,
11577
- finishSideTask
11578
- };
11680
+ if (served) return ran(STOPPED, sideTask);
11579
11681
  settled = {
11580
11682
  ok: true,
11581
11683
  outcome
11582
11684
  };
11583
11685
  } catch (error) {
11584
- if (served && control.signal.aborted) return {
11585
- result: STOPPED_RESULT,
11586
- finishSideTask
11587
- };
11686
+ if (served && control.signal.aborted) return ran(STOPPED, sideTask);
11588
11687
  settled = {
11589
11688
  ok: false,
11590
11689
  error,
11591
11690
  delivered
11592
11691
  };
11593
11692
  }
11594
- return {
11595
- result: await commandResult(settled, {
11596
- ...format,
11597
- fields: flags.fields
11598
- }, spill),
11599
- finishSideTask
11600
- };
11693
+ return ran(await commandResult(settled, {
11694
+ ...format,
11695
+ fields: flags.fields
11696
+ }, spill), sideTask);
11601
11697
  }
11602
11698
  async function noticesOf(cli, launch, ctx) {
11603
11699
  await installOf(cli).notice(launch, ctx);
@@ -11648,7 +11744,16 @@ async function whileStopping(signal, onStop, work) {
11648
11744
  }
11649
11745
  /** Runs the CLI, writes its output, then waits for the Side task it started. */
11650
11746
  async function runWithProgress(cli, argv, options, write, stopNotice) {
11651
- 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;
11652
11757
  await write.result(result);
11653
11758
  await finishSideTask();
11654
11759
  return result;
@@ -11685,10 +11790,10 @@ async function routeAndRun(cli, argv, options, write, stopNotice) {
11685
11790
  ...format,
11686
11791
  json: routed.json
11687
11792
  }));
11688
- case "usage error": return ranAlone(errorResult(routed.error, {
11793
+ case "usage error": return ranAlone((await errorResult(routed.error, {
11689
11794
  ...misused(routed.node),
11690
11795
  json: routed.json
11691
- }));
11796
+ })).result);
11692
11797
  case "command": return whileStopping(options.signal, stopNotice === void 0 ? void 0 : () => write.stderr(stopLine(stopNotice, routed.json)), () => runCommand(cli, routed, options, write, misused(routed.spec)));
11693
11798
  }
11694
11799
  }
@@ -11751,7 +11856,7 @@ async function runProcess(cli) {
11751
11856
  //#region package.json
11752
11857
  var package_default = {
11753
11858
  name: "@duyquangnvx/create-agent-cli",
11754
- version: "0.29.0",
11859
+ version: "0.30.1",
11755
11860
  description: "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
11756
11861
  type: "module",
11757
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.29.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.29.0",
24
+ "@duyquangnvx/agent-cli": "0.30.1",
25
25
  "tsdown": "0.23.0",
26
26
  "zod": "4.6.7"
27
27
  },