@duyquangnvx/create-agent-cli 0.28.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@ 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
+
22
+ ## v0.29.0 (2026-11-09)
23
+
24
+ ### Added
25
+
26
+ - `agentCli` returns `defineDoctorCheck`, which types a `doctor` row's `ctx` by the CLI's ports and Project file and its `errors` by the Error table, so a row needs no `DoctorCheck<Ports, Settings>` annotation and a code the table lacks is a type error.
27
+
28
+ ### Changed
29
+
30
+ - **Breaking:** `defineCommand`, as `agentCli` returns it, is one method per Effect: `defineCommand.read`, `.act`, `.write`, `.destructive`, `.serve`, and `.list` for a List Command. A Command no longer declares `effect`, which the method sets, and `defineCommand.read` no longer takes `rows`. Each method has one signature, so a type error names the property at fault, such as a `run` returning a string where `output` has a number, rather than an Effect. The scaffolded template uses the methods.
31
+ - **Breaking:** `checkExamples` from `@duyquangnvx/agent-cli/testing` is `checkCli`, since it checks more than examples; the scaffolded template's test calls it.
32
+ - **Breaking:** `checkCli` also fails on a Command help shows that has no example, and on an arg or option without `.describe()`, so a CLI's test that passed `checkExamples` may fail until it adds them.
33
+ - `ctx.secret` with a name the Command does not declare is a type error that names the declared Secrets, or reads `"the Command declares no secrets"` when it declares none, rather than one about `never`.
34
+ - The scaffolded template pins `@biomejs/biome` 2.6.2 and `@duyquangnvx/harness` 0.5.2, the versions the Runtime itself is checked with.
35
+
36
+ ### Upgrade from v0.28.0
37
+
38
+ - Call `checkCli` where the CLI's tests called `checkExamples`, then give each Command and field it names an example or a `.describe()`.
39
+ - Declare each Command with the method of its Effect and delete its `effect`: `defineCommand({ effect: "write", … })` becomes `defineCommand.write({ … })`, and a read Command that declares `rows` becomes `defineCommand.list({ … })`.
40
+
7
41
  ## v0.28.0 (2026-11-09)
8
42
 
9
43
  ### 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;
@@ -7700,10 +7709,20 @@ function checkErrors(errors, label, table) {
7700
7709
  if (unknown.length > 0) throw new Error(`${label} declares ${unknown.join(", ")} in errors, a code the Error table lacks`);
7701
7710
  }
7702
7711
  function defineCommandFor() {
7703
- return declared;
7712
+ return {
7713
+ list: withEffect("read"),
7714
+ read: withEffect("read"),
7715
+ act: withEffect("act"),
7716
+ serve: withEffect("serve"),
7717
+ write: withEffect("write"),
7718
+ destructive: withEffect("destructive")
7719
+ };
7704
7720
  }
