@duyquangnvx/create-agent-cli 0.29.0 → 0.30.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/bin.js +171 -80
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ What changed in each release of `@duyquangnvx/agent-cli` (the Runtime) and `@duy
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## v0.30.0 (2026-11-09)
8
+
9
+ ### Added
10
+
11
+ - 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.
12
+ - `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.
13
+
14
+ ### Changed
15
+
16
+ - **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.
17
+
18
+ ### Upgrade from v0.29.0
19
+
20
+ - 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`.
21
+
7
22
  ## v0.29.0 (2026-11-09)
8
23
 
9
24
  ### Added
package/dist/bin.js CHANGED
@@ -5972,10 +5972,19 @@ function bugHint(format) {
5972
5972
  const rerun = `rerun with ${debugVariable(format.cliName)}=1 to see the stack trace`;
5973
5973
  return bugs === void 0 ? rerun : `${rerun}, then report it at ${bugs}`;
5974
5974
  }
5975
+ /** The code and message the caller sees for one error a run failed with. */
5976
+ function runEndErrorOf(error) {
5977
+ return error instanceof CliError ? {
5978
+ code: error.code,
5979
+ message: error.message
5980
+ } : {
5981
+ code: INTERNAL,
5982
+ message: messageOf(error)
5983
+ };
5984
+ }
5975
5985
  function fieldsOf$1({ error, exitClass }, format) {
5976
5986
  if (!(error instanceof CliError)) return {
5977
- code: INTERNAL,
5978
- message: messageOf(error),
5987
+ ...runEndErrorOf(error),
5979
5988
  hint: bugHint(format)
5980
5989
  };
5981
5990
  const { code, message, hint, details, retryAt, next } = error;
@@ -7734,9 +7743,9 @@ function cliErrorOf(code, init, options) {
7734
7743
  /** The bundler's stamp, absent when the CLI runs unbundled. Read per call, so a test can define it. */
7735
7744
  function buildStamp() {
7736
7745
  return {
7737
- "sha": "0478338",
7746
+ "sha": "3c4069a",
7738
7747
  "dirty": false,
7739
- "builtAt": "2026-11-09T05:16:34.000Z"
7748
+ "builtAt": "2026-11-09T06:02:49.000Z"
7740
7749
  };
7741
7750
  }
7742
7751
  /** The first line of `--version`: the version, and the commit and time of a stamped build. */
@@ -8398,12 +8407,19 @@ const homeSyncSchema = object({
8398
8407
  written: array(string()),
8399
8408
  removed: array(string())
8400
8409
  });
8410
+ function copiedFromPackage(skill) {
8411
+ return skill !== void 0 && skill.add === void 0;
8412
+ }
8413
+ /** The home Skill copies `setup` and `update` write: none when the skill comes from its Skill Hub. */
8414
+ async function syncedCopiesOf(cli, ctx) {
8415
+ return copiedFromPackage(cli.skill) ? homeCopiesOf(ctx.files, cli.name, ctx.env) : [];
8416
+ }
8401
8417
  /**
8402
8418
  * 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.
8419
+ * declares no skill, its Skill Hub installs it, or that package ships none, which must not empty a copy.
8404
8420
  */
8405
8421
  async function syncHome(cli, packageDir, ctx) {
8406
- if (cli.skill === void 0) return null;
8422
+ if (!copiedFromPackage(cli.skill)) return null;
8407
8423
  const source = join(packageDir, cli.skill.folder);
8408
8424
  if ((await filesIn(ctx.files, source)).length === 0) return null;
8409
8425
  const copies = await homeCopiesOf(ctx.files, cli.name, ctx.env);
@@ -8463,14 +8479,27 @@ async function findingOf$1(cli, folder, install, ctx) {
8463
8479
  fix: `${install.runAs} setup`
8464
8480
  };
8465
8481
  }
8466
- /** A skill the package ships, which `setup` and `update` copy under home. */
8467
- function homeSkill(cli, { folder }, install) {
8482
+ async function hubFindingOf(cli, add, ctx) {
8483
+ const paths = await homeCopiesOf(ctx.files, cli.name, ctx.env);
8484
+ const copies = await Promise.all(paths.map((path) => homeCopyAt(ctx.files, path)));
8485
+ const found = copies.filter((copy) => copy.state !== "missing");
8486
+ return found.length === 0 ? {
8487
+ state: "problem",
8488
+ message: copies.map(describeCopy).join("; "),
8489
+ fix: add
8490
+ } : {
8491
+ state: "ok",
8492
+ message: found.map((copy) => copy.path).join("; ")
8493
+ };
8494
+ }
8495
+ /** A skill the package ships, which `setup` and `update` copy under home unless its Skill Hub installs it. */
8496
+ function homeSkill(cli, { folder, add }, install) {
8468
8497
  return {
8469
8498
  builtIns: {},
8470
8499
  rows: [{
8471
8500
  name: "skill",
8472
8501
  advisory: true,
8473
- run: (ctx) => findingOf$1(cli, folder, install, ctx)
8502
+ run: (ctx) => add === void 0 ? findingOf$1(cli, folder, install, ctx) : hubFindingOf(cli, add, ctx)
8474
8503
  }]
8475
8504
  };
8476
8505
  }
@@ -8543,7 +8572,7 @@ const setupSchema = object({
8543
8572
  version: string(),
8544
8573
  prefix: string(),
8545
8574
  installed: boolean(),
8546
- skill: homeSyncSchema.nullable(),
8575
+ skill: union([homeSyncSchema, object({ add: string() })]).nullable(),
8547
8576
  upgrades: array(upgradeSchema),
8548
8577
  docs: string().nullable()
8549
8578
  });
@@ -8603,13 +8632,23 @@ function withSetupLine(error, install) {
8603
8632
  next: [wholeLine(setupLineOf(install))]
8604
8633
  }, { cause: error });
8605
8634
  }
8635
+ function renderNext(name, docs, skill) {
8636
+ const read = docs === null ? [] : [`docs ${docs}`];
8637
+ if (skill === null || !("add" in skill)) return docs === null ? read : [...read, `Read that folder before using ${name}.`];
8638
+ const then = docs === null ? "" : `, then read that folder before using ${name}`;
8639
+ return [
8640
+ ...read,
8641
+ `Install the ${name} skill from its Skill Hub${then}.`,
8642
+ `run: ${skill.add}`
8643
+ ];
8644
+ }
8606
8645
  function renderSetup(cli, install, setup) {
8607
- const { version, prefix, docs } = setup;
8646
+ const { version, prefix, docs, skill } = setup;
8608
8647
  return [
8609
8648
  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),
8649
+ ...skill !== null && "copies" in skill ? renderHomeSync(cli.name, skill) : [],
8611
8650
  ...setup.upgrades.map(renderUpgrade),
8612
- ...docs === null ? [] : [`docs ${docs}`, `Read that folder before using ${cli.name}.`]
8651
+ ...renderNext(cli.name, docs, skill)
8613
8652
  ].join("\n");
8614
8653
  }
8615
8654
  /** The built-in `setup`, which runs from any copy, such as npx's, so it never asks to be the global one. */
@@ -8645,7 +8684,7 @@ function setupCommand(cli, install, launch) {
8645
8684
  latest: await latestVersion(npm),
8646
8685
  prefix,
8647
8686
  packageDir,
8648
- copies: cli.skill === void 0 ? [] : await homeCopiesOf(ctx.files, cli.name, ctx.env)
8687
+ copies: await syncedCopiesOf(cli, ctx)
8649
8688
  };
8650
8689
  },
8651
8690
  apply: async (plan, ctx) => {
@@ -8664,16 +8703,25 @@ function setupCommand(cli, install, launch) {
8664
8703
  version: installs || from === null ? latest : from,
8665
8704
  prefix,
8666
8705
  installed: installs,
8667
- skill,
8706
+ skill: cli.skill?.add === void 0 ? skill : { add: cli.skill.add },
8668
8707
  upgrades: installs && from !== null ? await upgradesOf(ctx.files, packageDir, from, latest) : [],
8669
8708
  docs: cli.docs === void 0 ? null : join(packageDir, cli.docs)
8670
8709
  };
8671
8710
  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
- });
8711
+ if (ctx.signal.aborted) {
8712
+ const add = cli.skill?.add;
8713
+ if (add === void 0) throw new CliError({
8714
+ code: INTERRUPTED,
8715
+ message: `interrupted, but ${plan.package}@${setup.version} and its skill are already in place`,
8716
+ hint: "nothing to run again"
8717
+ });
8718
+ throw runtimeError({
8719
+ code: INTERRUPTED,
8720
+ message: `interrupted, but ${plan.package}@${setup.version} is already installed`,
8721
+ hint: "run the Next step to install its skill from its Skill Hub",
8722
+ next: [wholeLine(add)]
8723
+ });
8724
+ }
8677
8725
  await assertOnPath(cli, prefix, launch, ctx);
8678
8726
  return setup;
8679
8727
  },
@@ -8734,7 +8782,7 @@ function updateCommand(cli, install, launch) {
8734
8782
  latest: await latestVersion(npm),
8735
8783
  prefix: copy.prefix,
8736
8784
  packageDir: copy.packageDir,
8737
- copies: cli.skill === void 0 ? [] : await homeCopiesOf(ctx.files, cli.name, ctx.env)
8785
+ copies: await syncedCopiesOf(cli, ctx)
8738
8786
  };
8739
8787
  },
8740
8788
  apply: async (plan, ctx) => {
@@ -9635,6 +9683,11 @@ function checkTree(commands, path, table) {
9635
9683
  }
9636
9684
  function checkSkill({ name, skill, install }) {
9637
9685
  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`);
