@duyquangnvx/create-agent-cli 0.15.1 → 0.17.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 +17 -0
- package/dist/bin.js +47 -50
- package/package.json +2 -2
- package/template/AGENTS.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ What changed in each release of `@duyquangnvx/agent-cli` (the Runtime) and `@duy
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## v0.17.0 (2026-10-27)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- A fifth Effect, `act`, for a Command that changes the state of a program already running, such as a page in a browser tab, rather than files or remote services. It is declared as a read Command is, with `run` and the new `ActCommand` type, and cannot declare a Plan or `rows`. Its help says so and that running it again may act again, its `--help --json` gives `"effect": "act"`, and of the Standard flags it gets only `--json`. The agent-cli skill says when to pick `act` over `read` and `write`.
|
|
12
|
+
- The Scaffolder's `AGENTS.md` names the `act` and `serve` Effects beside `read`, `write` and `destructive`.
|
|
13
|
+
|
|
14
|
+
### Upgrade from v0.16.0
|
|
15
|
+
|
|
16
|
+
- Nothing to do.
|
|
17
|
+
|
|
18
|
+
## v0.16.0 (2026-10-27)
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Breaking:** a Packaged skill's `AGENTS.md` block is the text the CLI declares as `skill: { packaged, agents }`, written verbatim between its markers; the Runtime no longer composes a sentence from `description` and the run-as. Without `agents`, `skill install` writes no block and names no `CLAUDE.md` hand edit, its `--json` `agentsMd` is `null`, and `doctor` has no `agents` row. A CLI relying on the old block declares `agents` to keep one, and the Scaffolder's `--dependency` template declares none.
|
|
23
|
+
|
|
7
24
|
## v0.15.1 (2026-10-27)
|
|
8
25
|
|
|
9
26
|
### Fixed
|
package/dist/bin.js
CHANGED
|
@@ -6429,6 +6429,7 @@ const FORCE_FLAG = standardFlag("--force", {
|
|
|
6429
6429
|
});
|
|
6430
6430
|
const EFFECT_FLAGS = {
|
|
6431
6431
|
read: [JSON_FLAG],
|
|
6432
|
+
act: [JSON_FLAG],
|
|
6432
6433
|
write: [JSON_FLAG, DRY_RUN_FLAG],
|
|
6433
6434
|
destructive: [
|
|
6434
6435
|
JSON_FLAG,
|
|
@@ -7390,7 +7391,6 @@ function dependencyInstall(cli, install) {
|
|
|
7390
7391
|
runAs,
|
|
7391
7392
|
builtIns: () => ({}),
|
|
7392
7393
|
exitClasses: [],
|
|
7393
|
-
agentsCaveat: `and only from a package that has \`${install.package}\` in its dependencies; from anywhere else npx reports the package missing`,
|
|
7394
7394
|
check: (launch) => checkOf$1(cli, install, runAs, launch)
|
|
7395
7395
|
};
|
|
7396
7396
|
}
|
|
@@ -7492,12 +7492,6 @@ function hubSkill(cli, source) {
|
|
|
7492
7492
|
}
|
|
7493
7493
|
//#endregion
|
|
7494
7494
|
//#region ../agent-cli/dist/built-ins/agents-md.js
|
|
7495
|
-
/** One paragraph telling every session that the CLI exists, how to run it and which skill to load. */
|
|
7496
|
-
function agentsBlock(cli, { runAs, agentsCaveat }) {
|
|
7497
|
-
const what = cli.description.replace(/\.$/, "");
|
|
7498
|
-
const run = `Run it as \`${runAs}\`${agentsCaveat === void 0 ? "" : `, ${agentsCaveat}`}`;
|
|
7499
|
-
return `\`${cli.name}\`: ${what}. ${run}. Load the \`${cli.name}\` skill before using it.`;
|
|
7500
|
-
}
|
|
7501
7495
|
function markersOf(name) {
|
|
7502
7496
|
return {
|
|
7503
7497
|
begin: `<!-- BEGIN:${name} -->`,
|
|
@@ -7505,33 +7499,32 @@ function markersOf(name) {
|
|
|
7505
7499
|
};
|
|
7506
7500
|
}
|
|
7507
7501
|
/** `text` with the CLI's block between its markers, appended when they are missing; nothing else changes. */
|
|
7508
|
-
function withBlock(
|
|
7509
|
-
const { begin, end } = markersOf(
|
|
7510
|
-
const whole = `${begin}\n${
|
|
7502
|
+
function withBlock({ name, text: block }, text) {
|
|
7503
|
+
const { begin, end } = markersOf(name);
|
|
7504
|
+
const whole = `${begin}\n${block}\n${end}`;
|
|
7511
7505
|
const start = text.indexOf(begin);
|
|
7512
7506
|
const stop = start === -1 ? -1 : text.indexOf(end, start);
|
|
7513
7507
|
if (stop !== -1) return `${text.slice(0, start)}${whole}${text.slice(stop + end.length)}`;
|
|
7514
7508
|
if (text === "") return `${whole}\n`;
|
|
7515
7509
|
return `${text}${text.endsWith("\n") ? "" : "\n"}\n${whole}\n`;
|
|
7516
7510
|
}
|
|
7517
|
-
async function agentsMdAt(
|
|
7511
|
+
async function agentsMdAt(agents, root) {
|
|
7518
7512
|
const path = join(root, "AGENTS.md");
|
|
7519
7513
|
const existing = await orMissing(readFile(path, "utf8"));
|
|
7520
|
-
const block = agentsBlock(cli, install);
|
|
7521
7514
|
if (existing === void 0) return {
|
|
7522
7515
|
path,
|
|
7523
|
-
|
|
7516
|
+
agents,
|
|
7524
7517
|
change: "create"
|
|
7525
7518
|
};
|
|
7526
|
-
if (withBlock(
|
|
7519
|
+
if (withBlock(agents, existing) === existing) return {
|
|
7527
7520
|
path,
|
|
7528
|
-
|
|
7521
|
+
agents,
|
|
7529
7522
|
change: null
|
|
7530
7523
|
};
|
|
7531
7524
|
return {
|
|
7532
7525
|
path,
|
|
7533
|
-
|
|
7534
|
-
change: existing.includes(markersOf(
|
|
7526
|
+
agents,
|
|
7527
|
+
change: existing.includes(markersOf(agents.name).begin) ? "rewrite" : "add"
|
|
7535
7528
|
};
|
|
7536
7529
|
}
|
|
7537
7530
|
async function handEditsAt(root) {
|
|
@@ -7540,25 +7533,26 @@ async function handEditsAt(root) {
|
|
|
7540
7533
|
if (text === void 0 || text.split(/\r?\n/).some((line) => line.trim() === "@AGENTS.md")) return [];
|
|
7541
7534
|
return [`add a line \`@AGENTS.md\` to ${claudeMd}, so Claude Code reads AGENTS.md`];
|
|
7542
7535
|
}
|
|
7543
|
-
function agentsRow(
|
|
7536
|
+
function agentsRow(agents, install) {
|
|
7537
|
+
const { name } = agents;
|
|
7544
7538
|
return {
|
|
7545
7539
|
name: "agents",
|
|
7546
7540
|
advisory: true,
|
|
7547
7541
|
run: async (ctx) => {
|
|
7548
7542
|
const root = await projectRootOf(ctx.cwd);
|
|
7549
|
-
const { path, change } = await agentsMdAt(
|
|
7543
|
+
const { path, change } = await agentsMdAt(agents, root);
|
|
7550
7544
|
const fix = `${install.runAs} skill install`;
|
|
7551
7545
|
if (change === "rewrite") return {
|
|
7552
7546
|
state: "problem",
|
|
7553
|
-
message: `${path} holds a ${
|
|
7547
|
+
message: `${path} holds a ${name} block other than the one ${fix} writes`,
|
|
7554
7548
|
fix
|
|
7555
7549
|
};
|
|
7556
7550
|
if (change !== null) return {
|
|
7557
7551
|
state: "problem",
|
|
7558
|
-
message: `${path} holds no ${
|
|
7552
|
+
message: `${path} holds no ${name} block, so sessions in this repo do not know ${name}`,
|
|
7559
7553
|
fix
|
|
7560
7554
|
};
|
|
7561
|
-
const holds = `${path} holds the ${
|
|
7555
|
+
const holds = `${path} holds the ${name} block`;
|
|
7562
7556
|
const [edit] = await handEditsAt(root);
|
|
7563
7557
|
return edit === void 0 ? {
|
|
7564
7558
|
state: "ok",
|
|
@@ -7936,14 +7930,14 @@ const installSchema = object({
|
|
|
7936
7930
|
agentsMd: object({
|
|
7937
7931
|
path: string(),
|
|
7938
7932
|
changed: boolean()
|
|
7939
|
-
}),
|
|
7933
|
+
}).nullable(),
|
|
7940
7934
|
handEdits: array(string())
|
|
7941
7935
|
});
|
|
7942
7936
|
async function sameContent(left, right) {
|
|
7943
7937
|
const [a, b] = await Promise.all([readFile(left), readFile(right)]);
|
|
7944
7938
|
return a.equals(b);
|
|
7945
7939
|
}
|
|
7946
|
-
async function planOf(cli, folder,
|
|
7940
|
+
async function planOf(cli, folder, agents, cwd, packageRoot) {
|
|
7947
7941
|
const source = join(packageRoot, folder);
|
|
7948
7942
|
const { root, copy } = await skillCopyOf(cli, cwd);
|
|
7949
7943
|
const [shipped, present] = await Promise.all([filesIn(source), filesIn(copy)]);
|
|
@@ -7955,8 +7949,8 @@ async function planOf(cli, folder, install, cwd, packageRoot) {
|
|
|
7955
7949
|
replace: shipped.filter((_file, index) => differs[index]),
|
|
7956
7950
|
remove: present.filter((file) => !shipped.includes(file)),
|
|
7957
7951
|
sameVersion: (await skillVersionsOf(source, copy)).same,
|
|
7958
|
-
agentsMd: await agentsMdAt(
|
|
7959
|
-
handEdits: await handEditsAt(root)
|
|
7952
|
+
agentsMd: agents === void 0 ? null : await agentsMdAt(agents, root),
|
|
7953
|
+
handEdits: agents === void 0 ? [] : await handEditsAt(root)
|
|
7960
7954
|
};
|
|
7961
7955
|
}
|
|
7962
7956
|
const BLOCK_VERBS = {
|
|
@@ -7970,11 +7964,11 @@ function renderPlan(cli, plan) {
|
|
|
7970
7964
|
...plan.replace.map((file) => ` replace ${file}`),
|
|
7971
7965
|
...plan.remove.map((file) => ` remove ${file}`)
|
|
7972
7966
|
];
|
|
7973
|
-
const {
|
|
7967
|
+
const { agentsMd } = plan;
|
|
7974
7968
|
return [
|
|
7975
7969
|
files.length === 0 ? `the ${cli.name} skill at ${plan.copy} already matches the package` : `would copy the ${cli.name} skill to ${plan.copy}:`,
|
|
7976
7970
|
...files,
|
|
7977
|
-
change === null ? `${path} already holds the ${cli.name} block` : `would ${BLOCK_VERBS[change]} ${path}:\n${
|
|
7971
|
+
...agentsMd === null ? [] : [agentsMd.change === null ? `${agentsMd.path} already holds the ${cli.name} block` : `would ${BLOCK_VERBS[agentsMd.change]} ${agentsMd.path}:\n${agentsMd.agents.text}`],
|
|
7978
7972
|
...plan.handEdits.map((edit) => `by hand: ${edit}`)
|
|
7979
7973
|
].join("\n");
|
|
7980
7974
|
}
|
|
@@ -7982,7 +7976,7 @@ function renderInstalled(cli, data) {
|
|
|
7982
7976
|
const { written, removed, agentsMd } = data;
|
|
7983
7977
|
return [
|
|
7984
7978
|
written.length + removed.length === 0 ? `the ${cli.name} skill at ${data.copy} already matches the package` : `copied the ${cli.name} skill to ${data.copy}: wrote ${written.length} files, removed ${removed.length}`,
|
|
7985
|
-
agentsMd.changed ? `wrote the ${cli.name} block in ${agentsMd.path}` : `${agentsMd.path} already holds the ${cli.name} block
|
|
7979
|
+
...agentsMd === null ? [] : [agentsMd.changed ? `wrote the ${cli.name} block in ${agentsMd.path}` : `${agentsMd.path} already holds the ${cli.name} block`],
|
|
7986
7980
|
...data.handEdits.map((edit) => `by hand: ${edit}`)
|
|
7987
7981
|
].join("\n");
|
|
7988
7982
|
}
|
|
@@ -7995,11 +7989,11 @@ function editedRemovals(plan, ctx) {
|
|
|
7995
7989
|
});
|
|
7996
7990
|
}
|
|
7997
7991
|
/** The built-in `skill install`, for a CLI whose skill ships in its package. */
|
|
7998
|
-
function skillInstallCommand(cli, folder, install) {
|
|
7992
|
+
function skillInstallCommand(cli, folder, install, agents) {
|
|
7999
7993
|
const { runAs } = install;
|
|
8000
7994
|
return defineCommand$1({
|
|
8001
7995
|
name: "install",
|
|
8002
|
-
description: "copy this CLI's skill into the project and write its AGENTS.md block",
|
|
7996
|
+
description: agents === void 0 ? "copy this CLI's skill into the project" : "copy this CLI's skill into the project and write its AGENTS.md block",
|
|
8003
7997
|
examples: [{
|
|
8004
7998
|
command: `${runAs} skill install`,
|
|
8005
7999
|
note: "install the skill this version ships"
|
|
@@ -8010,24 +8004,24 @@ function skillInstallCommand(cli, folder, install) {
|
|
|
8010
8004
|
effect: "write",
|
|
8011
8005
|
output: installSchema,
|
|
8012
8006
|
force: "replace or remove hand-edited files of a skill copy at the package's version",
|
|
8013
|
-
plan: (_input, ctx) => planOf(cli, folder,
|
|
8007
|
+
plan: (_input, ctx) => planOf(cli, folder, agents, ctx.cwd, ctx.packageRoot),
|
|
8014
8008
|
outputs: (plan) => plan.sameVersion ? [...plan.write, ...plan.replace].map((file) => join(plan.copy, file)) : [],
|
|
8015
8009
|
apply: async (plan, ctx) => {
|
|
8016
8010
|
if (plan.sameVersion && plan.remove.length > 0 && !ctx.force) throw editedRemovals(plan, ctx);
|
|
8017
8011
|
for (const file of [...plan.write, ...plan.replace]) await ctx.files.write(join(plan.copy, file), await readFile(join(plan.source, file)));
|
|
8018
8012
|
for (const file of plan.remove) await ctx.files.remove(join(plan.copy, file));
|
|
8019
|
-
const {
|
|
8020
|
-
if (change !== null) {
|
|
8021
|
-
const existing = await orMissing(readFile(path, "utf8")) ?? "";
|
|
8022
|
-
await ctx.files.write(path, withBlock(
|
|
8013
|
+
const { agentsMd } = plan;
|
|
8014
|
+
if (agentsMd !== null && agentsMd.change !== null) {
|
|
8015
|
+
const existing = await orMissing(readFile(agentsMd.path, "utf8")) ?? "";
|
|
8016
|
+
await ctx.files.write(agentsMd.path, withBlock(agentsMd.agents, existing));
|
|
8023
8017
|
}
|
|
8024
8018
|
return {
|
|
8025
8019
|
copy: plan.copy,
|
|
8026
8020
|
written: [...plan.write, ...plan.replace].sort(),
|
|
8027
8021
|
removed: [...plan.remove],
|
|
8028
|
-
agentsMd: {
|
|
8029
|
-
path,
|
|
8030
|
-
changed: change !== null
|
|
8022
|
+
agentsMd: agentsMd === null ? null : {
|
|
8023
|
+
path: agentsMd.path,
|
|
8024
|
+
changed: agentsMd.change !== null
|
|
8031
8025
|
},
|
|
8032
8026
|
handEdits: [...plan.handEdits]
|
|
8033
8027
|
};
|
|
@@ -8102,16 +8096,20 @@ async function notice(cli, folder, install, { builtIn, bin }, ctx) {
|
|
|
8102
8096
|
if (finding.state !== "problem") return;
|
|
8103
8097
|
ctx.notice(finding.fix === void 0 ? finding.message : `${finding.message}; run \`${finding.fix}\``);
|
|
8104
8098
|
}
|
|
8105
|
-
/** A skill shipped in the package at `
|
|
8106
|
-
function packagedSkill(cli, folder, install) {
|
|
8099
|
+
/** A skill shipped in the package at `packaged`, which `skill install` copies to the project root. */
|
|
8100
|
+
function packagedSkill(cli, { packaged: folder, agents: text }, install) {
|
|
8101
|
+
const agents = text === void 0 ? void 0 : {
|
|
8102
|
+
name: cli.name,
|
|
8103
|
+
text
|
|
8104
|
+
};
|
|
8107
8105
|
return {
|
|
8108
8106
|
kind: "packaged",
|
|
8109
|
-
builtIns: { skill: { install: skillInstallCommand(cli, folder, install) } },
|
|
8107
|
+
builtIns: { skill: { install: skillInstallCommand(cli, folder, install, agents) } },
|
|
8110
8108
|
rows: [{
|
|
8111
8109
|
name: "skill",
|
|
8112
8110
|
advisory: true,
|
|
8113
8111
|
run: (ctx) => findingOf(cli, folder, install, ctx)
|
|
8114
|
-
}, agentsRow(
|
|
8112
|
+
}, ...agents === void 0 ? [] : [agentsRow(agents, install)]],
|
|
8115
8113
|
notice: (launch, ctx) => notice(cli, folder, install, launch, ctx),
|
|
8116
8114
|
findingAt: (_version, ctx) => findingOf(cli, folder, install, ctx)
|
|
8117
8115
|
};
|
|
@@ -8121,7 +8119,7 @@ function packagedSkill(cli, folder, install) {
|
|
|
8121
8119
|
/** A Packaged skill, or else a Skill Hub's, also for a CLI that declares no `skill`. */
|
|
8122
8120
|
function skillOf(cli, install) {
|
|
8123
8121
|
const { skill } = cli;
|
|
8124
|
-
return skill !== void 0 && "packaged" in skill ? packagedSkill(cli, skill
|
|
8122
|
+
return skill !== void 0 && "packaged" in skill ? packagedSkill(cli, skill, install) : hubSkill(cli, skill?.source);
|
|
8125
8123
|
}
|
|
8126
8124
|
//#endregion
|
|
8127
8125
|
//#region ../agent-cli/dist/interrupts.js
|
|
@@ -8509,7 +8507,6 @@ function globalInstall(cli, install) {
|
|
|
8509
8507
|
runAs: runAsOf(cli),
|
|
8510
8508
|
builtIns: (launch) => ({ update: updateCommand(cli, install, skillOf(cli, adapter), launch) }),
|
|
8511
8509
|
exitClasses: ["source"],
|
|
8512
|
-
agentsCaveat: void 0,
|
|
8513
8510
|
check: (launch) => checkOf(cli, install, launch)
|
|
8514
8511
|
};
|
|
8515
8512
|
return adapter;
|
|
@@ -8525,7 +8522,6 @@ function noInstall(cli) {
|
|
|
8525
8522
|
runAs: runAsOf(cli),
|
|
8526
8523
|
builtIns: () => ({}),
|
|
8527
8524
|
exitClasses: [],
|
|
8528
|
-
agentsCaveat: void 0,
|
|
8529
8525
|
check: () => ({
|
|
8530
8526
|
install: skipped,
|
|
8531
8527
|
release: async () => ({ skipped: await skipped() })
|
|
@@ -9120,6 +9116,7 @@ function groupHelp(group, style) {
|
|
|
9120
9116
|
}
|
|
9121
9117
|
const EFFECT_NOTES = {
|
|
9122
9118
|
read: "changes nothing",
|
|
9119
|
+
act: "changes the state of a running program, not files or services; may act again each time it runs",
|
|
9123
9120
|
write: "changes something; show the Plan with --dry-run",
|
|
9124
9121
|
destructive: "changes something hard to undo; show the Plan with --dry-run, run with --confirm",
|
|
9125
9122
|
serve: "changes nothing; prints its data at once, then runs until Ctrl-C, which exits 0"
|
|
@@ -9509,7 +9506,7 @@ async function execute(command, input, ctx, { dryRun, confirm, yes, page, invoca
|
|
|
9509
9506
|
...input,
|
|
9510
9507
|
...page
|
|
9511
9508
|
}, ctx), (data, style) => command.render(data, style));
|
|
9512
|
-
if (command.effect === "read") return outcomeOf(await command.run(input, ctx), (data, style) => command.render(data, style));
|
|
9509
|
+
if (command.effect === "read" || command.effect === "act") return outcomeOf(await command.run(input, ctx), (data, style) => command.render(data, style));
|
|
9513
9510
|
if (command.effect === "serve") {
|
|
9514
9511
|
let end = () => {};
|
|
9515
9512
|
const ended = new Promise((resolve) => {
|
|
@@ -10258,14 +10255,14 @@ async function runProcess(cli) {
|
|
|
10258
10255
|
}
|
|
10259
10256
|
//#endregion
|
|
10260
10257
|
//#region package.json
|
|
10261
|
-
var version$1 = "0.
|
|
10258
|
+
var version$1 = "0.17.0";
|
|
10262
10259
|
var description = "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.";
|
|
10263
10260
|
//#endregion
|
|
10264
10261
|
//#region src/build.ts
|
|
10265
10262
|
const build = {
|
|
10266
|
-
"sha": "
|
|
10263
|
+
"sha": "8bb3938",
|
|
10267
10264
|
"dirty": false,
|
|
10268
|
-
"builtAt": "2026-10-
|
|
10265
|
+
"builtAt": "2026-10-27T10:54:44.000Z"
|
|
10269
10266
|
};
|
|
10270
10267
|
//#endregion
|
|
10271
10268
|
//#region src/fill.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duyquangnvx/create-agent-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@duyquangnvx/agent-cli": "0.
|
|
23
|
+
"@duyquangnvx/agent-cli": "0.17.0",
|
|
24
24
|
"tsdown": "0.23.0",
|
|
25
25
|
"zod": "4.6.7"
|
|
26
26
|
},
|
package/template/AGENTS.md
CHANGED
|
@@ -19,7 +19,7 @@ Greenfield with no deployments or users: change code directly. Breaking changes
|
|
|
19
19
|
## Coding standards
|
|
20
20
|
|
|
21
21
|
- Every Command is declared in `src/cli.ts` with the `defineCommand` that `agentCli<Ports>()` returns: a description, realistic examples, Zod schemas for args and options (with `.describe()` on each field), and its Effect.
|
|
22
|
-
- A Command never prints. A `read` Command returns data from `run` and turns it into short text in `render`. A `write` or `destructive` Command returns a Plan from `plan`, with no side effects, and performs it in `apply`; the Runtime adds `--dry-run`, and `--confirm` for a destructive one.
|
|
22
|
+
- A Command never prints. A `read` Command returns data from `run` and turns it into short text in `render`. An `act` Command, which changes the state of a program already running rather than files or services, does the same with no Plan. A `write` or `destructive` Command returns a Plan from `plan`, with no side effects, and performs it in `apply`; the Runtime adds `--dry-run`, and `--confirm` for a destructive one. A `serve` Command returns data from `start` and keeps running until `ctx.signal` aborts.
|
|
23
23
|
- A Command that writes files names them in `outputs(plan)`, and the Runtime refuses to overwrite one without `--force`. A Command that costs money states it in `cost(plan)`, a sentence or `undefined` when cheap, and the Runtime refuses a costly run without `--yes`.
|
|
24
24
|
- A Command reaches a remote service through a port: an interface in `src/ports.ts`, built by the `ports` factory passed to `defineCli` and read as `ctx.ports`. It writes files through `ctx.files` and runs programs through `ctx.spawn`, so a test replaces each of them.
|
|
25
25
|
- Every error code is declared once in a `defineErrors` table passed to `defineCli({ errors })`, with the Exit class that must act on it and an optional `details` schema, and thrown through `cliError(errors, code, { message, hint })`, whose `hint` names the command to run next. An undeclared code, and anything else thrown, becomes an internal error (exit 70), whose stack trace `{{debugVariable}}=1` shows.
|