7705
- function declared(command) {
7706
- return command;
7721
+ function withEffect(effect) {
7722
+ return (command) => ({
7723
+ ...command,
7724
+ effect
7725
+ });
7707
7726
  }
7708
7727
  const defineCommand$1 = defineCommandFor();
7709
7728
  //#endregion
@@ -7724,9 +7743,9 @@ function cliErrorOf(code, init, options) {
7724
7743
  /** The bundler's stamp, absent when the CLI runs unbundled. Read per call, so a test can define it. */
7725
7744
  function buildStamp() {
7726
7745
  return {
7727
- "sha": "b509cf2",
7746
+ "sha": "3c4069a",
7728
7747
  "dirty": false,
7729
- "builtAt": "2026-11-09T01:38:28.000Z"
7748
+ "builtAt": "2026-11-09T06:02:49.000Z"
7730
7749
  };
7731
7750
  }
7732
7751
  /** The first line of `--version`: the version, and the commit and time of a stamped build. */
@@ -8388,12 +8407,19 @@ const homeSyncSchema = object({
8388
8407
  written: array(string()),
8389
8408
  removed: array(string())
8390
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
+ }
8391
8417
  /**
8392
8418
  * Makes each home Skill copy match the folder in the package at `packageDir`. Null when the CLI
8393
- * 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.
8394
8420
  */
8395
8421
  async function syncHome(cli, packageDir, ctx) {
8396
- if (cli.skill === void 0) return null;
8422
+ if (!copiedFromPackage(cli.skill)) return null;
8397
8423
  const source = join(packageDir, cli.skill.folder);
8398
8424
  if ((await filesIn(ctx.files, source)).length === 0) return null;
8399
8425
  const copies = await homeCopiesOf(ctx.files, cli.name, ctx.env);
@@ -8453,14 +8479,27 @@ async function findingOf$1(cli, folder, install, ctx) {
8453
8479
  fix: `${install.runAs} setup`
8454
8480
  };
8455
8481
  }
8456
- /** A skill the package ships, which `setup` and `update` copy under home. */
8457
- 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) {
8458
8497
  return {
8459
8498
  builtIns: {},
8460
8499
  rows: [{
8461
8500
  name: "skill",
8462
8501
  advisory: true,
8463
- run: (ctx) => findingOf$1(cli, folder, install, ctx)
8502
+ run: (ctx) => add === void 0 ? findingOf$1(cli, folder, install, ctx) : hubFindingOf(cli, add, ctx)
8464
8503
  }]
8465
8504
  };
8466
8505
  }
@@ -8533,7 +8572,7 @@ const setupSchema = object({
8533
8572
  version: string(),
8534
8573
  prefix: string(),
8535
8574
  installed: boolean(),
8536
- skill: homeSyncSchema.nullable(),
8575
+ skill: union([homeSyncSchema, object({ add: string() })]).nullable(),
8537
8576
  upgrades: array(upgradeSchema),
8538
8577
  docs: string().nullable()
8539
8578
  });
@@ -8593,18 +8632,28 @@ function withSetupLine(error, install) {
8593
8632
  next: [wholeLine(setupLineOf(install))]
8594
8633
  }, { cause: error });
8595
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
+ }
8596
8645
  function renderSetup(cli, install, setup) {
8597
- const { version, prefix, docs } = setup;
8646
+ const { version, prefix, docs, skill } = setup;
8598
8647
  return [
8599
8648
  setup.installed ? `installed ${install.package}@${version} with the npm under ${prefix}` : `${install.package}@${version} is already installed under ${prefix}`,
8600
- ...renderHomeSync(cli.name, setup.skill),
8649
+ ...skill !== null && "copies" in skill ? renderHomeSync(cli.name, skill) : [],
8601
8650
  ...setup.upgrades.map(renderUpgrade),
8602
- ...docs === null ? [] : [`docs ${docs}`, `Read that folder before using ${cli.name}.`]
8651
+ ...renderNext(cli.name, docs, skill)
8603
8652
  ].join("\n");
8604
8653
  }
8605
8654
  /** The built-in `setup`, which runs from any copy, such as npx's, so it never asks to be the global one. */
