@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 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 label = `<${key}>`;
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
- ...describe(schema),
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 ? [FORCE_FLAG] : [],
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
- function permissionHint({ path, callerLine, env }) {
6373
- return isAgent(env) ? `your sandbox may block it: ask your user to run \`! ${callerLine}\`, or to allow ${path} in the sandbox's write list` : `check the permissions of ${path} and its folder`;
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
- const planned = command.effect === "write" || command.effect === "destructive";
7625
+ if (!hasPlan(command)) return {
7626
+ outputs: false,
7627
+ force: void 0,
7628
+ cost: false
7629
+ };
7580
7630
  return {
7581
- outputs: planned && command.outputs !== void 0,
7582
- cost: planned && command.cost !== void 0
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, force, page, invocation, commandLine, started }) {
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
- return raw === void 0 ? [` ${field.label}: required, ${howToPass(field)}`] : [` ${field.label} ${JSON.stringify(raw)}: ${issue.message}`];
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
- ...operands.slice(argFields.length).filter((operand) => !operand.afterUnknownOption).map((operand) => ` unexpected argument ${JSON.stringify(operand.value)}`),
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
- return parseEnv(readFileSync(path, "utf8"));
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, base, callerLine);
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 ctx = {
8238
- ...base,
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(cli.name, options.env, name)
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": "411548a",
8501
+ "sha": "a7760d0",
8382
8502
  "dirty": false,
8383
- "builtAt": "2026-10-19T08:42:00.085Z"
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.3.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.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.3.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
  }