@duyquangnvx/create-agent-cli 0.3.0 → 0.4.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 +22 -0
- package/dist/bin.js +185 -65
- package/package.json +2 -2
- package/template/test/cli.test.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ What changed in each release of `@duyquangnvx/agent-cli` (the Runtime) and `@duy
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## v0.4.0 (2026-10-19)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- The last field of a Command's `args` may be a `z.array(...)`, optionally wrapped in optional or default, which takes every remaining operand: `tool preview a b c` gives `dirs: ["a", "b", "c"]`. None gives an empty array for the schema to judge, or leaves an optional field out so its default applies. Each operand is coerced like a scalar arg of the element's type, and an issue on one names it and its position, as in `<values...> "x" (argument 3): ...`. Text help shows the field as `<name...>` (`[name...]` when optional), and `--help --json` gives it the array schema. `defineCommand` throws when an array field is not the last of `args`.
|
|
12
|
+
- The ports factory receives `PortsBase`, the run's base context: `env`, `cwd`, `packageRoot`, `secret`, `spawn`, `files`, `clock` and `signal`, the same classified, cwd-resolved helpers a Command gets, so an adapter reads a secret or the package root as a Command does. Under `runCli` it sees the test's `env`, `cwd`, `clock`, `files` and `spawn`, and the `ports` option still replaces its result. It runs when a run first reads `ctx.ports`, at most once, and what it throws exits by its Exit class like a Command error; a run that never reads them, such as `doctor` or `update`, never calls it, and a `doctor` row that reads them reports the error as its problem. A factory written as `() => Ports` still type-checks; the type `PortsBase` is exported.
|
|
13
|
+
- A write or destructive Command may declare `force: "<what it replaces>"`, which gives it the Standard flag `--force` with that sentence as the flag's description in help and help JSON. A Command gets `--force` from `outputs`, `force` or both; with `outputs`, `--force` still lifts `OUTPUT_EXISTS`, and `outputs` alone keeps the generic description. `ctx.force` is true only when the caller passed `--force` to a Command that has it, and `plan` sees it under `--dry-run` too. `defineCommand` throws when a read or serve Command declares `force`, or when the sentence is empty.
|
|
14
|
+
- `ctx.files.read(path)` returns a file's bytes as a `Uint8Array`; `ctx.files.list(folder)` returns the names in a folder, sorted, never the Runtime's temp files; `ctx.files.remove(path)` removes a file or a folder with everything in it, and succeeds when the path does not exist; `ctx.files.mkdir(path, { exclusive? })` creates a folder and its missing parents, and with `exclusive: true` fails when the folder exists. Each resolves its path against `ctx.cwd` and fails as `write` does: `PERMISSION_DENIED` (exit 2, with the sandbox hint under a Coding agent), `DISK_FULL` (exit 2) or `NOT_FOUND` (exit 1), naming the path. The type `MkdirOptions` is exported.
|
|
15
|
+
- The Runtime code `ALREADY_EXISTS` (exit 1), raised for EEXIST, such as an exclusive `mkdir` of an existing folder, naming the path, with a hint to pass another path or remove that one. `ALREADY_EXISTS`, `PERMISSION_DENIED`, `DISK_FULL` and `NOT_FOUND` are exported as constants, so a Command catches one with `error instanceof CliError && error.code === ALREADY_EXISTS`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Breaking:** `doctor` exits 0 when only its `install` or `registry` row is a problem (not a global install, a newer version on the registry, or the registry unreachable). Those rows still show `problem` with their fix, in text and `--json`, and the fix stays under `next`.
|
|
20
|
+
- **Breaking:** the `Files` interface gains `read`, `list`, `remove` and `mkdir`, so every fake passed as `runCli`'s `files` option must implement them. The scaffolded template's fake and the skill's example implement them.
|
|
21
|
+
- **Breaking:** `ctx.files.write` creates missing parent folders, so a write into a missing folder no longer fails with `NOT_FOUND`.
|
|
22
|
+
- The agent-cli skill teaches every 0.4.0 feature: the ports factory reading `PortsBase`, array positionals, `force` with `ctx.force`, and `ctx.files`' `read`, `list`, `remove` and `mkdir` with a caught `ALREADY_EXISTS`. Its example `links` gains `bundle <folder> <ids...>`, which writes shortcut files into a new folder and replaces an existing one under `--force`, a `web` port that `links check` uses and whose factory reads `LINKS_TOKEN`, and reads `links.json` through `ctx.files`.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Under a Coding agent, a `PERMISSION_DENIED` on a read (`ctx.files.read`, `list` or `exists`, an unreadable `.env`, a tool `ctx.spawn` cannot run) asks the user to allow reading the path in the sandbox, not to add it to the sandbox's write list.
|
|
27
|
+
- A `$XDG_CONFIG_HOME/<cli>/.env` the Runtime cannot read makes `ctx.secret` fail with `PERMISSION_DENIED` (exit 2) naming the file, with the sandbox hint under an agent, instead of exit 70. A missing `.env` still means no secrets.
|
|
28
|
+
|
|
7
29
|
## v0.3.0 (2026-10-19)
|
|
8
30
|
|
|
9
31
|
### Added
|
package/dist/bin.js
CHANGED
|
@@ -5603,21 +5603,29 @@ function describe(schema) {
|
|
|
5603
5603
|
current = current.unwrap();
|
|
5604
5604
|
description ??= current.description;
|
|
5605
5605
|
}
|
|
5606
|
+
const variadic = current instanceof ZodArray;
|
|
5607
|
+
if (current instanceof ZodArray) current = current.element;
|
|
5606
5608
|
return {
|
|
5607
5609
|
type: typeOf(current),
|
|
5608
5610
|
required,
|
|
5609
5611
|
defaultValue,
|
|
5610
5612
|
description,
|
|
5611
|
-
choices: current instanceof ZodEnum ? current.options.map(String) : void 0
|
|
5613
|
+
choices: current instanceof ZodEnum ? current.options.map(String) : void 0,
|
|
5614
|
+
variadic
|
|
5612
5615
|
};
|
|
5613
5616
|
}
|
|
5617
|
+
function argLabel(key, { variadic, required }) {
|
|
5618
|
+
if (!variadic) return `<${key}>`;
|
|
5619
|
+
return required ? `<${key}...>` : `[${key}...]`;
|
|
5620
|
+
}
|
|
5614
5621
|
function argField(key, schema) {
|
|
5615
|
-
const
|
|
5622
|
+
const described = describe(schema);
|
|
5623
|
+
const label = argLabel(key, described);
|
|
5616
5624
|
return {
|
|
5617
5625
|
key,
|
|
5618
5626
|
name: key,
|
|
5619
5627
|
schema,
|
|
5620
|
-
...
|
|
5628
|
+
...described,
|
|
5621
5629
|
label,
|
|
5622
5630
|
usage: label
|
|
5623
5631
|
};
|
|
@@ -5643,6 +5651,14 @@ function fieldsOf$1(schema, toField) {
|
|
|
5643
5651
|
function argFields(schema) {
|
|
5644
5652
|
return fieldsOf$1(schema, argField);
|
|
5645
5653
|
}
|
|
5654
|
+
/** Throws when an arg that takes every remaining operand is not the last arg. */
|
|
5655
|
+
function checkArrayArgLast(name, args) {
|
|
5656
|
+
const misplaced = argFields(args).slice(0, -1).filter((field) => field.variadic);
|
|
5657
|
+
if (misplaced.length > 0) {
|
|
5658
|
+
const keys = misplaced.map(({ key }) => `'${key}'`).join(", ");
|
|
5659
|
+
throw new Error(`Command '${name}' declares array arg ${keys} before its last arg; only the last arg may be an array`);
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5646
5662
|
function optionFields(schema) {
|
|
5647
5663
|
return fieldsOf$1(schema, optionField);
|
|
5648
5664
|
}
|
|
@@ -5935,6 +5951,12 @@ function pageFooter({ items, next }) {
|
|
|
5935
5951
|
return `${items.length} shown; ${more}\n`;
|
|
5936
5952
|
}
|
|
5937
5953
|
//#endregion
|
|
5954
|
+
//#region ../agent-cli/dist/planned.js
|
|
5955
|
+
/** Whether `command` changes something through a Plan: a write or destructive Command. */
|
|
5956
|
+
function hasPlan(command) {
|
|
5957
|
+
return command.effect === "write" || command.effect === "destructive";
|
|
5958
|
+
}
|
|
5959
|
+
//#endregion
|
|
5938
5960
|
//#region ../agent-cli/dist/flags.js
|
|
5939
5961
|
const HELP_FLAG = "--help";
|
|
5940
5962
|
const VERSION_FLAG = "--version";
|
|
@@ -5997,10 +6019,14 @@ const RESERVED = new Map([
|
|
|
5997
6019
|
...LIST_FLAGS
|
|
5998
6020
|
].map((flag) => [nameOf(flag), flag]));
|
|
5999
6021
|
/** The Standard flags of a Command with this Effect and declarations, and of a List Command. */
|
|
6000
|
-
function standardFlagsFor(effect, { outputs, cost }, list) {
|
|
6022
|
+
function standardFlagsFor(effect, { outputs, force, cost }, list) {
|
|
6023
|
+
const forceFlag = force === void 0 ? FORCE_FLAG : {
|
|
6024
|
+
...FORCE_FLAG,
|
|
6025
|
+
description: force
|
|
6026
|
+
};
|
|
6001
6027
|
return [
|
|
6002
6028
|
...EFFECT_FLAGS[effect],
|
|
6003
|
-
...outputs ? [
|
|
6029
|
+
...outputs || force !== void 0 ? [forceFlag] : [],
|
|
6004
6030
|
...cost ? [YES_FLAG] : [],
|
|
6005
6031
|
...list?.standardFlags ?? []
|
|
6006
6032
|
];
|
|
@@ -6019,10 +6045,19 @@ function checkNoStandardFlags(command) {
|
|
|
6019
6045
|
});
|
|
6020
6046
|
if (lines.length > 0) throw new Error([`Command '${command.name}' declares Standard flags the Runtime adds; rename them:`, ...lines].join("\n"));
|
|
6021
6047
|
}
|
|
6048
|
+
/** Throws when `force` is declared on a Command with no Plan, or says nothing. */
|
|
6049
|
+
function checkForce(command) {
|
|
6050
|
+
const force = "force" in command ? command.force : void 0;
|
|
6051
|
+
if (force === void 0) return;
|
|
6052
|
+
if (!hasPlan(command)) throw new Error(`Command '${command.name}' declares force, which only a write or destructive Command may`);
|
|
6053
|
+
if (force.trim() === "") throw new Error(`Command '${command.name}' declares an empty force; say what ${FORCE_FLAG.flag} replaces`);
|
|
6054
|
+
}
|
|
6022
6055
|
//#endregion
|
|
6023
6056
|
//#region ../agent-cli/dist/define.js
|
|
6024
6057
|
function checkedCommand(command) {
|
|
6025
6058
|
checkNoStandardFlags(command);
|
|
6059
|
+
checkForce(command);
|
|
6060
|
+
checkArrayArgLast(command.name, command.args);
|
|
6026
6061
|
return command;
|
|
6027
6062
|
}
|
|
6028
6063
|
function defineCommandFor() {
|
|
@@ -6369,8 +6404,10 @@ function isAgent(env) {
|
|
|
6369
6404
|
const PERMISSION_DENIED = "PERMISSION_DENIED";
|
|
6370
6405
|
const DISK_FULL = "DISK_FULL";
|
|
6371
6406
|
const NOT_FOUND = "NOT_FOUND";
|
|
6372
|
-
|
|
6373
|
-
|
|
6407
|
+
const ALREADY_EXISTS = "ALREADY_EXISTS";
|
|
6408
|
+
function permissionHint({ path, access, callerLine, env }) {
|
|
6409
|
+
if (!isAgent(env)) return `check the permissions of ${path} and its folder`;
|
|
6410
|
+
return `your sandbox may block it: ask your user to run \`! ${callerLine}\`, or ${access === "read" ? `to allow reading ${path} in the sandbox` : `to allow ${path} in the sandbox's write list`}`;
|
|
6374
6411
|
}
|
|
6375
6412
|
/** The error a file system failure on `path` stands for; any other error comes back unchanged. */
|
|
6376
6413
|
function classifyErrno(error, context) {
|
|
@@ -6392,6 +6429,11 @@ function classifyErrno(error, context) {
|
|
|
6392
6429
|
message: `not found: ${path}`,
|
|
6393
6430
|
hint: `check that ${path} and its folder exist`
|
|
6394
6431
|
}, { cause: error });
|
|
6432
|
+
case "EEXIST": return new CliError({
|
|
6433
|
+
code: ALREADY_EXISTS,
|
|
6434
|
+
message: `already exists: ${path}`,
|
|
6435
|
+
hint: `pass another path, or remove ${path}`
|
|
6436
|
+
}, { cause: error });
|
|
6395
6437
|
default: return error;
|
|
6396
6438
|
}
|
|
6397
6439
|
}
|
|
@@ -6740,6 +6782,7 @@ function contextSpawn(run, defaults, callerLine) {
|
|
|
6740
6782
|
if (isMissingFile(error)) throw missingError(command, error);
|
|
6741
6783
|
throw classifyErrno(error, {
|
|
6742
6784
|
path: command,
|
|
6785
|
+
access: "read",
|
|
6743
6786
|
callerLine,
|
|
6744
6787
|
env: defaults.env
|
|
6745
6788
|
});
|
|
@@ -6778,6 +6821,7 @@ function runtimeCodes() {
|
|
|
6778
6821
|
[PERMISSION_DENIED]: "operator",
|
|
6779
6822
|
[DISK_FULL]: "operator",
|
|
6780
6823
|
[NOT_FOUND]: "caller",
|
|
6824
|
+
[ALREADY_EXISTS]: "caller",
|
|
6781
6825
|
[TOOL_FAILED]: "operator",
|
|
6782
6826
|
[TOOL_MISSING]: "operator",
|
|
6783
6827
|
[PROJECT_INVALID]: "operator",
|
|
@@ -7219,6 +7263,7 @@ function projectRow(projectFile) {
|
|
|
7219
7263
|
function installRow(cli, bin) {
|
|
7220
7264
|
return {
|
|
7221
7265
|
name: "install",
|
|
7266
|
+
advisory: true,
|
|
7222
7267
|
run: async (ctx) => {
|
|
7223
7268
|
const { install } = cli;
|
|
7224
7269
|
if (install === void 0) return ok(`${cli.name} declares no install, so it has no \`update\``);
|
|
@@ -7238,6 +7283,7 @@ function installRow(cli, bin) {
|
|
|
7238
7283
|
function registryRow(cli) {
|
|
7239
7284
|
return {
|
|
7240
7285
|
name: "registry",
|
|
7286
|
+
advisory: true,
|
|
7241
7287
|
run: async (ctx) => {
|
|
7242
7288
|
const { install } = cli;
|
|
7243
7289
|
if (install === void 0) return ok(`${cli.name} declares no registry to compare this build with`);
|
|
@@ -7325,15 +7371,15 @@ function problemOf(check, error, { signal, deadline, ms }) {
|
|
|
7325
7371
|
message: `the ${check.name} row hit a bug: ${messageOf(error)}`
|
|
7326
7372
|
};
|
|
7327
7373
|
}
|
|
7374
|
+
function withSignal(ctx, signal) {
|
|
7375
|
+
return Object.create(ctx, { signal: { value: signal } });
|
|
7376
|
+
}
|
|
7328
7377
|
async function rowOf(check, ctx) {
|
|
7329
7378
|
const ms = check.deadlineMs ?? DEFAULT_DEADLINE_MS;
|
|
7330
7379
|
const signal = ctx.deadline(ms);
|
|
7331
7380
|
let finding;
|
|
7332
7381
|
try {
|
|
7333
|
-
finding = await Promise.race([Promise.resolve().then(() => check.run(
|
|
7334
|
-
...ctx,
|
|
7335
|
-
signal
|
|
7336
|
-
})), untilAborted(signal)]);
|
|
7382
|
+
finding = await Promise.race([Promise.resolve().then(() => check.run(withSignal(ctx, signal))), untilAborted(signal)]);
|
|
7337
7383
|
} catch (error) {
|
|
7338
7384
|
finding = problemOf(check, error, {
|
|
7339
7385
|
signal: ctx.signal,
|
|
@@ -7576,10 +7622,15 @@ function cliError(_table, code, init, options) {
|
|
|
7576
7622
|
//#endregion
|
|
7577
7623
|
//#region ../agent-cli/dist/command-spec.js
|
|
7578
7624
|
function declaredOf(command) {
|
|
7579
|
-
|
|
7625
|
+
if (!hasPlan(command)) return {
|
|
7626
|
+
outputs: false,
|
|
7627
|
+
force: void 0,
|
|
7628
|
+
cost: false
|
|
7629
|
+
};
|
|
7580
7630
|
return {
|
|
7581
|
-
outputs:
|
|
7582
|
-
|
|
7631
|
+
outputs: command.outputs !== void 0,
|
|
7632
|
+
force: command.force,
|
|
7633
|
+
cost: command.cost !== void 0
|
|
7583
7634
|
};
|
|
7584
7635
|
}
|
|
7585
7636
|
function commandSpec(commandLine, command) {
|
|
@@ -7641,7 +7692,7 @@ function dryRunOutcome(command, plan) {
|
|
|
7641
7692
|
].join("\n")
|
|
7642
7693
|
};
|
|
7643
7694
|
}
|
|
7644
|
-
async function execute(command, input, ctx, { dryRun, confirm, yes,
|
|
7695
|
+
async function execute(command, input, ctx, { dryRun, confirm, yes, page, invocation, commandLine, started }) {
|
|
7645
7696
|
await checkTools(command.tools ?? [], ctx.env);
|
|
7646
7697
|
if (command.rows !== void 0) return outcomeOf(await command.run({
|
|
7647
7698
|
...input,
|
|
@@ -7657,7 +7708,7 @@ async function execute(command, input, ctx, { dryRun, confirm, yes, force, page,
|
|
|
7657
7708
|
await checkOutputs({
|
|
7658
7709
|
invocation,
|
|
7659
7710
|
commandLine,
|
|
7660
|
-
force,
|
|
7711
|
+
force: ctx.force,
|
|
7661
7712
|
files: ctx.files
|
|
7662
7713
|
}, command.outputs?.(plan));
|
|
7663
7714
|
checkCost({
|
|
@@ -7708,6 +7759,7 @@ function realFiles() {
|
|
|
7708
7759
|
},
|
|
7709
7760
|
write: async (path, data) => {
|
|
7710
7761
|
const folder = dirname(path);
|
|
7762
|
+
await mkdir(folder, { recursive: true });
|
|
7711
7763
|
if (!swept.has(folder)) {
|
|
7712
7764
|
await sweep(folder);
|
|
7713
7765
|
swept.add(folder);
|
|
@@ -7720,9 +7772,50 @@ function realFiles() {
|
|
|
7720
7772
|
await rm(temp, { force: true });
|
|
7721
7773
|
throw error;
|
|
7722
7774
|
}
|
|
7775
|
+
},
|
|
7776
|
+
read: (path) => readFile(path),
|
|
7777
|
+
list: (folder) => readdir(folder),
|
|
7778
|
+
remove: (path) => rm(path, {
|
|
7779
|
+
recursive: true,
|
|
7780
|
+
force: true
|
|
7781
|
+
}),
|
|
7782
|
+
mkdir: async (path, options) => {
|
|
7783
|
+
if (options?.exclusive !== true) {
|
|
7784
|
+
await mkdir(path, { recursive: true });
|
|
7785
|
+
return;
|
|
7786
|
+
}
|
|
7787
|
+
await mkdir(dirname(path), { recursive: true });
|
|
7788
|
+
await mkdir(path);
|
|
7723
7789
|
}
|
|
7724
7790
|
};
|
|
7725
7791
|
}
|
|
7792
|
+
/**
|
|
7793
|
+
* `files` resolving paths against `cwd`, its failures classified by errno; `list` drops the
|
|
7794
|
+
* Runtime's temp files and sorts.
|
|
7795
|
+
*/
|
|
7796
|
+
function classifiedFiles(files, { cwd, env, callerLine }) {
|
|
7797
|
+
async function at(access, path, work) {
|
|
7798
|
+
const target = resolve(cwd, path);
|
|
7799
|
+
try {
|
|
7800
|
+
return await work(target);
|
|
7801
|
+
} catch (error) {
|
|
7802
|
+
throw classifyErrno(error, {
|
|
7803
|
+
path: target,
|
|
7804
|
+
access,
|
|
7805
|
+
callerLine,
|
|
7806
|
+
env
|
|
7807
|
+
});
|
|
7808
|
+
}
|
|
7809
|
+
}
|
|
7810
|
+
return {
|
|
7811
|
+
exists: (path) => at("read", path, (target) => files.exists(target)),
|
|
7812
|
+
write: (path, data) => at("write", path, (target) => files.write(target, data)),
|
|
7813
|
+
read: (path) => at("read", path, (target) => files.read(target)),
|
|
7814
|
+
list: (folder) => at("read", folder, async (target) => (await files.list(target)).filter((name) => !TEMP_FILE.test(name)).sort()),
|
|
7815
|
+
remove: (path) => at("write", path, (target) => files.remove(target)),
|
|
7816
|
+
mkdir: (path, options) => at("write", path, (target) => files.mkdir(target, options))
|
|
7817
|
+
};
|
|
7818
|
+
}
|
|
7726
7819
|
//#endregion
|
|
7727
7820
|
//#region ../agent-cli/dist/package-root.js
|
|
7728
7821
|
/**
|
|
@@ -7893,10 +7986,14 @@ function optionValue(field, raw) {
|
|
|
7893
7986
|
return field.type === "boolean" && raw === true && field.label === `--no-${field.name}` ? false : raw;
|
|
7894
7987
|
}
|
|
7895
7988
|
function converted(field, raw) {
|
|
7989
|
+
if (Array.isArray(raw)) return raw.map((element) => converted(field, element));
|
|
7896
7990
|
if (field.type !== "number" || typeof raw !== "string" || raw.trim() === "") return raw;
|
|
7897
7991
|
const number = Number(raw);
|
|
7898
7992
|
return Number.isNaN(number) ? raw : number;
|
|
7899
7993
|
}
|
|
7994
|
+
function variadicValue(field, values) {
|
|
7995
|
+
return values.length === 0 && !field.required ? void 0 : values;
|
|
7996
|
+
}
|
|
7900
7997
|
function parse(schema, values) {
|
|
7901
7998
|
const input = Object.fromEntries([...values].filter(([, raw]) => raw !== void 0).map(([field, raw]) => [field.key, converted(field, raw)]));
|
|
7902
7999
|
return schema.safeParse(input);
|
|
@@ -7908,9 +8005,15 @@ function issueLines(result, values, howToPass, reported) {
|
|
|
7908
8005
|
if (field === void 0) return [` ${issue.message}`];
|
|
7909
8006
|
if (reported.includes(field)) return [];
|
|
7910
8007
|
const raw = values.get(field);
|
|
7911
|
-
|
|
8008
|
+
if (raw === void 0) return [` ${field.label}: required, ${howToPass(field)}`];
|
|
8009
|
+
return Array.isArray(raw) ? [operandsIssueLine(field, raw, issue, [...values.keys()].indexOf(field) + 1)] : [` ${field.label} ${JSON.stringify(raw)}: ${issue.message}`];
|
|
7912
8010
|
});
|
|
7913
8011
|
}
|
|
8012
|
+
function operandsIssueLine(field, raw, issue, first) {
|
|
8013
|
+
const [, element] = issue.path;
|
|
8014
|
+
if (typeof element === "number") return ` ${field.label} ${JSON.stringify(raw[element])} (argument ${first + element}): ${issue.message}`;
|
|
8015
|
+
return raw.length === 0 ? ` ${field.label}: ${issue.message}, pass them as arguments from ${first}` : ` ${field.label} ${JSON.stringify(raw)}: ${issue.message}`;
|
|
8016
|
+
}
|
|
7914
8017
|
function unknownLine({ flag, suggestion }) {
|
|
7915
8018
|
return ` unknown option '${flag}'${suggestion === void 0 ? "" : ` (did you mean ${suggestion}?)`}`;
|
|
7916
8019
|
}
|
|
@@ -7934,7 +8037,8 @@ function parseInput(spec, words, invocation) {
|
|
|
7934
8037
|
const { operands, unknownOptions, values } = tokenize(words, known);
|
|
7935
8038
|
if (values.get(CONFIRM_FLAG.flag) === true) throw invalidInputError(spec.commandLine, `option '${CONFIRM_FLAG.usage}' argument missing`);
|
|
7936
8039
|
const missing = [...optionFields, ...pageFields].filter((field) => field.type !== "boolean" && values.get(field.label) === true);
|
|
7937
|
-
const argValues = new Map(argFields.map((field, index) => [field, operands[index]?.value]));
|
|
8040
|
+
const argValues = new Map(argFields.map((field, index) => [field, field.variadic ? variadicValue(field, operands.slice(index).filter((operand) => !operand.afterUnknownOption).map((operand) => operand.value)) : operands[index]?.value]));
|
|
8041
|
+
const extraOperands = argFields.some((field) => field.variadic) ? [] : operands.slice(argFields.length);
|
|
7938
8042
|
const valuesOf = (fields) => new Map(fields.map((field) => [field, missing.includes(field) ? void 0 : optionValue(field, values.get(field.label))]));
|
|
7939
8043
|
const optionValues = valuesOf(optionFields);
|
|
7940
8044
|
const pageValues = valuesOf(pageFields);
|
|
@@ -7944,7 +8048,7 @@ function parseInput(spec, words, invocation) {
|
|
|
7944
8048
|
const unknown = unknownOptionsOf(unknownOptions, candidateFlags(known));
|
|
7945
8049
|
const otherLines = [
|
|
7946
8050
|
...missing.map((field) => ` ${field.label}: missing value, pass ${field.usage}`),
|
|
7947
|
-
...
|
|
8051
|
+
...extraOperands.filter((operand) => !operand.afterUnknownOption).map((operand) => ` unexpected argument ${JSON.stringify(operand.value)}`),
|
|
7948
8052
|
...issueLines(args, argValues, (field) => `pass it as argument ${argFields.indexOf(field) + 1}`, []),
|
|
7949
8053
|
...issueLines(options, optionValues, (field) => `pass ${field.usage}`, missing),
|
|
7950
8054
|
...issueLines(page, pageValues, (field) => `pass ${field.usage}`, missing)
|
|
@@ -8059,23 +8163,30 @@ function dotenvPath(cliName, env) {
|
|
|
8059
8163
|
const config = env.XDG_CONFIG_HOME || join(env.HOME || homedir(), ".config");
|
|
8060
8164
|
return join(config, cliName, ".env");
|
|
8061
8165
|
}
|
|
8062
|
-
function readDotenv(path) {
|
|
8166
|
+
function readDotenv(path, env, callerLine) {
|
|
8167
|
+
let text;
|
|
8063
8168
|
try {
|
|
8064
|
-
|
|
8169
|
+
text = readFileSync(path, "utf8");
|
|
8065
8170
|
} catch (error) {
|
|
8066
8171
|
if (isMissingFile(error)) return {};
|
|
8067
|
-
throw error
|
|
8172
|
+
throw classifyErrno(error, {
|
|
8173
|
+
path,
|
|
8174
|
+
access: "read",
|
|
8175
|
+
callerLine,
|
|
8176
|
+
env
|
|
8177
|
+
});
|
|
8068
8178
|
}
|
|
8179
|
+
return parseEnv(text);
|
|
8069
8180
|
}
|
|
8070
8181
|
/** Looks `name` up in the env, then in `$XDG_CONFIG_HOME/<cli>/.env`; an empty value counts as unset. */
|
|
8071
|
-
function secretOf(cliName, env, name) {
|
|
8182
|
+
function secretOf({ cliName, env, callerLine }, name) {
|
|
8072
8183
|
const fromEnv = env[name];
|
|
8073
8184
|
if (fromEnv) return {
|
|
8074
8185
|
value: fromEnv,
|
|
8075
8186
|
origin: { kind: "env" }
|
|
8076
8187
|
};
|
|
8077
8188
|
const path = dotenvPath(cliName, env);
|
|
8078
|
-
const fromFile = readDotenv(path)[name];
|
|
8189
|
+
const fromFile = readDotenv(path, env, callerLine)[name];
|
|
8079
8190
|
return fromFile ? {
|
|
8080
8191
|
value: fromFile,
|
|
8081
8192
|
origin: {
|
|
@@ -8111,6 +8222,7 @@ async function prepare(folder, cutoff, target) {
|
|
|
8111
8222
|
} catch (error) {
|
|
8112
8223
|
throw classifyErrno(error, {
|
|
8113
8224
|
path: folder,
|
|
8225
|
+
access: "write",
|
|
8114
8226
|
callerLine: target.callerLine,
|
|
8115
8227
|
env: target.env
|
|
8116
8228
|
});
|
|
@@ -8168,35 +8280,6 @@ function styleFor(env, stdoutIsTTY) {
|
|
|
8168
8280
|
}
|
|
8169
8281
|
//#endregion
|
|
8170
8282
|
//#region ../agent-cli/dist/run-cli.js
|
|
8171
|
-
/** `files` resolving paths against `cwd`, its failures classified by errno. */
|
|
8172
|
-
function classifiedFiles(files, base, callerLine) {
|
|
8173
|
-
return {
|
|
8174
|
-
exists: async (path) => {
|
|
8175
|
-
const target = resolve(base.cwd, path);
|
|
8176
|
-
try {
|
|
8177
|
-
return await files.exists(target);
|
|
8178
|
-
} catch (error) {
|
|
8179
|
-
throw classifyErrno(error, {
|
|
8180
|
-
path: target,
|
|
8181
|
-
callerLine,
|
|
8182
|
-
env: base.env
|
|
8183
|
-
});
|
|
8184
|
-
}
|
|
8185
|
-
},
|
|
8186
|
-
write: async (path, data) => {
|
|
8187
|
-
const target = resolve(base.cwd, path);
|
|
8188
|
-
try {
|
|
8189
|
-
await files.write(target, data);
|
|
8190
|
-
} catch (error) {
|
|
8191
|
-
throw classifyErrno(error, {
|
|
8192
|
-
path: target,
|
|
8193
|
-
callerLine,
|
|
8194
|
-
env: base.env
|
|
8195
|
-
});
|
|
8196
|
-
}
|
|
8197
|
-
}
|
|
8198
|
-
};
|
|
8199
|
-
}
|
|
8200
8283
|
async function runCommand(routed, { base, writeStdout, signal, format: baseFormat, ports, clock, files, spawn, projectFile }) {
|
|
8201
8284
|
const { command, commandLine } = routed.spec;
|
|
8202
8285
|
const format = {
|
|
@@ -8225,7 +8308,11 @@ async function runCommand(routed, { base, writeStdout, signal, format: baseForma
|
|
|
8225
8308
|
});
|
|
8226
8309
|
let delivered;
|
|
8227
8310
|
const callerLine = commandLineOf(invocation);
|
|
8228
|
-
const classified = classifiedFiles(files,
|
|
8311
|
+
const classified = classifiedFiles(files, {
|
|
8312
|
+
cwd: base.cwd,
|
|
8313
|
+
env: base.env,
|
|
8314
|
+
callerLine
|
|
8315
|
+
});
|
|
8229
8316
|
const spill = spillTo({
|
|
8230
8317
|
cliName: format.cliName,
|
|
8231
8318
|
commandName: command.name,
|
|
@@ -8234,20 +8321,31 @@ async function runCommand(routed, { base, writeStdout, signal, format: baseForma
|
|
|
8234
8321
|
files: classified,
|
|
8235
8322
|
callerLine
|
|
8236
8323
|
});
|
|
8237
|
-
const
|
|
8238
|
-
|
|
8324
|
+
const portsBase = {
|
|
8325
|
+
cwd: base.cwd,
|
|
8326
|
+
env: base.env,
|
|
8327
|
+
packageRoot: base.packageRoot,
|
|
8328
|
+
secret: base.secret,
|
|
8239
8329
|
signal: control.signal,
|
|
8240
|
-
deadline: (ms) => control.deadline(ms),
|
|
8241
|
-
runToEnd: (name, work) => control.runToEnd(name, work),
|
|
8242
|
-
ports: ports(),
|
|
8243
8330
|
clock,
|
|
8244
8331
|
files: classified,
|
|
8245
8332
|
spawn: contextSpawn(spawn, {
|
|
8246
8333
|
cwd: base.cwd,
|
|
8247
8334
|
env: base.env,
|
|
8248
8335
|
signal: control.signal
|
|
8249
|
-
}, callerLine)
|
|
8336
|
+
}, callerLine)
|
|
8337
|
+
};
|
|
8338
|
+
const builtPorts = once(() => ports(portsBase));
|
|
8339
|
+
const ctx = {
|
|
8340
|
+
...base,
|
|
8341
|
+
...portsBase,
|
|
8342
|
+
deadline: (ms) => control.deadline(ms),
|
|
8343
|
+
runToEnd: (name, work) => control.runToEnd(name, work),
|
|
8344
|
+
get ports() {
|
|
8345
|
+
return builtPorts();
|
|
8346
|
+
},
|
|
8250
8347
|
project,
|
|
8348
|
+
force: flags.force,
|
|
8251
8349
|
poll: (check, { intervalMs }) => poll(clock, check, intervalMs, control.signal),
|
|
8252
8350
|
deliver: (data) => {
|
|
8253
8351
|
delivered = deliveredOutcome(command, data);
|
|
@@ -8262,7 +8360,6 @@ async function runCommand(routed, { base, writeStdout, signal, format: baseForma
|
|
|
8262
8360
|
dryRun: flags.dryRun,
|
|
8263
8361
|
confirm: flags.confirm,
|
|
8264
8362
|
yes: flags.yes,
|
|
8265
|
-
force: flags.force,
|
|
8266
8363
|
page: flags.page,
|
|
8267
8364
|
invocation,
|
|
8268
8365
|
commandLine,
|
|
@@ -8285,6 +8382,25 @@ async function runCommand(routed, { base, writeStdout, signal, format: baseForma
|
|
|
8285
8382
|
fields: flags.fields
|
|
8286
8383
|
}, spill);
|
|
8287
8384
|
}
|
|
8385
|
+
/** `build` runs on the first call; later calls give its value or throw its error again. */
|
|
8386
|
+
function once(build) {
|
|
8387
|
+
let outcome;
|
|
8388
|
+
return () => {
|
|
8389
|
+
if (outcome === void 0) try {
|
|
8390
|
+
outcome = {
|
|
8391
|
+
ok: true,
|
|
8392
|
+
value: build()
|
|
8393
|
+
};
|
|
8394
|
+
} catch (error) {
|
|
8395
|
+
outcome = {
|
|
8396
|
+
ok: false,
|
|
8397
|
+
error
|
|
8398
|
+
};
|
|
8399
|
+
}
|
|
8400
|
+
if (outcome.ok) return outcome.value;
|
|
8401
|
+
throw outcome.error;
|
|
8402
|
+
};
|
|
8403
|
+
}
|
|
8288
8404
|
function portsOf(cli, replacement) {
|
|
8289
8405
|
if (replacement !== void 0) return () => replacement;
|
|
8290
8406
|
return cli.ports ?? (() => ({}));
|
|
@@ -8328,7 +8444,11 @@ async function runWithProgress(cli, argv, options, write) {
|
|
|
8328
8444
|
env: options.env,
|
|
8329
8445
|
packageRoot: packageRootOf(options.bin, options.cwd),
|
|
8330
8446
|
progress: (message) => write.stderr(`${message}\n`),
|
|
8331
|
-
secret: (name) => secretOf(
|
|
8447
|
+
secret: (name) => secretOf({
|
|
8448
|
+
cliName: cli.name,
|
|
8449
|
+
env: options.env,
|
|
8450
|
+
callerLine: commandLineOf(format.invocation)
|
|
8451
|
+
}, name)
|
|
8332
8452
|
},
|
|
8333
8453
|
writeStdout: (text) => write.stdout(text),
|
|
8334
8454
|
signal: options.signal,
|
|
@@ -8378,9 +8498,9 @@ async function runProcess(cli) {
|
|
|
8378
8498
|
//#endregion
|
|
8379
8499
|
//#region src/build.ts
|
|
8380
8500
|
const build = {
|
|
8381
|
-
"sha": "
|
|
8501
|
+
"sha": "a7760d0",
|
|
8382
8502
|
"dirty": false,
|
|
8383
|
-
"builtAt": "2026-10-
|
|
8503
|
+
"builtAt": "2026-10-19T10:55:24.637Z"
|
|
8384
8504
|
};
|
|
8385
8505
|
//#endregion
|
|
8386
8506
|
//#region src/steps.ts
|
|
@@ -8509,7 +8629,7 @@ async function writeTemplate(root, files, values) {
|
|
|
8509
8629
|
}
|
|
8510
8630
|
//#endregion
|
|
8511
8631
|
//#region src/version.ts
|
|
8512
|
-
const version = "0.
|
|
8632
|
+
const version = "0.4.0";
|
|
8513
8633
|
//#endregion
|
|
8514
8634
|
//#region src/create.ts
|
|
8515
8635
|
const { defineCommand } = agentCli();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duyquangnvx/create-agent-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@duyquangnvx/agent-cli": "0.
|
|
22
|
+
"@duyquangnvx/agent-cli": "0.4.0",
|
|
23
23
|
"tsdown": "0.23.0",
|
|
24
24
|
"zod": "4.6.7"
|
|
25
25
|
},
|
|
@@ -14,6 +14,18 @@ function fakeFiles(existing: readonly string[] = []) {
|
|
|
14
14
|
write: async (path, data) => {
|
|
15
15
|
written.set(path, String(data));
|
|
16
16
|
},
|
|
17
|
+
read: async (path) => {
|
|
18
|
+
const text = written.get(path);
|
|
19
|
+
if (text === undefined) {
|
|
20
|
+
throw Object.assign(new Error(`ENOENT: no such file, open '${path}'`), { code: "ENOENT" });
|
|
21
|
+
}
|
|
22
|
+
return new TextEncoder().encode(text);
|
|
23
|
+
},
|
|
24
|
+
list: async () => [],
|
|
25
|
+
remove: async (path) => {
|
|
26
|
+
written.delete(path);
|
|
27
|
+
},
|
|
28
|
+
mkdir: async () => {},
|
|
17
29
|
};
|
|
18
30
|
return { files, written };
|
|
19
31
|
}
|