8606
8655
  function setupCommand(cli, install, launch) {
8607
- return defineCommand$1({
8656
+ return defineCommand$1.write({
8608
8657
  description: "install this CLI and its skill on this machine",
8609
8658
  examples: [{
8610
8659
  command: `${cli.name} setup`,
@@ -8613,7 +8662,6 @@ function setupCommand(cli, install, launch) {
8613
8662
  command: `${cli.name} setup --dry-run`,
8614
8663
  note: "show what it would change"
8615
8664
  }],
8616
- effect: "write",
8617
8665
  errors: [
8618
8666
  NOT_GLOBAL_INSTALL,
8619
8667
  NOT_ON_PATH,
@@ -8636,7 +8684,7 @@ function setupCommand(cli, install, launch) {
8636
8684
  latest: await latestVersion(npm),
8637
8685
  prefix,
8638
8686
  packageDir,
8639
- copies: cli.skill === void 0 ? [] : await homeCopiesOf(ctx.files, cli.name, ctx.env)
8687
+ copies: await syncedCopiesOf(cli, ctx)
8640
8688
  };
8641
8689
  },
8642
8690
  apply: async (plan, ctx) => {
@@ -8655,16 +8703,25 @@ function setupCommand(cli, install, launch) {
8655
8703
  version: installs || from === null ? latest : from,
8656
8704
  prefix,
8657
8705
  installed: installs,
8658
- skill,
8706
+ skill: cli.skill?.add === void 0 ? skill : { add: cli.skill.add },
8659
8707
  upgrades: installs && from !== null ? await upgradesOf(ctx.files, packageDir, from, latest) : [],
8660
8708
  docs: cli.docs === void 0 ? null : join(packageDir, cli.docs)
8661
8709
  };
8662
8710
  ctx.deliver(setup);
8663
- if (ctx.signal.aborted) throw new CliError({
8664
- code: INTERRUPTED,
8665
- message: `interrupted, but ${plan.package}@${setup.version} and its skill are already in place`,
8666
- hint: "nothing to run again"
8667
- });
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
+ }
8668
8725
  await assertOnPath(cli, prefix, launch, ctx);
8669
8726
  return setup;
8670
8727
  },
@@ -8705,7 +8762,7 @@ function interruptedAfterInstall(plan) {
8705
8762
  }
8706
8763
  /** The built-in `update`, for a CLI installed globally with npm. */
8707
8764
  function updateCommand(cli, install, launch) {
8708
- return defineCommand$1({
8765
+ return defineCommand$1.write({
8709
8766
  description: "install the latest version of this CLI from its registry",
8710
8767
  examples: [{
8711
8768
  command: `${cli.name} update`,
@@ -8714,7 +8771,6 @@ function updateCommand(cli, install, launch) {
8714
8771
  command: `${cli.name} update --dry-run`,
8715
8772
  note: "show the version it would install"
8716
8773
  }],
8717
- effect: "write",
8718
8774
  errors: [NOT_GLOBAL_INSTALL, ...NPM_ERRORS],
8719
8775
  output: updateSchema,
8720
8776
  plan: async (_input, ctx) => {
@@ -8726,7 +8782,7 @@ function updateCommand(cli, install, launch) {
8726
8782
  latest: await latestVersion(npm),
8727
8783
  prefix: copy.prefix,
8728
8784
  packageDir: copy.packageDir,
8729
- copies: cli.skill === void 0 ? [] : await homeCopiesOf(ctx.files, cli.name, ctx.env)
8785
+ copies: await syncedCopiesOf(cli, ctx)
8730
8786
  };
8731
8787
  },
8732
8788
  apply: async (plan, ctx) => {
@@ -9077,7 +9133,7 @@ function renderInstalled(cli, data) {
9077
9133
  }
9078
9134
  /** The built-in `skill install`, for a CLI whose skill ships in its package. */
9079
9135
  function skillInstallCommand(cli, folder, agents) {
9080
- return defineCommand$1({
9136
+ return defineCommand$1.write({
9081
9137
  description: agents === void 0 ? "copy this CLI's skill into the project" : "copy this CLI's skill into the project and write its Agents section",
9082
9138
  examples: [{
9083
9139
  command: `${cli.name} skill install`,
@@ -9086,7 +9142,6 @@ function skillInstallCommand(cli, folder, agents) {
9086
9142
  command: `${cli.name} skill install --dry-run`,
9087
9143
  note: "show what it would change"
9088
9144
  }],
9089
- effect: "write",
9090
9145
  output: installSchema,
9091
9146
  plan: (_input, ctx) => planOf(cli, folder, agents, ctx),
9092
9147
  apply: async (plan, ctx) => {
@@ -9428,7 +9483,7 @@ function checkedRows(error, problems, table) {
9428
9483
  }
9429
9484
  /** The built-in `doctor`: the fixed rows around the CLI's own, each under its own deadline. */
9430
9485
  function doctorCommand(cli, launch) {
9431
- const doctor = defineCommand$1({
9486
+ const doctor = defineCommand$1.read({
9432
9487
  description: "check the setup and say what fixes each problem",
9433
9488
  examples: [{
9434
9489
  command: `${cli.name} doctor`,
@@ -9437,7 +9492,6 @@ function doctorCommand(cli, launch) {
9437
9492
  command: `${cli.name} doctor --json`,
9438
9493
  note: "get the rows and fixes as data"
9439
9494
  }],
9440
- effect: "read",
9441
9495
  output: reportSchema,
9442
9496
  run: async (_input, ctx) => {
9443
9497
  const rowsScope = {
@@ -9629,12 +9683,17 @@ function checkTree(commands, path, table) {
9629
9683
  }
9630
9684
  function checkSkill({ name, skill, install }) {
9631
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`);
9632
9691
  }
9633
9692
  function checkDocs({ name, docs }) {
9634
9693
  if (docs === void 0) return;
9635
9694
  if (!(docs !== "" && !isAbsolute(docs) && !normalize(docs).startsWith(".."))) throw new Error(`${name} declares docs ${JSON.stringify(docs)}: name a folder inside its package, relative to the package root`);
9636
9695
  }
9637
- function checkCli(cli) {
9696
+ function checkDeclared(cli) {
9638
9697
  const table = errorTableOf(cli);
9639
9698
  checkDocs(cli);
9640
9699
  if (cli.commands === void 0) {
@@ -9707,16 +9766,17 @@ function resolved(cli) {
9707
9766
  }
9708
9767
  /**
9709
9768
  * Takes the CLI's ports factory, Project file and Error table once; the `defineCommand`,
9710
- * `defineCli` and `cliError` it returns are typed by them.
9769
+ * `defineDoctorCheck`, `defineCli` and `cliError` it returns are typed by them.
9711
9770
  */
9712
9771
  function agentCli(values = {}) {
9713
9772
  checkOwnCodes(errorTableOf(values));
9714
9773
  return {
9715
9774
  defineCommand: defineCommandFor(),
9716
- defineCli: (cli) => checkCli({
9775
+ defineCli: (cli) => checkDeclared({
9717
9776
  ...resolved(cli),
9718
9777
  ...values
9719
9778
  }),
9779
+ defineDoctorCheck: (row) => row,
9720
9780
  cliError: cliErrorOf
9721
9781
  };
9722
9782
  }
@@ -9725,17 +9785,25 @@ function agentCli(values = {}) {
9725
9785
  /** How long a run waits for its Side task once its output is written. */
9726
9786
  const SIDE_TASK_WAIT_MS = 1e3;
9727
9787
  const noSideTask = async () => {};
9788
+ const NO_SIDE_TASK = {
9789
+ end: () => {},
9790
+ finish: noSideTask
9791
+ };
9728
9792
  function waitOver() {
9729
9793
  return new DOMException(`the side task ran past its ${SIDE_TASK_WAIT_MS} ms wait`, "TimeoutError");
9730
9794
  }
9731
9795
  function startSideTask(sideTask, runWith, { clock, failed }) {
9732
- 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
+ });
9733
9801
  const controller = new AbortController();
9734
9802
  const { signal } = controller;
9735
- const running = (async () => sideTask(runWith(signal)))().catch((error) => {
9803
+ const running = (async () => sideTask(runWith(signal, ended)))().catch((error) => {
9736
9804
  if (!(signal.aborted && error === signal.reason)) failed(error);
9737
9805
  });
9738
- return async () => {
9806
+ const finish = async () => {
9739
9807
  const waited = new AbortController();
9740
9808
  const capped = new Promise((resolve) => after(clock, SIDE_TASK_WAIT_MS, waited.signal, () => {
9741
9809
  const over = waitOver();
@@ -9746,6 +9814,10 @@ function startSideTask(sideTask, runWith, { clock, failed }) {
9746
9814
  await Promise.race([running, capped]);
9747
9815
  waited.abort();
9748
9816
  };
9817
+ return {
9818
+ end,
9819
+ finish
9820
+ };
9749
9821
  }
9750
9822
  //#endregion
9751
9823
  //#region ../agent-cli/dist/command-line/suggest.js
@@ -10485,11 +10557,24 @@ function errorText({ leaves }, format) {
10485
10557
  const { usageHelp, json } = format;
10486
10558
  return usageHelp !== void 0 && !json && leaves.some((leaf) => leaf.error instanceof UsageError) ? `${text}\n${usageHelp}` : text;
10487
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
+ };
10488
10570
  function failedResult(failure, format) {
10489
10571
  return {
10490
- stdout: "",
10491
- stderr: errorText(failure, format),
10492
- exitCode: failure.exitCode
10572
+ result: {
10573
+ stdout: "",
10574
+ stderr: errorText(failure, format),
10575
+ exitCode: failure.exitCode
10576
+ },
10577
+ end: endOf(failure)
10493
10578
  };
10494
10579
  }
10495
10580
  function stdoutOf({ value, render }, format, spill) {
@@ -10504,10 +10589,13 @@ function startedOutput(outcome, format, spill) {
10504
10589
  }, spill);
10505
10590
  }
10506
10591
  /** A serve Command stopped, or ended itself, after it started: that is how it is meant to end. */
10507
- const STOPPED_RESULT = {
10508
- stdout: "",
10509
- stderr: "",
10510
- exitCode: SUCCESS_EXIT_CODE
10592
+ const STOPPED = {
10593
+ result: {
10594
+ stdout: "",
10595
+ stderr: "",
10596
+ exitCode: SUCCESS_EXIT_CODE
10597
+ },
10598
+ end: SUCCEEDED
10511
10599
  };
10512
10600
  async function errorResult(error, format) {
10513
10601
  return failedResult(await failureOf(error, format), format);
@@ -10515,9 +10603,12 @@ async function errorResult(error, format) {
10515
10603
  async function commandResult(settled, format, spill) {
10516
10604
  if (settled.ok) try {
10517
10605
  return {
10518
- stdout: await stdoutOf(settled.outcome, format, spill),
10519
- stderr: "",
10520
- 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
10521
10612
  };
10522
10613
  } catch (spillError) {
10523
10614
  return errorResult(spillError, format);
@@ -10531,16 +10622,26 @@ async function commandResult(settled, format, spill) {
10531
10622
  } catch (rejected) {
10532
10623
  return failedResult(withLeaves(failure, rejected, format), format);
10533
10624
  }
10534
- const result = failedResult(failure, format);
10625
+ const { result, end } = failedResult(failure, format);
10535
10626
  try {
10536
10627
  return {
10537
- ...result,
10538
- stdout: await stdoutOf(outcome, format, spill)
10628
+ result: {
10629
+ ...result,
10630
+ stdout: await stdoutOf(outcome, format, spill)
10631
+ },
10632
+ end
10539
10633
  };
10540
10634
  } catch (spillError) {
10635
+ const lost = withLeaves(failure, spillError, format);
10541
10636
  return {
10542
- ...result,
10543
- 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
+ }
10544
10645
  };
10545
10646
  }
10546
10647
  }
@@ -11483,13 +11584,35 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11483
11584
  json: routed.json,
11484
11585
  invocation
11485
11586
  };
11486
- const failed = async (error, finishSideTask) => {
11487
- 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));
11488
11590
  return {
11489
- result: await errorResult(error, format),
11490
- finishSideTask
11591
+ ...sideBase,
11592
+ command: routed.spec.path,
11593
+ dryRun,
11594
+ ended,
11595
+ get ports() {
11596
+ return sidePorts();
11597
+ }
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
11491
11610
  };
11492
11611
  };
11612
+ const failed = (error) => {
11613
+ control.close();
11614
+ return errorResult(error, format);
11615
+ };
11493
11616
  let parsed;
11494
11617
  try {
11495
11618
  checkOperator(routed.spec, env, rerun);
@@ -11501,27 +11624,11 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11501
11624
  });
11502
11625
  });
11503
11626
  } catch (error) {
11504
- return failed(error, noSideTask);
11627
+ return ran(await failed(error), sideTaskOf(false));
11505
11628
  }
11506
11629
  const { input, flags } = parsed;
11507
11630
  resumed = command.deadline?.resume(input, rerunOf);
11508
- const finishSideTask = startSideTask(builtIn ? void 0 : cli.sideTask?.bind(cli), (sideSignal) => {
11509
- const sideBase = scope.portsBase(sideSignal);
11510
- const sidePorts = once(() => scope.ports(sideBase));
11511
- return {
11512
- ...sideBase,
11513
- command: routed.spec.path,
11514
- dryRun: flags.dryRun,
11515
- get ports() {
11516
- return sidePorts();
11517
- }
11518
- };
11519
- }, {
11520
- clock,
11521
- failed: (error) => {
11522
- if (isDebug(baseFormat)) progress(`side task failed: ${traceOf(error)}`);
11523
- }
11524
- });
11631
+ const sideTask = sideTaskOf(flags.dryRun);
11525
11632
  let project;
11526
11633
  try {
11527
11634
  project = builtIn || cli.project === void 0 ? void 0 : projectOf(cli.project, cwd, {
@@ -11529,7 +11636,7 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11529
11636
  rerun
11530
11637
  });
11531
11638
  } catch (error) {
11532
- return failed(error, finishSideTask);
11639
+ return ran(await failed(error), sideTask);
11533
11640
  }
11534
11641
  let delivered;
11535
11642
  const ctx = contextOf(scope, control, {
@@ -11565,32 +11672,23 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11565
11672
  },
11566
11673
  outputs
11567
11674
  })));
11568
- if (served) return {
11569
- result: STOPPED_RESULT,
11570
- finishSideTask
11571
- };
11675
+ if (served) return ran(STOPPED, sideTask);
11572
11676
  settled = {
11573
11677
  ok: true,
11574
11678
  outcome
11575
11679
  };
11576
11680
  } catch (error) {
11577
- if (served && control.signal.aborted) return {
11578
- result: STOPPED_RESULT,
11579
- finishSideTask
11580
- };
11681
+ if (served && control.signal.aborted) return ran(STOPPED, sideTask);
11581
11682
  settled = {
11582
11683
  ok: false,
11583
11684
  error,
11584
11685
  delivered
11585
11686
  };
11586
11687
  }
11587
- return {
11588
- result: await commandResult(settled, {
11589
- ...format,
11590
- fields: flags.fields
11591
- }, spill),
11592
- finishSideTask
11593
- };
11688
+ return ran(await commandResult(settled, {
11689
+ ...format,
11690
+ fields: flags.fields
11691
+ }, spill), sideTask);
11594
11692
  }
11595
11693
  async function noticesOf(cli, launch, ctx) {
11596
11694
  await installOf(cli).notice(launch, ctx);
@@ -11678,10 +11776,10 @@ async function routeAndRun(cli, argv, options, write, stopNotice) {
11678
11776
  ...format,
11679
11777
  json: routed.json
11680
11778
  }));
11681
- case "usage error": return ranAlone(errorResult(routed.error, {
11779
+ case "usage error": return ranAlone((await errorResult(routed.error, {
11682
11780
  ...misused(routed.node),
11683
11781
  json: routed.json
11684
- }));
11782
+ })).result);
11685
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)));
11686
11784
  }
11687
11785
  }
@@ -11744,7 +11842,7 @@ async function runProcess(cli) {
11744
11842
  //#region package.json
11745
11843
  var package_default = {
11746
11844
  name: "@duyquangnvx/create-agent-cli",
11747
- version: "0.28.0",
11845
+ version: "0.30.0",
11748
11846
  description: "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
11749
11847
  type: "module",
11750
11848
  engines: { "node": ">=22.12.0" },
@@ -12031,13 +12129,12 @@ async function ensureFree(path) {
12031
12129
  //#region src/bin.ts
12032
12130
  await runProcess(defineCli({
12033
12131
  manifest: package_default,
12034
- command: defineCommand({
12132
+ command: defineCommand.write({
12035
12133
  description: "Create a new Agent CLI project in ./<name>",
12036
12134
  examples: [{
12037
12135
  command: "create-agent-cli weather --description \"Weather forecasts\"",
12038
12136
  note: "create ./weather, install dependencies and make the first commit"
12039
12137
  }],
12040
- effect: "write",
12041
12138
  errors: ["STEP_FAILED"],
12042
12139
  args: object({ name: string().regex(PACKAGE_NAME, "must be a lowercase npm package name, such as weather or @me/weather").describe("package name; its unscoped part names the bin and the directory") }),
12043
12140
  options: object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duyquangnvx/create-agent-cli",
3
- "version": "0.28.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.28.0",
24
+ "@duyquangnvx/agent-cli": "0.30.0",
25
25
  "tsdown": "0.23.0",
26
26
  "zod": "4.6.7"
27
27
  },
@@ -1,4 +1,4 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
2
+ "$schema": "https://biomejs.dev/schemas/2.6.2/schema.json",
3
3
  "extends": ["@duyquangnvx/harness/biome"]
4
4
  }
@@ -28,9 +28,9 @@
28
28
  "release": "node scripts/release.ts"
29
29
  },
30
30
  "devDependencies": {
31
- "@biomejs/biome": "2.5.15",
31
+ "@biomejs/biome": "2.6.2",
32
32
  "@duyquangnvx/agent-cli": "{{runtimeVersion}}",
33
- "@duyquangnvx/harness": "0.3.1",
33
+ "@duyquangnvx/harness": "0.5.2",
34
34
  "@duyquangnvx/release": "0.9.0",
35
35
  "@types/node": "^22.19.0",
36
36
  "tsdown": "0.23.0",
@@ -9,7 +9,7 @@ const { defineCli, defineCommand } = agentCli({ errors, ports });
9
9
  const FREE_LANGUAGE = "en";
10
10
  const PRICE_PER_CARD = "$0.01";
11
11
 
12
- const greet = defineCommand({
12
+ const greet = defineCommand.read({
13
13
  description: "Greet someone by name",
14
14
  examples: [
15
15
  {
@@ -21,14 +21,13 @@ const greet = defineCommand({
21
21
  note: "get the greeting as JSON",
22
22
  },
23
23
  ],
24
- effect: "read",
25
24
  args: z.object({
26
25
  name: z.string().min(1).describe("who to greet"),
27
26
  }),
28
27
  run: ({ name }) => ({ greeting: `Hello, ${name}!` }),
29
28
  });
30
29
 
31
- const card = defineCommand({
30
+ const card = defineCommand.write({
32
31
  description: "Write a greeting card to <name>.txt",
33
32
  help:
34
33
  `A card in English is free; one in another language costs ${PRICE_PER_CARD} and needs --yes. ` +
@@ -47,7 +46,6 @@ const card = defineCommand({
47
46
  note: "sign the card from Ben and Cy",
48
47
  },
49
48
  ],
50
- effect: "write",
51
49
  errors: ["LANGUAGE_UNSUPPORTED"],
52
50
  args: z.object({
53
51
  name: z
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { type Files, runCli } from "@duyquangnvx/agent-cli";
6
- import { checkExamples } from "@duyquangnvx/agent-cli/testing";
6
+ import { checkCli } from "@duyquangnvx/agent-cli/testing";
7
7
  import { describe, expect, test } from "vitest";
8
8
  import manifest from "../package.json" with { type: "json" };
9
9
  import { cli } from "../src/cli.ts";
@@ -74,8 +74,8 @@ function run(
74
74
  return runCli(cli, argv, { cwd: CWD, files, ports: { greetings }, stdin });
75
75
  }
76
76
 
77
- test("every example parses, names a Command and passes its schemas", async () => {
78
- await checkExamples(cli);
77
+ test("every example runs, every Command help shows has one, and every arg and option is described", async () => {
78
+ await checkCli(cli);
79
79
  });
80
80
 
81
81
  describe("greet", () => {