9686
+ const add = skill?.add;
9687
+ if (add === void 0) return;
9688
+ if (add.trim() === "") throw new Error(`${name} declares an empty Skill Hub install line`);
9689
+ if (/[\r\n]/.test(add)) throw new Error(`${name} declares a Skill Hub install line holding a line break`);
9690
+ 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
9691
  }
9639
9692
  function checkDocs({ name, docs }) {
9640
9693
  if (docs === void 0) return;
@@ -9732,17 +9785,25 @@ function agentCli(values = {}) {
9732
9785
  /** How long a run waits for its Side task once its output is written. */
9733
9786
  const SIDE_TASK_WAIT_MS = 1e3;
9734
9787
  const noSideTask = async () => {};
9788
+ const NO_SIDE_TASK = {
9789
+ end: () => {},
9790
+ finish: noSideTask
9791
+ };
9735
9792
  function waitOver() {
9736
9793
  return new DOMException(`the side task ran past its ${SIDE_TASK_WAIT_MS} ms wait`, "TimeoutError");
9737
9794
  }
9738
9795
  function startSideTask(sideTask, runWith, { clock, failed }) {
9739
- if (sideTask === void 0) return noSideTask;
9796
+ if (sideTask === void 0) return NO_SIDE_TASK;
9797
+ let end = () => {};
9798
+ const ended = new Promise((resolve) => {
9799
+ end = resolve;
9800
+ });
9740
9801
  const controller = new AbortController();
9741
9802
  const { signal } = controller;
9742
- const running = (async () => sideTask(runWith(signal)))().catch((error) => {
9803
+ const running = (async () => sideTask(runWith(signal, ended)))().catch((error) => {
9743
9804
  if (!(signal.aborted && error === signal.reason)) failed(error);
9744
9805
  });
9745
- return async () => {
9806
+ const finish = async () => {
9746
9807
  const waited = new AbortController();
9747
9808
  const capped = new Promise((resolve) => after(clock, SIDE_TASK_WAIT_MS, waited.signal, () => {
9748
9809
  const over = waitOver();
@@ -9753,6 +9814,10 @@ function startSideTask(sideTask, runWith, { clock, failed }) {
9753
9814
  await Promise.race([running, capped]);
9754
9815
  waited.abort();
9755
9816
  };
9817
+ return {
9818
+ end,
9819
+ finish
9820
+ };
9756
9821
  }
9757
9822
  //#endregion
9758
9823
  //#region ../agent-cli/dist/command-line/suggest.js
@@ -10492,11 +10557,24 @@ function errorText({ leaves }, format) {
10492
10557
  const { usageHelp, json } = format;
10493
10558
  return usageHelp !== void 0 && !json && leaves.some((leaf) => leaf.error instanceof UsageError) ? `${text}\n${usageHelp}` : text;
10494
10559
  }
10560
+ function endOf(failure) {
10561
+ return {
10562
+ exitCode: failure.exitCode,
10563
+ errors: failure.leaves.map((leaf) => runEndErrorOf(leaf.error))
10564
+ };
10565
+ }
10566
+ const SUCCEEDED = {
10567
+ exitCode: SUCCESS_EXIT_CODE,
10568
+ errors: []
10569
+ };
10495
10570
  function failedResult(failure, format) {
10496
10571
  return {
10497
- stdout: "",
10498
- stderr: errorText(failure, format),
10499
- exitCode: failure.exitCode
10572
+ result: {
10573
+ stdout: "",
10574
+ stderr: errorText(failure, format),
10575
+ exitCode: failure.exitCode
10576
+ },
10577
+ end: endOf(failure)
10500
10578
  };
10501
10579
  }
10502
10580
  function stdoutOf({ value, render }, format, spill) {
@@ -10511,10 +10589,13 @@ function startedOutput(outcome, format, spill) {
10511
10589
  }, spill);
10512
10590
  }
10513
10591
  /** 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
10592
+ const STOPPED = {
10593
+ result: {
10594
+ stdout: "",
10595
+ stderr: "",
10596
+ exitCode: SUCCESS_EXIT_CODE
10597
+ },
10598
+ end: SUCCEEDED
10518
10599
  };
10519
10600
  async function errorResult(error, format) {
10520
10601
  return failedResult(await failureOf(error, format), format);
@@ -10522,9 +10603,12 @@ async function errorResult(error, format) {
10522
10603
  async function commandResult(settled, format, spill) {
10523
10604
  if (settled.ok) try {
10524
10605
  return {
10525
- stdout: await stdoutOf(settled.outcome, format, spill),
10526
- stderr: "",
10527
- exitCode: SUCCESS_EXIT_CODE
10606
+ result: {
10607
+ stdout: await stdoutOf(settled.outcome, format, spill),
10608
+ stderr: "",
10609
+ exitCode: SUCCESS_EXIT_CODE
10610
+ },
10611
+ end: SUCCEEDED
10528
10612
  };
10529
10613
  } catch (spillError) {
10530
10614
  return errorResult(spillError, format);
@@ -10538,16 +10622,26 @@ async function commandResult(settled, format, spill) {
10538
10622
  } catch (rejected) {
10539
10623
  return failedResult(withLeaves(failure, rejected, format), format);
10540
10624
  }
10541
- const result = failedResult(failure, format);
10625
+ const { result, end } = failedResult(failure, format);
10542
10626
  try {
10543
10627
  return {
10544
- ...result,
10545
- stdout: await stdoutOf(outcome, format, spill)
10628
+ result: {
10629
+ ...result,
10630
+ stdout: await stdoutOf(outcome, format, spill)
10631
+ },
10632
+ end
10546
10633
  };
10547
10634
  } catch (spillError) {
10635
+ const lost = withLeaves(failure, spillError, format);
10548
10636
  return {
10549
- ...result,
10550
- stderr: errorText(withLeaves(failure, spillError, format), format)
10637
+ result: {
10638
+ ...result,
10639
+ stderr: errorText(lost, format)
10640
+ },
10641
+ end: {
10642
+ ...endOf(lost),
10643
+ exitCode: result.exitCode
10644
+ }
10551
10645
  };
10552
10646
  }
10553
10647
  }
@@ -11490,12 +11584,34 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11490
11584
  json: routed.json,
11491
11585
  invocation
11492
11586
  };
11493
- const failed = async (error, finishSideTask) => {
11494
- control.close();
11587
+ const sideTaskOf = (dryRun) => startSideTask(builtIn ? void 0 : cli.sideTask?.bind(cli), (sideSignal, ended) => {
11588
+ const sideBase = scope.portsBase(sideSignal);
11589
+ const sidePorts = once(() => scope.ports(sideBase));
11495
11590
  return {
11496
- result: await errorResult(error, format),
11497
- finishSideTask
11591
+ ...sideBase,
11592
+ command: routed.spec.path,
11593
+ dryRun,
11594
+ ended,
11595
+ get ports() {
11596
+ return sidePorts();
11597
+ }
11498
11598
  };
11599
+ }, {
11600
+ clock,
11601
+ failed: (error) => {
11602
+ if (isDebug(baseFormat)) progress(`side task failed: ${traceOf(error)}`);
11603
+ }
11604
+ });
11605
+ const ran = ({ result, end }, sideTask) => {
11606
+ sideTask.end(end);
11607
+ return {
11608
+ result,
11609
+ finishSideTask: sideTask.finish
11610
+ };
11611
+ };
11612
+ const failed = (error) => {
11613
+ control.close();
11614
+ return errorResult(error, format);
11499
11615
  };
11500
11616
  let parsed;
11501
11617
  try {
@@ -11508,27 +11624,11 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11508
11624
  });
11509
11625
  });
11510
11626
  } catch (error) {
11511
- return failed(error, noSideTask);
11627
+ return ran(await failed(error), sideTaskOf(false));
11512
11628
  }
11513
11629
  const { input, flags } = parsed;
11514
11630
  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
- });
11631
+ const sideTask = sideTaskOf(flags.dryRun);
11532
11632
  let project;
11533
11633
  try {
11534
11634
  project = builtIn || cli.project === void 0 ? void 0 : projectOf(cli.project, cwd, {
@@ -11536,7 +11636,7 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11536
11636
  rerun
11537
11637
  });
11538
11638
  } catch (error) {
11539
- return failed(error, finishSideTask);
11639
+ return ran(await failed(error), sideTask);
11540
11640
  }
11541
11641
  let delivered;
11542
11642
  const ctx = contextOf(scope, control, {
@@ -11572,32 +11672,23 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11572
11672
  },
11573
11673
  outputs
11574
11674
  })));
11575
- if (served) return {
11576
- result: STOPPED_RESULT,
11577
- finishSideTask
11578
- };
11675
+ if (served) return ran(STOPPED, sideTask);
11579
11676
  settled = {
11580
11677
  ok: true,
11581
11678
  outcome
11582
11679
  };
11583
11680
  } catch (error) {
11584
- if (served && control.signal.aborted) return {
11585
- result: STOPPED_RESULT,
11586
- finishSideTask
11587
- };
11681
+ if (served && control.signal.aborted) return ran(STOPPED, sideTask);
11588
11682
  settled = {
11589
11683
  ok: false,
11590
11684
  error,
11591
11685
  delivered
11592
11686
  };
11593
11687
  }
11594
- return {
11595
- result: await commandResult(settled, {
11596
- ...format,
11597
- fields: flags.fields
11598
- }, spill),
11599
- finishSideTask
11600
- };
11688
+ return ran(await commandResult(settled, {
11689
+ ...format,
11690
+ fields: flags.fields
11691
+ }, spill), sideTask);
11601
11692
  }
11602
11693
  async function noticesOf(cli, launch, ctx) {
11603
11694
  await installOf(cli).notice(launch, ctx);
@@ -11685,10 +11776,10 @@ async function routeAndRun(cli, argv, options, write, stopNotice) {
11685
11776
  ...format,
11686
11777
  json: routed.json
11687
11778
  }));
11688
- case "usage error": return ranAlone(errorResult(routed.error, {
11779
+ case "usage error": return ranAlone((await errorResult(routed.error, {
11689
11780
  ...misused(routed.node),
11690
11781
  json: routed.json
11691
- }));
11782
+ })).result);
11692
11783
  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
11784
  }
11694
11785
  }
@@ -11751,7 +11842,7 @@ async function runProcess(cli) {
11751
11842
  //#region package.json
11752
11843
  var package_default = {
11753
11844
  name: "@duyquangnvx/create-agent-cli",
11754
- version: "0.29.0",
11845
+ version: "0.30.0",
11755
11846
  description: "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
11756
11847
  type: "module",
11757
11848
  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.0",
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.0",
25
25
  "tsdown": "0.23.0",
26
26
  "zod": "4.6.7"
27
27
  },