@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 +34 -0
- package/dist/bin.js +193 -96
- package/package.json +2 -2
- package/template/biome.json +1 -1
- package/template/package.json +2 -2
- package/template/src/cli.ts +2 -4
- package/template/test/cli.test.ts +3 -3
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
|
-
|
|
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
|
|
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
|
|
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": "
|
|
7746
|
+
"sha": "3c4069a",
|
|
7728
7747
|
"dirty": false,
|
|
7729
|
-
"builtAt": "2026-11-
|
|
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
|
|
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
|
-
|
|
8457
|
-
|
|
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,
|
|
8649
|
+
...skill !== null && "copies" in skill ? renderHomeSync(cli.name, skill) : [],
|
|
8601
8650
|
...setup.upgrades.map(renderUpgrade),
|
|
8602
|
-
...
|
|
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:
|
|
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)
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
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:
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
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
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
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
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
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
|
-
|
|
10538
|
-
|
|
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
|
-
|
|
10543
|
-
|
|
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
|
|
11487
|
-
|
|
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
|
-
|
|
11490
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
24
|
+
"@duyquangnvx/agent-cli": "0.30.0",
|
|
25
25
|
"tsdown": "0.23.0",
|
|
26
26
|
"zod": "4.6.7"
|
|
27
27
|
},
|
package/template/biome.json
CHANGED
package/template/package.json
CHANGED
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"release": "node scripts/release.ts"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@biomejs/biome": "2.
|
|
31
|
+
"@biomejs/biome": "2.6.2",
|
|
32
32
|
"@duyquangnvx/agent-cli": "{{runtimeVersion}}",
|
|
33
|
-
"@duyquangnvx/harness": "0.
|
|
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",
|
package/template/src/cli.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
78
|
-
await
|
|
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", () => {
|