@duyquangnvx/create-agent-cli 0.13.1 → 0.14.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/dist/bin.js CHANGED
@@ -5803,6 +5803,15 @@ function errnoOf(error) {
5803
5803
  function isMissingFile(error) {
5804
5804
  return errnoOf(error) === "ENOENT";
5805
5805
  }
5806
+ /** What `pending` resolves with, or undefined when it fails because a file is missing. */
5807
+ async function orMissing(pending) {
5808
+ try {
5809
+ return await pending;
5810
+ } catch (error) {
5811
+ if (isMissingFile(error)) return void 0;
5812
+ throw error;
5813
+ }
5814
+ }
5806
5815
  function isNothingThere(error) {
5807
5816
  return isMissingFile(error) || errnoOf(error) === "ENOTDIR";
5808
5817
  }
@@ -6142,8 +6151,8 @@ const SHELL_SAFE = /^[\w@%+=:,./-]+$/;
6142
6151
  function quoted$1(token) {
6143
6152
  return SHELL_SAFE.test(token) ? token : `'${token.replaceAll("'", `'\\''`)}'`;
6144
6153
  }
6145
- function pasteable(cliName, tokens, stdinArg) {
6146
- const line = [cliName, ...tokens].map(quoted$1).join(" ");
6154
+ function pasteable(runAs, tokens, stdinArg) {
6155
+ const line = [runAs, ...tokens.map(quoted$1)].join(" ");
6147
6156
  return stdinArg === void 0 ? line : `${line} < <${stdinArg} file>`;
6148
6157
  }
6149
6158
  function split(argv) {
@@ -6173,10 +6182,10 @@ function without(flag, takes, tokens) {
6173
6182
  * The caller's command line as typed, ready to paste, with each flag in `remove` dropped along with
6174
6183
  * its value, the words in `add` placed before any `--`, and the stdin arg's file redirected in.
6175
6184
  */
6176
- function commandLineOf({ cliName, argv, flags, stdinArg }, { add = [], remove = [] } = {}) {
6185
+ function commandLineOf({ runAs, argv, flags, stdinArg }, { add = [], remove = [] } = {}) {
6177
6186
  const { options, operands } = split(argv);
6178
6187
  const takes = new Map(flags.map((known) => [known.flag, known.takes]));
6179
- return pasteable(cliName, [
6188
+ return pasteable(runAs, [
6180
6189
  ...remove.reduce((tokens, flag) => without(flag, takes.get(flag) ?? "nothing", tokens), options),
6181
6190
  ...add,
6182
6191
  ...operands
@@ -6189,9 +6198,9 @@ function renamed(token, renames) {
6189
6198
  return value.length === 0 ? name : `${name}=${value.join("=")}`;
6190
6199
  }
6191
6200
  /** The caller's command line, ready to paste, with each flag in `renames` typed as its new name. */
6192
- function commandLineRenaming({ cliName, argv, stdinArg }, renames) {
6201
+ function commandLineRenaming({ runAs, argv, stdinArg }, renames) {
6193
6202
  const { options, operands } = split(argv);
6194
- return pasteable(cliName, [...options.map((token) => renamed(token, renames)), ...operands], stdinArg);
6203
+ return pasteable(runAs, [...options.map((token) => renamed(token, renames)), ...operands], stdinArg);
6195
6204
  }
6196
6205
  //#endregion
6197
6206
  //#region ../agent-cli/dist/usage.js
@@ -6478,6 +6487,15 @@ function isCommand(node) {
6478
6487
  return typeof node.effect === "string";
6479
6488
  }
6480
6489
  //#endregion
6490
+ //#region ../agent-cli/dist/run-as.js
6491
+ /**
6492
+ * The Run-as: how every command line the Runtime gives its caller starts. A Dependency install runs
6493
+ * the copy the project pins, so it is `npx -q --no-install <bin>`; otherwise the bare name (ADR 0011).
6494
+ */
6495
+ function runAsOf(cli) {
6496
+ return cli.install?.mode === "dependency" ? `npx -q --no-install ${cli.name}` : cli.name;
6497
+ }
6498
+ //#endregion
6481
6499
  //#region ../agent-cli/dist/project.js
6482
6500
  const PROJECT_INVALID = "PROJECT_INVALID";
6483
6501
  function invalid(path, problem, callerLine, options) {
@@ -6717,11 +6735,7 @@ async function feed(stdin, input, signal) {
6717
6735
  */
6718
6736
  async function spawnChild(command, args, { cwd, env, signal, onLine, stdin, detached = false }) {
6719
6737
  signal.throwIfAborted();
6720
- const launch = process.platform === "win32" ? await windowsLaunch(command, args, env) : {
6721
- command,
6722
- args,
6723
- shell: false
6724
- };
6738
+ const launch = await launchOf(command, args, env);
6725
6739
  signal.throwIfAborted();
6726
6740
  return new Promise((resolve, reject) => {
6727
6741
  const child = spawn(launch.command, launch.args, {
@@ -6786,6 +6800,38 @@ async function spawnChild(command, args, { cwd, env, signal, onLine, stdin, deta
6786
6800
  });
6787
6801
  });
6788
6802
  }
6803
+ async function launchOf(command, args, env) {
6804
+ return process.platform === "win32" ? windowsLaunch(command, args, env) : {
6805
+ command,
6806
+ args,
6807
+ shell: false
6808
+ };
6809
+ }
6810
+ /**
6811
+ * Starts a child in its own process group (its own console on Windows) with no stdio, and settles
6812
+ * once it has started; the parent neither waits for it nor keeps it from exiting.
6813
+ */
6814
+ const realSpawnDetached = async (command, args, { cwd, env }) => {
6815
+ const launch = await launchOf(command, args, env);
6816
+ return new Promise((resolve, reject) => {
6817
+ const child = spawn(launch.command, launch.args, {
6818
+ cwd,
6819
+ env,
6820
+ shell: launch.shell,
6821
+ windowsHide: true,
6822
+ stdio: "ignore",
6823
+ detached: true
6824
+ });
6825
+ child.unref();
6826
+ child.once("error", reject);
6827
+ child.once("spawn", () => {
6828
+ child.off("error", reject);
6829
+ const { pid } = child;
6830
+ if (pid === void 0) reject(/* @__PURE__ */ new Error(`${command} started without a pid`));
6831
+ else resolve({ pid });
6832
+ });
6833
+ });
6834
+ };
6789
6835
  const realSpawn = (command, args, options) => spawnChild(command, args, options);
6790
6836
  function failedError(command, { exitCode, stderr }) {
6791
6837
  const tail = lastLines(stderr, STDERR_TAIL_LINES$1);
@@ -6855,6 +6901,20 @@ function contextSpawn(run, defaults, scope) {
6855
6901
  return result;
6856
6902
  };
6857
6903
  }
6904
+ /** `ctx.spawnDetached`: `run` bound to the run's cwd and env, its start failures classified by errno. */
6905
+ function contextSpawnDetached(run, defaults, scope) {
6906
+ return async (command, args = [], options = {}) => {
6907
+ defaults.signal.throwIfAborted();
6908
+ try {
6909
+ return await run(command, args, {
6910
+ cwd: options.cwd ?? defaults.cwd,
6911
+ env: options.env ?? defaults.env
6912
+ });
6913
+ } catch (error) {
6914
+ throw startError(command, error, scope);
6915
+ }
6916
+ };
6917
+ }
6858
6918
  //#endregion
6859
6919
  //#region ../agent-cli/dist/built-ins/semver.js
6860
6920
  const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
@@ -7177,17 +7237,19 @@ const LINES = {
7177
7237
  pnpm: {
7178
7238
  upgrade: (packages) => `pnpm up --latest ${packages.join(" ")}`,
7179
7239
  add: (packages) => `pnpm add -D ${packages.join(" ")}`,
7180
- reinstall: "pnpm install",
7181
- exec: (cli) => `pnpm exec ${cli}`
7240
+ install: "pnpm install"
7182
7241
  },
7183
7242
  npm: {
7184
7243
  upgrade: (packages) => `npm install ${packages.map((name) => `${name}@latest`).join(" ")}`,
7185
7244
  add: (packages) => `npm install -D ${packages.join(" ")}`,
7186
- reinstall: "npm install",
7187
- exec: (cli) => `npx ${cli}`
7245
+ install: "npm install"
7188
7246
  }
7189
7247
  };
7190
- const manifestSchema$1 = object({ packageManager: string().optional() });
7248
+ const manifestSchema$1 = object({
7249
+ packageManager: string().optional(),
7250
+ dependencies: record(string(), unknown()).optional(),
7251
+ devDependencies: record(string(), unknown()).optional()
7252
+ });
7191
7253
  const packageSchema = object({ version: string() });
7192
7254
  /** `dir` and every folder above it, nearest first. */
7193
7255
  function ancestorsOf(dir) {
@@ -7203,7 +7265,7 @@ async function exists(path) {
7203
7265
  throw error;
7204
7266
  }
7205
7267
  }
7206
- async function declaredManager(dir) {
7268
+ async function manifestIn(dir) {
7207
7269
  let text;
7208
7270
  try {
7209
7271
  text = await readFile(join(dir, "package.json"), "utf8");
@@ -7217,15 +7279,30 @@ async function declaredManager(dir) {
7217
7279
  } catch {
7218
7280
  return;
7219
7281
  }
7220
- const name = manifestSchema$1.safeParse(manifest).data?.packageManager?.split("@")[0];
7282
+ return manifestSchema$1.safeParse(manifest).data;
7283
+ }
7284
+ async function declaredManager(dir) {
7285
+ const name = (await manifestIn(dir))?.packageManager?.split("@")[0];
7221
7286
  return MANAGERS.find((manager) => manager === name);
7222
7287
  }
7288
+ async function manifestListing(name, cwd, root) {
7289
+ const dirs = ancestorsOf(cwd);
7290
+ const top = root === void 0 ? dirs.length : dirs.indexOf(root.dir) + 1;
7291
+ for (const dir of dirs.slice(0, top)) {
7292
+ const manifest = await manifestIn(dir);
7293
+ if (manifest?.dependencies?.[name] !== void 0 || manifest?.devDependencies?.[name] !== void 0) return join(dir, "package.json");
7294
+ }
7295
+ }
7223
7296
  async function lockfileRootOf(cwd) {
7224
7297
  for (const dir of ancestorsOf(cwd)) for (const [file, kind] of LOCKFILES) if (await exists(join(dir, file))) return {
7225
7298
  dir,
7226
7299
  manager: await declaredManager(dir) ?? kind
7227
7300
  };
7228
7301
  }
7302
+ /** The package manager's project root: the folder of the nearest lockfile, or `cwd` without one. */
7303
+ async function projectRootOf(cwd) {
7304
+ return (await lockfileRootOf(cwd))?.dir ?? cwd;
7305
+ }
7229
7306
  async function versionIn(manifest) {
7230
7307
  try {
7231
7308
  return packageSchema.parse(JSON.parse(await readFile(manifest, "utf8"))).version;
@@ -7234,7 +7311,7 @@ async function versionIn(manifest) {
7234
7311
  throw error;
7235
7312
  }
7236
7313
  }
7237
- async function standingOf(install, bin, cwd) {
7314
+ async function standingOf(install, bin, cwd, root) {
7238
7315
  if (bin === void 0) return { kind: "source" };
7239
7316
  for (const projectDir of ancestorsOf(cwd)) {
7240
7317
  const folder = join(projectDir, "node_modules", install.package);
@@ -7268,14 +7345,18 @@ async function standingOf(install, bin, cwd) {
7268
7345
  runningFrom
7269
7346
  };
7270
7347
  }
7271
- return { kind: "missing" };
7348
+ const manifest = await manifestListing(install.package, cwd, root);
7349
+ return manifest === void 0 ? { kind: "missing" } : {
7350
+ kind: "listed",
7351
+ manifest
7352
+ };
7272
7353
  }
7273
7354
  async function surveyDependency(install, bin, cwd) {
7274
- const [root, standing] = await Promise.all([lockfileRootOf(cwd), standingOf(install, bin, cwd)]);
7355
+ const root = await lockfileRootOf(cwd);
7275
7356
  return {
7276
7357
  cwd,
7277
7358
  root,
7278
- standing
7359
+ standing: await standingOf(install, bin, cwd, root)
7279
7360
  };
7280
7361
  }
7281
7362
  function packagesOf(install) {
@@ -7290,6 +7371,10 @@ function skippedFor(cli, install, standing) {
7290
7371
  message: standing.kind === "source" ? `not checked: ${cli.name} runs from source, not from an install` : `not checked: ${install.package} links to ${standing.copy.realDir}, so the workspace decides its version`
7291
7372
  };
7292
7373
  }
7374
+ function searchedFrom(cwd, root) {
7375
+ if (root === void 0) return `at or above ${cwd}`;
7376
+ return root.dir === cwd ? `at ${cwd}` : `from ${cwd} up to ${root.dir}`;
7377
+ }
7293
7378
  function problem(message, fix) {
7294
7379
  return fix === void 0 ? {
7295
7380
  state: "problem",
@@ -7306,11 +7391,12 @@ function dependencyInstallFinding(cli, install, { cwd, root, standing }) {
7306
7391
  switch (standing.kind) {
7307
7392
  case "source":
7308
7393
  case "linked": return skippedFor(cli, install, standing);
7309
- case "missing": return problem(`no package at or above ${cwd} depends on ${install.package}`, lines?.add(packagesOf(install)));
7310
- case "unreadable": return problem(`${standing.manifest} is not a package.json with a version: ${standing.reason}`, lines?.reinstall);
7394
+ case "missing": return problem(`no package ${searchedFrom(cwd, root)} depends on ${install.package}`, lines?.add(packagesOf(install)));
7395
+ case "listed": return problem(`${standing.manifest} lists ${install.package}, but no copy of it is installed at or above ${cwd}`, lines?.install);
7396
+ case "unreadable": return problem(`${standing.manifest} is not a package.json with a version: ${standing.reason}`, lines?.install);
7311
7397
  case "other": {
7312
7398
  const { copy, runningFrom } = standing;
7313
- return problem(`${cli.name} runs from ${runningFrom}, not from this project's ${install.package} ${copy.version} at ${copy.realDir}`, lines?.exec(cli.name));
7399
+ return problem(`${cli.name} runs from ${runningFrom}, not from this project's ${install.package} ${copy.version} at ${copy.realDir}`, `${runAsOf(cli)} doctor`);
7314
7400
  }
7315
7401
  case "installed": {
7316
7402
  const { copy } = standing;
@@ -7409,9 +7495,93 @@ function fixFor(cliName, copies, source) {
7409
7495
  }
7410
7496
  async function skillReport(cli, version, ctx) {
7411
7497
  const copies = await skillCopies(cli.name, version, ctx);
7498
+ const source = cli.skill !== void 0 && "source" in cli.skill ? cli.skill.source : void 0;
7412
7499
  return {
7413
7500
  copies,
7414
- fix: fixFor(cli.name, copies, cli.skill?.source)
7501
+ fix: fixFor(cli.name, copies, source)
7502
+ };
7503
+ }
7504
+ //#endregion
7505
+ //#region ../agent-cli/dist/built-ins/packaged-skill.js
7506
+ /** The folder, relative to the package root, of a CLI's Packaged skill, if it declares one. */
7507
+ function packagedFolderOf(cli) {
7508
+ return cli.skill !== void 0 && "packaged" in cli.skill ? cli.skill.packaged : void 0;
7509
+ }
7510
+ /** The project root and the Skill copy under it, as `skill install` writes it. */
7511
+ async function skillCopyOf(cli, cwd) {
7512
+ const root = await projectRootOf(cwd);
7513
+ return {
7514
+ root,
7515
+ copy: join(root, ".claude", "skills", cli.name)
7516
+ };
7517
+ }
7518
+ /** Every file under `dir`, relative to it and sorted; none when `dir` is missing. */
7519
+ async function filesIn(dir) {
7520
+ return (await orMissing(readdir(dir, {
7521
+ recursive: true,
7522
+ withFileTypes: true
7523
+ })) ?? []).filter((entry) => entry.isFile()).map((entry) => relative(dir, join(entry.parentPath, entry.name))).sort();
7524
+ }
7525
+ /** The versions of the package's skill in `source` and of its copy in `copy`. */
7526
+ async function skillVersionsOf(source, copy) {
7527
+ const [shipped, installed] = await Promise.all([readFile(join(source, "SKILL.md"), "utf8"), orMissing(readFile(join(copy, "SKILL.md"), "utf8"))]);
7528
+ const packageVersion = skillVersionIn(shipped) ?? null;
7529
+ const copyVersion = installed === void 0 ? void 0 : skillVersionIn(installed) ?? null;
7530
+ return {
7531
+ packageVersion,
7532
+ copyVersion,
7533
+ same: copyVersion === packageVersion
7534
+ };
7535
+ }
7536
+ async function personalCopyOf(name, copy, ctx) {
7537
+ const home = join(homeOf(ctx.env), ".claude", "skills", name);
7538
+ const real = await orMissing(realpath(home));
7539
+ return real === void 0 || real === await orMissing(realpath(copy)) ? void 0 : home;
7540
+ }
7541
+ /** What the `skill` row of a Packaged skill carries under `--json` beside its message. */
7542
+ const packagedSkillDetailsSchema = object({
7543
+ copy: string(),
7544
+ copyVersion: string().nullable(),
7545
+ packageVersion: string().nullable(),
7546
+ /** A personal copy, which Claude Code prefers over the project's. */
7547
+ home: string().optional()
7548
+ });
7549
+ function versionLabel(version) {
7550
+ return version ?? "(no metadata.version)";
7551
+ }
7552
+ function compared(name, { copy, copyVersion, packageVersion }, same) {
7553
+ const stated = `${copy} ${versionLabel(copyVersion)}`;
7554
+ return same ? `${stated}, the version ${name} ships` : `${stated} differs from the ${versionLabel(packageVersion)} that ${name} ships`;
7555
+ }
7556
+ /** The `skill` row of a Packaged skill: the project root's copy against the package's, and any under home. */
7557
+ async function packagedSkillFinding(cli, folder, ctx) {
7558
+ const { copy } = await skillCopyOf(cli, ctx.cwd);
7559
+ const versions = await skillVersionsOf(join(ctx.packageRoot, folder), copy);
7560
+ const { packageVersion } = versions;
7561
+ const copyVersion = versions.copyVersion ?? null;
7562
+ const home = await personalCopyOf(cli.name, copy, ctx);
7563
+ const skill = {
7564
+ copy,
7565
+ copyVersion,
7566
+ packageVersion,
7567
+ ...home === void 0 ? {} : { home }
7568
+ };
7569
+ const head = versions.copyVersion === void 0 ? `no copy of the ${cli.name} skill at ${copy}` : compared(cli.name, skill, versions.same);
7570
+ const message = home === void 0 ? head : `${head}; remove ${home}: Claude Code prefers a personal skill over the project's`;
7571
+ if (!versions.same) return {
7572
+ state: "problem",
7573
+ message,
7574
+ fix: `${runAsOf(cli)} skill install`,
7575
+ skill
7576
+ };
7577
+ return home === void 0 ? {
7578
+ state: "ok",
7579
+ message,
7580
+ skill
7581
+ } : {
7582
+ state: "problem",
7583
+ message,
7584
+ skill
7415
7585
  };
7416
7586
  }
7417
7587
  //#endregion
@@ -7581,9 +7751,12 @@ function describeCopy({ path, version, state }) {
7581
7751
  return `${path}${version === void 0 ? "" : ` ${version}`} (${SKILL_STATE_NOTES[state]})`;
7582
7752
  }
7583
7753
  function skillRow(cli) {
7754
+ const packaged = packagedFolderOf(cli);
7584
7755
  return {
7585
7756
  name: "skill",
7757
+ advisory: true,
7586
7758
  run: async (ctx) => {
7759
+ if (packaged !== void 0) return packagedSkillFinding(cli, packaged, ctx);
7587
7760
  const { copies, fix } = await skillReport(cli, cli.version, ctx);
7588
7761
  const message = copies.length === 0 ? `no copy of the ${cli.name} skill is installed` : copies.map(describeCopy).join("; ");
7589
7762
  return fix === void 0 ? ok(message) : {
@@ -7624,7 +7797,8 @@ const reportSchema = object({
7624
7797
  ]),
7625
7798
  message: string(),
7626
7799
  fix: string().optional(),
7627
- code: string().optional()
7800
+ code: string().optional(),
7801
+ skill: packagedSkillDetailsSchema.optional()
7628
7802
  })),
7629
7803
  next: array(string())
7630
7804
  });
@@ -7694,10 +7868,10 @@ function doctorCommand(cli, launch) {
7694
7868
  name: "doctor",
7695
7869
  description: "check the setup and say what fixes each problem",
7696
7870
  examples: [{
7697
- command: `${cli.name} doctor`,
7871
+ command: `${runAsOf(cli)} doctor`,
7698
7872
  note: "check the setup and list the commands that fix it"
7699
7873
  }, {
7700
- command: `${cli.name} doctor --json`,
7874
+ command: `${runAsOf(cli)} doctor --json`,
7701
7875
  note: "get the rows and fixes as data"
7702
7876
  }],
7703
7877
  effect: "read",
@@ -7725,687 +7899,841 @@ function doctorCommand(cli, launch) {
7725
7899
  });
7726
7900
  }
7727
7901
  //#endregion
7728
- //#region ../agent-cli/dist/interrupts.js
7729
- const INTERRUPTED = "INTERRUPTED";
7730
- const DEADLINE_EXCEEDED = "DEADLINE_EXCEEDED";
7731
- /** How long an interrupted Command may take to settle before the run ends without it. */
7732
- const GRACE_MS = 3e3;
7733
- const GRACE_OVER = Symbol("grace over");
7734
- function interruptedError(callerLine) {
7735
- return new CliError({
7736
- code: INTERRUPTED,
7737
- message: "interrupted before it finished",
7738
- hint: `to finish it, run \`${callerLine}\` again, unless it was stopped on purpose; if a tool timeout stopped it, give it longer`
7739
- });
7902
+ //#region ../agent-cli/dist/agent.js
7903
+ const AGENT_NAME = /^[a-zA-Z0-9_-]+$/u;
7904
+ const isSet = (name) => (env) => (env[name] ?? "") !== "";
7905
+ const equals = (name, value) => (env) => env[name] === value;
7906
+ const MARKERS = [
7907
+ (env) => AGENT_NAME.test(env.AI_AGENT ?? ""),
7908
+ equals("AGENT", "amp"),
7909
+ isSet("CODEX_SANDBOX"),
7910
+ isSet("CODEX_CI"),
7911
+ isSet("CODEX_THREAD_ID"),
7912
+ isSet("GEMINI_CLI"),
7913
+ isSet("COPILOT_CLI"),
7914
+ isSet("OPENCODE"),
7915
+ isSet("ANTIGRAVITY_AGENT"),
7916
+ isSet("AUGMENT_AGENT"),
7917
+ isSet("REPL_ID"),
7918
+ isSet("CLAUDE_CODE_IS_COWORK"),
7919
+ isSet("CLAUDECODE"),
7920
+ isSet("CLAUDE_CODE"),
7921
+ isSet("CURSOR_TRACE_ID"),
7922
+ isSet("CURSOR_AGENT"),
7923
+ equals("CURSOR_EXTENSION_HOST_ROLE", "agent-exec"),
7924
+ equals("TERM_PROGRAM", "kiro"),
7925
+ (env) => ["/.pi/agent", "\\.pi\\agent"].some((dir) => (env.PATH ?? "").includes(dir)),
7926
+ isSet("GOOSE_PROVIDER")
7927
+ ];
7928
+ /**
7929
+ * Whether a Coding agent, not a person, is running the Agent CLI. Some Coding agents run commands
7930
+ * in a PTY, so a TTY alone doesn't mean a person is watching.
7931
+ */
7932
+ function isAgent(env) {
7933
+ return MARKERS.some((marker) => marker(env));
7740
7934
  }
7741
- function waitHint(callerLine, bound) {
7742
- return bound === "timeout" ? `the call may have been billed: ask your user before running \`${callerLine}\` again` : `run \`${callerLine}\` again later, when the service it waits on may answer faster`;
7935
+ //#endregion
7936
+ //#region ../agent-cli/dist/errno.js
7937
+ const PERMISSION_DENIED = "PERMISSION_DENIED";
7938
+ const DISK_FULL = "DISK_FULL";
7939
+ const NOT_FOUND = "NOT_FOUND";
7940
+ const ALREADY_EXISTS = "ALREADY_EXISTS";
7941
+ const NOT_A_FILE = "NOT_A_FILE";
7942
+ function permissionHint({ path, access, callerLine, env, platform }) {
7943
+ if (platform === "win32") return `a program may hold ${path} open: close it, then run \`${callerLine}\` again`;
7944
+ if (!isAgent(env)) return `check the permissions of ${path} and its folder`;
7945
+ 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`}`;
7743
7946
  }
7744
- function deadlineError(commandLine, ms, hint, bound = "deadline") {
7745
- return new CliError({
7746
- code: DEADLINE_EXCEEDED,
7747
- message: `${commandLine} ran past its ${ms} ms ${bound}`,
7748
- hint
7947
+ function fileErrorOf(scope) {
7948
+ return (error, at) => classifyErrno(error, {
7949
+ ...at,
7950
+ ...scope
7749
7951
  });
7750
7952
  }
7751
- /** One Command run's cancellation: its signal, its deadlines and the steps it must finish. */
7752
- var RunControl = class {
7753
- signal;
7754
- #controller = new AbortController();
7755
- #commandLine;
7756
- #callerLine;
7757
- #outer;
7758
- #onOuterAbort = () => this.#controller.abort(interruptedError(this.#callerLine));
7759
- /** Each running must-finish step, tracked until it settles, with its name. */
7760
- #steps = /* @__PURE__ */ new Map();
7761
- #progress;
7762
- #clock;
7763
- #disposed = new AbortController();
7764
- /** The reasons this run's deadlines and timeouts aborted with, to tell them in a Command's error. */
7765
- #abortReasons = /* @__PURE__ */ new WeakSet();
7766
- #graceCancel = new AbortController();
7767
- #endGrace = () => {};
7768
- #graceOver = new Promise((resolve) => {
7769
- this.#endGrace = resolve;
7770
- });
7771
- constructor({ commandLine, callerLine, outer, clock, progress, runDeadline }) {
7772
- this.#commandLine = commandLine;
7773
- this.#callerLine = callerLine;
7774
- this.#outer = outer;
7775
- this.#clock = clock;
7776
- this.#progress = progress;
7777
- this.signal = this.#controller.signal;
7778
- this.signal.addEventListener("abort", () => this.#onStop(), { once: true });
7779
- if (outer?.aborted === true) this.#onOuterAbort();
7780
- else outer?.addEventListener("abort", this.#onOuterAbort, { once: true });
7781
- if (runDeadline !== void 0) {
7782
- const { ms, hint } = runDeadline;
7783
- after(clock, ms, this.#disposed.signal, () => this.#controller.abort(deadlineError(commandLine, ms, hint())));
7784
- }
7953
+ function classifyErrno(error, context) {
7954
+ const { path } = context;
7955
+ switch (errnoOf(error)) {
7956
+ case "EACCES":
7957
+ case "EPERM":
7958
+ case "EROFS":
7959
+ case "EBUSY": return new CliError({
7960
+ code: PERMISSION_DENIED,
7961
+ message: `permission denied: ${path}`,
7962
+ hint: permissionHint(context)
7963
+ }, { cause: error });
7964
+ case "ENOSPC":
7965
+ case "EDQUOT": return new CliError({
7966
+ code: DISK_FULL,
7967
+ message: `no space left on the disk holding ${path}`,
7968
+ hint: `free space on that disk, then run \`${context.callerLine}\` again`
7969
+ }, { cause: error });
7970
+ case "ENOENT":
7971
+ case "ENOTDIR":
7972
+ case "ENAMETOOLONG": return new CliError({
7973
+ code: NOT_FOUND,
7974
+ message: `not found: ${path}`,
7975
+ hint: `check that ${path} and its folder exist`
7976
+ }, { cause: error });
7977
+ case "EEXIST": return new CliError({
7978
+ code: ALREADY_EXISTS,
7979
+ message: `already exists: ${path}`,
7980
+ hint: `pass another path, or remove ${path}`
7981
+ }, { cause: error });
7982
+ case "EISDIR": return new CliError({
7983
+ code: NOT_A_FILE,
7984
+ message: `not a file: ${path} is a folder`,
7985
+ hint: `pass the path of a file, not of the folder ${path}`
7986
+ }, { cause: error });
7987
+ default: return error;
7785
7988
  }
7786
- deadline(ms) {
7787
- return AbortSignal.any([this.signal, this.#abortsAfter(ms, "deadline")]);
7989
+ }
7990
+ //#endregion
7991
+ //#region ../agent-cli/dist/files.js
7992
+ const TEMP_FILE = /^\..+\.agent-cli-(\d+)\.tmp$/;
7993
+ /** Where the Runtime writes `path` before moving it into place; a dead writer's is swept. */
7994
+ function tempPathOf(path) {
7995
+ return join(dirname(path), `.${basename(path)}.agent-cli-${process.pid}.tmp`);
7996
+ }
7997
+ function isAlive(pid) {
7998
+ try {
7999
+ process.kill(pid, 0);
8000
+ return true;
8001
+ } catch (error) {
8002
+ return errnoOf(error) === "EPERM";
7788
8003
  }
7789
- timeout(ms) {
7790
- return this.#abortsAfter(ms, "timeout");
8004
+ }
8005
+ async function sweep(folder) {
8006
+ const dead = (await readdir(folder)).filter((name) => {
8007
+ const pid = TEMP_FILE.exec(name)?.[1];
8008
+ return pid !== void 0 && !isAlive(Number(pid));
8009
+ });
8010
+ await Promise.all(dead.map((name) => rm(join(folder, name), { force: true })));
8011
+ }
8012
+ /** The file a symlink at `path` points to, so a replacing write keeps the link. */
8013
+ async function targetOf$1(path) {
8014
+ try {
8015
+ return await realpath(path);
8016
+ } catch (error) {
8017
+ if (isMissingFile(error)) return path;
8018
+ throw error;
7791
8019
  }
7792
- #abortsAfter(ms, bound) {
7793
- const controller = new AbortController();
7794
- after(this.#clock, ms, this.#disposed.signal, () => {
7795
- const reason = deadlineError(this.#commandLine, ms, waitHint(this.#callerLine, bound), bound);
7796
- this.#abortReasons.add(reason);
7797
- controller.abort(reason);
7798
- });
7799
- return controller.signal;
8020
+ }
8021
+ async function writeWithMode(path, data, mode) {
8022
+ if (mode === void 0) {
8023
+ await writeFile(path, data);
8024
+ return;
7800
8025
  }
7801
- async runToEnd(name, work) {
7802
- this.#graceCancel.abort();
7803
- if (this.signal.aborted) this.#finishing(name);
7804
- const step = work();
7805
- const settled = step.then(() => {}, () => {});
7806
- this.#steps.set(settled, name);
7807
- try {
7808
- return await step;
7809
- } finally {
7810
- this.#steps.delete(settled);
7811
- this.#startGrace();
7812
- }
7813
- }
7814
- /** Waits for the Command, its must-finish steps, and no longer than the grace once interrupted. */
7815
- async settle(work) {
7816
- try {
7817
- const value = await Promise.race([work, this.#graceOver]);
7818
- if (this.signal.aborted || value === GRACE_OVER) throw this.signal.reason;
7819
- return value;
7820
- } finally {
7821
- await this.#stepsDone();
7822
- this.close();
7823
- }
7824
- }
7825
- /**
7826
- * The error a run ends with: an interrupt or the run deadline outranks what the Command threw,
7827
- * unless the Command threw its own `INTERRUPTED`, saying what it finished before it stopped.
7828
- * Otherwise each leaf that is not a `CliError` and whose cause chain holds a deadline's or timeout's
7829
- * abort is reported as that abort, so a declared error keeps its own code.
7830
- */
7831
- errorAfter(error) {
7832
- if (this.signal.aborted) return error instanceof CliError && error.code === "INTERRUPTED" ? error : this.signal.reason;
7833
- const leaves = errorsOf(error).map((leaf) => this.#abortIn(leaf) ?? leaf);
7834
- return leaves.length === 1 ? leaves[0] : new AggregateError(leaves);
7835
- }
7836
- #abortIn(error) {
7837
- if (error instanceof CliError) return void 0;
7838
- return causesOf(error).find((each) => each instanceof CliError && this.#abortReasons.has(each));
7839
- }
7840
- #onStop() {
7841
- for (const name of this.#steps.values()) this.#finishing(name);
7842
- this.#startGrace();
7843
- }
7844
- #finishing(name) {
7845
- this.#progress(`finishing ${name}`);
7846
- }
7847
- #startGrace() {
7848
- if (this.signal.aborted && this.#steps.size === 0) {
7849
- this.#graceCancel.abort();
7850
- this.#graceCancel = new AbortController();
7851
- after(this.#clock, GRACE_MS, this.#graceCancel.signal, () => this.#endGrace(GRACE_OVER));
7852
- }
7853
- }
7854
- async #stepsDone() {
7855
- while (this.#steps.size > 0) await Promise.all(this.#steps.keys());
7856
- }
7857
- /** Stops the run's timers and its hold on the caller's signal, for a run that never settles. */
7858
- close() {
7859
- this.#graceCancel.abort();
7860
- this.#disposed.abort();
7861
- this.#outer?.removeEventListener("abort", this.#onOuterAbort);
7862
- }
7863
- };
7864
- //#endregion
7865
- //#region ../agent-cli/dist/built-ins/upgrades.js
7866
- /** A skipped release's Upgrade steps, or its whole section when it lists none. */
7867
- const upgradeSchema = union([object({
7868
- version: string(),
7869
- steps: array(string())
7870
- }), object({
7871
- version: string(),
7872
- notes: string()
7873
- })]);
7874
- const RELEASE_HEADING = /^## v?(\S+)/;
7875
- const UPGRADE_HEADING = /^### Upgrade from v\S+$/;
7876
- const HEADING = /^#{1,6} /;
7877
- const ITEM = /^[-*] /;
7878
- const NOTHING_TO_DO = /^nothing to do\.?$/i;
7879
- function releasesIn(changelog) {
7880
- const releases = [];
7881
- let current;
7882
- for (const line of changelog.split(/\r?\n/)) {
7883
- const heading = RELEASE_HEADING.exec(line);
7884
- if (heading !== null) {
7885
- const version = heading[1] ?? "";
7886
- current = isVersion(version) ? {
7887
- version,
7888
- lines: []
7889
- } : void 0;
7890
- if (current !== void 0) releases.push(current);
7891
- } else current?.lines.push(line);
7892
- }
7893
- return releases;
7894
- }
7895
- function listedSteps(lines) {
7896
- const start = lines.findIndex((line) => UPGRADE_HEADING.test(line.trimEnd()));
7897
- if (start === -1) return [];
7898
- const end = lines.findIndex((line, i) => i > start && HEADING.test(line));
7899
- const body = lines.slice(start + 1, end === -1 ? void 0 : end).filter((line) => line.trim() !== "");
7900
- const starts = body.flatMap((line, i) => ITEM.test(line) ? [i] : []);
7901
- return starts.map((first, i) => body.slice(first, starts[i + 1]).map((line) => line.trim()).join(" ").replace(ITEM, ""));
8026
+ await writeFile(path, data, { mode });
8027
+ await chmod(path, mode);
7902
8028
  }
7903
- function upgradeOf({ version, lines }) {
7904
- const listed = listedSteps(lines);
7905
- if (listed.length === 0) return [{
7906
- version,
7907
- notes: lines.join("\n").trim()
7908
- }];
7909
- const todo = listed.filter((step) => !NOTHING_TO_DO.test(step));
7910
- return todo.length === 0 ? [] : [{
7911
- version,
7912
- steps: todo
7913
- }];
8029
+ /** What `link` answers on a file system without hard links, such as exFAT. */
8030
+ const NO_HARD_LINKS = /* @__PURE__ */ new Set([
8031
+ "EPERM",
8032
+ "ENOTSUP",
8033
+ "EXDEV"
8034
+ ]);
8035
+ /** A link fails atomically when `path` is taken, so no reader ever sees half a file there. */
8036
+ async function linkNew(temp, path, data, mode) {
8037
+ try {
8038
+ await link(temp, path);
8039
+ } catch (error) {
8040
+ if (!NO_HARD_LINKS.has(errnoOf(error))) throw error;
8041
+ await createNew(path, data, mode);
8042
+ }
7914
8043
  }
7915
- /**
7916
- * What each release after `from` up to `to` asks of the operator, oldest first, from the
7917
- * CHANGELOG.md in `packageDir`; none when the package ships no CHANGELOG.
7918
- */
7919
- async function upgradesOf(packageDir, from, to) {
7920
- let changelog;
8044
+ /** A file this call created and could not finish is removed, so a rerun does not find half of it. */
8045
+ async function createNew(path, data, mode) {
8046
+ const handle = await open(path, "wx", mode);
7921
8047
  try {
7922
- changelog = await readFile(join(packageDir, "CHANGELOG.md"), "utf8");
8048
+ await handle.writeFile(data);
8049
+ if (mode !== void 0) await handle.chmod(mode);
7923
8050
  } catch (error) {
7924
- if (isMissingFile(error)) return [];
8051
+ await handle.close();
8052
+ await rm(path, { force: true });
7925
8053
  throw error;
7926
8054
  }
7927
- return releasesIn(changelog).filter(({ version }) => compareVersions(version, from) > 0 && compareVersions(version, to) <= 0).sort((a, b) => compareVersions(a.version, b.version)).flatMap(upgradeOf);
7928
- }
7929
- //#endregion
7930
- //#region ../agent-cli/dist/built-ins/update.js
7931
- const updateSchema = object({
7932
- from: string(),
7933
- latest: string(),
7934
- installed: object({
7935
- version: string(),
7936
- prefix: string()
7937
- }).nullable(),
7938
- upgrades: array(upgradeSchema),
7939
- next: array(string())
7940
- });
7941
- function nothingToInstall({ from, latest }) {
7942
- return standingOf$1(from, latest) === "current" ? `already on ${from}, the latest release: nothing to install` : `${from} is newer than the latest release ${latest}: nothing to install`;
8055
+ await handle.close();
7943
8056
  }
7944
- function renderUpgrade(upgrade) {
7945
- return "steps" in upgrade ? [`\nupgrade steps for ${upgrade.version}:`, ...upgrade.steps.map((step) => `- ${step}`)].join("\n") : `\nchanges in ${upgrade.version}:\n${upgrade.notes}`;
8057
+ const RENAME_ATTEMPTS = 10;
8058
+ const RENAME_BACKOFF_MS = 10;
8059
+ const HELD_BY_ANOTHER_PROGRAM = /* @__PURE__ */ new Set(["EPERM", "EBUSY"]);
8060
+ async function renameRetrying(from, to) {
8061
+ for (let attempt = 1;; attempt++) try {
8062
+ await rename(from, to);
8063
+ return;
8064
+ } catch (error) {
8065
+ if (!(process.platform === "win32" && HELD_BY_ANOTHER_PROGRAM.has(errnoOf(error))) || attempt === RENAME_ATTEMPTS) throw error;
8066
+ await setTimeout$1(attempt * RENAME_BACKOFF_MS);
8067
+ }
7946
8068
  }
7947
- function renderUpdate(cli, install, update) {
7948
- const { installed } = update;
7949
- const head = installed === null ? [nothingToInstall(update)] : [`installed ${install.package}@${installed.version} with the npm under ${installed.prefix}, replacing ${update.from}`, ...update.upgrades.map(renderUpgrade)];
7950
- const next = update.next.map((command) => `next: ${command}, so the ${cli.name} skill describes ${update.latest}`);
7951
- return (next.length === 0 ? head : [
7952
- ...head,
7953
- "",
7954
- ...next
7955
- ]).join("\n");
8069
+ /** Real files for one run: the first write to each folder sweeps the temp files of dead writers. */
8070
+ function realFiles() {
8071
+ const swept = /* @__PURE__ */ new Set();
8072
+ return {
8073
+ exists: async (path) => {
8074
+ try {
8075
+ await access(path);
8076
+ return true;
8077
+ } catch (error) {
8078
+ if (isNothingThere(error)) return false;
8079
+ throw error;
8080
+ }
8081
+ },
8082
+ write: async (path, data, options = {}) => {
8083
+ await mkdir(dirname(path), { recursive: true });
8084
+ const target = options.exclusive === true ? path : await targetOf$1(path);
8085
+ const folder = dirname(target);
8086
+ if (!swept.has(folder)) {
8087
+ await sweep(folder);
8088
+ swept.add(folder);
8089
+ }
8090
+ const temp = tempPathOf(target);
8091
+ try {
8092
+ await writeWithMode(temp, data, options.mode);
8093
+ if (options.exclusive === true) {
8094
+ await linkNew(temp, target, data, options.mode);
8095
+ await rm(temp);
8096
+ } else await renameRetrying(temp, target);
8097
+ } catch (error) {
8098
+ await rm(temp, { force: true });
8099
+ throw error;
8100
+ }
8101
+ },
8102
+ read: (path) => readFile(path),
8103
+ list: (folder) => readdir(folder),
8104
+ remove: (path) => rm(path, {
8105
+ recursive: true,
8106
+ force: true
8107
+ }),
8108
+ mkdir: async (path, options) => {
8109
+ const mode = options?.mode;
8110
+ if (options?.exclusive !== true) {
8111
+ await mkdir(path, {
8112
+ recursive: true,
8113
+ mode
8114
+ });
8115
+ return;
8116
+ }
8117
+ await mkdir(dirname(path), {
8118
+ recursive: true,
8119
+ mode
8120
+ });
8121
+ await mkdir(path, { mode });
8122
+ },
8123
+ stat: async (path) => {
8124
+ try {
8125
+ const { size, dev, ino } = await stat(path, { bigint: true });
8126
+ return {
8127
+ size: Number(size),
8128
+ id: `${dev}:${ino}`
8129
+ };
8130
+ } catch (error) {
8131
+ if (isNothingThere(error)) return void 0;
8132
+ throw error;
8133
+ }
8134
+ }
8135
+ };
7956
8136
  }
7957
- async function skillNext(cli, latest, ctx) {
7958
- const { fix } = await skillReport(cli, latest, ctx);
7959
- return fix === void 0 ? [] : [fix];
8137
+ /**
8138
+ * `files` resolving paths against `cwd`, its failures classified by errno; `list` drops the
8139
+ * Runtime's temp files and sorts.
8140
+ */
8141
+ function classifiedFiles(files, { cwd, fileError }) {
8142
+ async function at(access, path, work) {
8143
+ const target = resolve(cwd, path);
8144
+ try {
8145
+ return await work(target);
8146
+ } catch (error) {
8147
+ throw fileError(error, {
8148
+ path: target,
8149
+ access
8150
+ });
8151
+ }
8152
+ }
8153
+ return {
8154
+ exists: (path) => at("read", path, (target) => files.exists(target)),
8155
+ write: (path, data, options) => at("write", path, (target) => files.write(target, data, options)),
8156
+ read: (path) => at("read", path, (target) => files.read(target)),
8157
+ list: (folder) => at("read", folder, async (target) => (await files.list(target)).filter((name) => !TEMP_FILE.test(name)).sort()),
8158
+ remove: (path) => at("write", path, (target) => files.remove(target)),
8159
+ mkdir: (path, options) => at("write", path, (target) => files.mkdir(target, options)),
8160
+ stat: (path) => at("read", path, (target) => files.stat(target))
8161
+ };
7960
8162
  }
7961
- function interruptedAfterInstall(plan) {
7962
- return new CliError({
7963
- code: INTERRUPTED,
7964
- message: `interrupted, but ${plan.package}@${plan.latest} had already been installed with the npm under ${plan.prefix}, replacing ${plan.from}`,
7965
- hint: "nothing to run again: run the commands under next"
8163
+ //#endregion
8164
+ //#region ../agent-cli/dist/outputs.js
8165
+ const OUTPUT_EXISTS = "OUTPUT_EXISTS";
8166
+ /** Refuses a run that would overwrite existing outputs unless the caller passed `--force`. */
8167
+ async function checkOutputs({ invocation, commandLine, force, files }, outputs) {
8168
+ if (outputs === void 0 || force) return;
8169
+ const found = await Promise.all(outputs.map((path) => files.exists(path)));
8170
+ const existing = outputs.filter((_path, index) => found[index]);
8171
+ if (existing.length === 0) return;
8172
+ const them = existing.length === 1 ? "it" : "them";
8173
+ throw new CliError({
8174
+ code: OUTPUT_EXISTS,
8175
+ message: `\`${commandLine}\` would overwrite ${existing.join(", ")}`,
8176
+ hint: `to replace ${them}, run \`${commandLineOf(invocation, { add: [FORCE_FLAG.flag] })}\``
7966
8177
  });
7967
8178
  }
7968
- /** The built-in `update`, for a CLI installed globally with npm. */
7969
- function updateCommand(cli, install, { bin, node }) {
7970
- return defineCommand$1({
7971
- name: "update",
7972
- description: "install the latest version of this CLI from its registry",
7973
- examples: [{
7974
- command: `${cli.name} update`,
7975
- note: "install the latest version and show what changed"
7976
- }, {
7977
- command: `${cli.name} update --dry-run`,
7978
- note: "show the version it would install"
7979
- }],
7980
- effect: "write",
7981
- output: updateSchema,
7982
- plan: async (_input, ctx) => {
7983
- const npm = globalNpm(cli, install, ctx, node);
7984
- const copy = await globalInstallOf(bin, npm);
7985
- return {
7986
- package: install.package,
7987
- from: cli.version,
7988
- latest: await latestVersion(npm),
7989
- prefix: copy.prefix,
7990
- packageDir: copy.packageDir
7991
- };
7992
- },
7993
- apply: async (plan, ctx) => {
7994
- const { from, latest, prefix } = plan;
7995
- if (standingOf$1(from, latest) !== "behind") return {
7996
- from,
7997
- latest,
7998
- installed: null,
7999
- upgrades: [],
8000
- next: await skillNext(cli, latest, ctx)
8001
- };
8002
- ctx.signal.throwIfAborted();
8003
- ctx.progress(`installing ${plan.package}@${latest} with the npm under ${prefix}…`);
8004
- await ctx.runToEnd(`installing ${plan.package}@${latest}`, () => installGlobally(latest, globalNpm(cli, install, ctx, node)));
8005
- const update = {
8006
- from,
8007
- latest,
8008
- installed: {
8009
- version: latest,
8010
- prefix
8011
- },
8012
- upgrades: await upgradesOf(plan.packageDir, from, latest),
8013
- next: await skillNext(cli, latest, ctx)
8014
- };
8015
- if (ctx.signal.aborted) {
8016
- ctx.deliver(update);
8017
- throw interruptedAfterInstall(plan);
8018
- }
8019
- return update;
8179
+ /** Guards the writes to declared outputs of `files`, whose paths resolve against `cwd`. */
8180
+ function outputsGuard(files, cwd) {
8181
+ const unwritten = /* @__PURE__ */ new Set();
8182
+ let forceLine = "";
8183
+ const exclusiveWrite = async (path, data, options) => {
8184
+ const target = resolve(cwd, path);
8185
+ unwritten.delete(target);
8186
+ try {
8187
+ await files.write(path, data, {
8188
+ ...options,
8189
+ exclusive: true
8190
+ });
8191
+ } catch (error) {
8192
+ unwritten.add(target);
8193
+ if (!(error instanceof CliError) || error.code !== "ALREADY_EXISTS") throw error;
8194
+ throw new CliError({
8195
+ code: ALREADY_EXISTS,
8196
+ message: error.message,
8197
+ hint: `to replace it, run \`${forceLine}\``
8198
+ }, { cause: error });
8199
+ }
8200
+ };
8201
+ return {
8202
+ files: {
8203
+ ...files,
8204
+ write: (path, data, options) => unwritten.has(resolve(cwd, path)) ? exclusiveWrite(path, data, options) : files.write(path, data, options)
8020
8205
  },
8021
- render: (update) => renderUpdate(cli, install, update),
8022
- renderPlan: (plan) => standingOf$1(plan.from, plan.latest) === "behind" ? `would install ${plan.package}@${plan.latest} with the npm under ${plan.prefix}, replacing ${plan.from}` : nothingToInstall(plan)
8023
- });
8206
+ guard: (outputs, line) => {
8207
+ forceLine = line;
8208
+ for (const path of outputs) unwritten.add(resolve(cwd, path));
8209
+ }
8210
+ };
8211
+ }
8212
+ /**
8213
+ * Creates each output folder and a temp file in it, so a sandbox denial fails before `apply`
8214
+ * spends anything. The temp file's name is the Runtime's, so a crashed run's leftover is swept.
8215
+ */
8216
+ async function probeOutputs({ files, cwd, fileError }, outputs) {
8217
+ const folders = new Set(outputs.map((path) => dirname(resolve(cwd, path))));
8218
+ await Promise.all([...folders].map(async (folder) => {
8219
+ const probe = tempPathOf(join(folder, "outputs-probe"));
8220
+ try {
8221
+ await files.mkdir(folder);
8222
+ await files.write(probe, "");
8223
+ await files.remove(probe);
8224
+ } catch (error) {
8225
+ throw fileError(error, {
8226
+ path: folder,
8227
+ access: "write"
8228
+ });
8229
+ }
8230
+ }));
8024
8231
  }
8025
8232
  //#endregion
8026
- //#region ../agent-cli/dist/built-ins/commands.js
8027
- /** The Commands the Runtime adds to a multi-Command CLI: `update` only for a Global install. */
8028
- function builtInCommands(cli, launch) {
8233
+ //#region ../agent-cli/dist/built-ins/skill-install.js
8234
+ const installSchema = object({
8235
+ copy: string(),
8236
+ written: array(string()),
8237
+ removed: array(string()),
8238
+ agentsMd: object({
8239
+ path: string(),
8240
+ changed: boolean()
8241
+ }),
8242
+ handEdits: array(string())
8243
+ });
8244
+ async function sameContent(left, right) {
8245
+ const [a, b] = await Promise.all([readFile(left), readFile(right)]);
8246
+ return a.equals(b);
8247
+ }
8248
+ /** One paragraph telling every session that the CLI exists, how to run it and which skill to load. */
8249
+ function agentsBlock(cli) {
8250
+ const { install } = cli;
8251
+ const where = install?.mode === "dependency" ? `, and only from a package that has \`${install.package}\` in its dependencies; from anywhere else npx reports the package missing` : "";
8252
+ const what = cli.description.replace(/\.$/, "");
8253
+ return `\`${cli.name}\`: ${what}. Run it as \`${runAsOf(cli)}\`${where}. Load the \`${cli.name}\` skill before using it.`;
8254
+ }
8255
+ function markersOf(name) {
8029
8256
  return {
8030
- doctor: doctorCommand(cli, launch),
8031
- ...cli.install?.mode === "global" ? { update: updateCommand(cli, cli.install, launch) } : {}
8257
+ begin: `<!-- BEGIN:${name} -->`,
8258
+ end: `<!-- END:${name} -->`
8259
+ };
8260
+ }
8261
+ /** `text` with the block between its markers, appended when they are missing; nothing else changes. */
8262
+ function withBlock(text, block, { begin, end }) {
8263
+ const whole = `${begin}\n${block}\n${end}`;
8264
+ const start = text.indexOf(begin);
8265
+ const stop = start === -1 ? -1 : text.indexOf(end, start);
8266
+ if (stop !== -1) return `${text.slice(0, start)}${whole}${text.slice(stop + end.length)}`;
8267
+ if (text === "") return `${whole}\n`;
8268
+ return `${text}${text.endsWith("\n") ? "" : "\n"}\n${whole}\n`;
8269
+ }
8270
+ function blockChangeOf(existing, block, markers) {
8271
+ if (existing === void 0) return "create";
8272
+ if (withBlock(existing, block, markers) === existing) return null;
8273
+ return existing.includes(markers.begin) ? "rewrite" : "add";
8274
+ }
8275
+ async function handEditsAt(root) {
8276
+ const claudeMd = join(root, "CLAUDE.md");
8277
+ const text = await orMissing(readFile(claudeMd, "utf8"));
8278
+ if (text === void 0 || text.split(/\r?\n/).some((line) => line.trim() === "@AGENTS.md")) return [];
8279
+ return [`add a line \`@AGENTS.md\` to ${claudeMd}, so Claude Code reads AGENTS.md`];
8280
+ }
8281
+ async function planOf(cli, folder, cwd, packageRoot) {
8282
+ const source = join(packageRoot, folder);
8283
+ const { root, copy } = await skillCopyOf(cli, cwd);
8284
+ const [shipped, present] = await Promise.all([filesIn(source), filesIn(copy)]);
8285
+ const differs = await Promise.all(shipped.map(async (file) => present.includes(file) && !await sameContent(join(source, file), join(copy, file))));
8286
+ const agentsPath = join(root, "AGENTS.md");
8287
+ const block = agentsBlock(cli);
8288
+ return {
8289
+ source,
8290
+ copy,
8291
+ write: shipped.filter((file) => !present.includes(file)),
8292
+ replace: shipped.filter((_file, index) => differs[index]),
8293
+ remove: present.filter((file) => !shipped.includes(file)),
8294
+ sameVersion: (await skillVersionsOf(source, copy)).same,
8295
+ agentsMd: {
8296
+ path: agentsPath,
8297
+ block,
8298
+ change: blockChangeOf(await orMissing(readFile(agentsPath, "utf8")), block, markersOf(cli.name))
8299
+ },
8300
+ handEdits: await handEditsAt(root)
8032
8301
  };
8033
8302
  }
8034
- //#endregion
8035
- //#region ../agent-cli/dist/confirm.js
8036
- const CONFIRMATION_REQUIRED = "CONFIRMATION_REQUIRED";
8037
- const CONFIRMATION_MISMATCH = "CONFIRMATION_MISMATCH";
8038
- function checkConfirmation({ invocation, commandLine, given }, expected) {
8039
- if (given === expected) return;
8040
- const hint = `to confirm, run \`${commandLineOf(invocation, {
8041
- remove: [CONFIRM_FLAG.flag],
8042
- add: [CONFIRM_FLAG.flag, expected]
8043
- })}\``;
8044
- if (given === void 0) throw new UsageError({
8045
- code: CONFIRMATION_REQUIRED,
8046
- message: `\`${commandLine}\` is destructive and needs ${CONFIRM_FLAG.flag}`,
8047
- hint
8048
- });
8049
- throw new UsageError({
8050
- code: CONFIRMATION_MISMATCH,
8051
- message: `${CONFIRM_FLAG.flag} ${JSON.stringify(given)} does not match the target of \`${commandLine}\``,
8052
- hint
8303
+ const BLOCK_VERBS = {
8304
+ create: "create",
8305
+ add: "add the block to",
8306
+ rewrite: "rewrite the block in"
8307
+ };
8308
+ function renderPlan(cli, plan) {
8309
+ const files = [
8310
+ ...plan.write.map((file) => ` write ${file}`),
8311
+ ...plan.replace.map((file) => ` replace ${file}`),
8312
+ ...plan.remove.map((file) => ` remove ${file}`)
8313
+ ];
8314
+ const { path, block, change } = plan.agentsMd;
8315
+ return [
8316
+ files.length === 0 ? `the ${cli.name} skill at ${plan.copy} already matches the package` : `would copy the ${cli.name} skill to ${plan.copy}:`,
8317
+ ...files,
8318
+ change === null ? `${path} already holds the ${cli.name} block` : `would ${BLOCK_VERBS[change]} ${path}:\n${block}`,
8319
+ ...plan.handEdits.map((edit) => `by hand: ${edit}`)
8320
+ ].join("\n");
8321
+ }
8322
+ function renderInstalled(cli, data) {
8323
+ const { written, removed, agentsMd } = data;
8324
+ return [
8325
+ 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}`,
8326
+ agentsMd.changed ? `wrote the ${cli.name} block in ${agentsMd.path}` : `${agentsMd.path} already holds the ${cli.name} block`,
8327
+ ...data.handEdits.map((edit) => `by hand: ${edit}`)
8328
+ ].join("\n");
8329
+ }
8330
+ function editedRemovals(plan, ctx) {
8331
+ const them = plan.remove.length === 1 ? "it" : "them";
8332
+ return new CliError({
8333
+ code: OUTPUT_EXISTS,
8334
+ message: `\`${ctx.commandLine()}\` would remove ${plan.remove.map((file) => join(plan.copy, file)).join(", ")}`,
8335
+ hint: `to remove ${them}, run \`${ctx.commandLine({ add: [FORCE_FLAG.flag] })}\``
8053
8336
  });
8054
8337
  }
8055
- //#endregion
8056
- //#region ../agent-cli/dist/cost.js
8057
- const NOT_CONFIRMED = "NOT_CONFIRMED";
8058
- /** Refuses a run whose Plan states a cost the caller has not approved with `--yes`. */
8059
- function checkCost({ invocation, commandLine, yes }, cost) {
8060
- if (cost === void 0 || yes) return;
8061
- throw new CliError({
8062
- code: NOT_CONFIRMED,
8063
- message: `\`${commandLine}\` needs ${YES_FLAG.flag}: ${cost}`,
8064
- hint: `to approve the cost, run \`${commandLineOf(invocation, { add: [YES_FLAG.flag] })}\``
8338
+ /** The built-in `skill install`, for a CLI whose skill ships in its package. */
8339
+ function skillInstallCommand(cli, folder) {
8340
+ const runAs = runAsOf(cli);
8341
+ return defineCommand$1({
8342
+ name: "install",
8343
+ description: "copy this CLI's skill into the project and write its AGENTS.md block",
8344
+ examples: [{
8345
+ command: `${runAs} skill install`,
8346
+ note: "install the skill this version ships"
8347
+ }, {
8348
+ command: `${runAs} skill install --dry-run`,
8349
+ note: "show what it would change"
8350
+ }],
8351
+ effect: "write",
8352
+ output: installSchema,
8353
+ force: "replace or remove hand-edited files of a skill copy at the package's version",
8354
+ plan: (_input, ctx) => planOf(cli, folder, ctx.cwd, ctx.packageRoot),
8355
+ outputs: (plan) => plan.sameVersion ? [...plan.write, ...plan.replace].map((file) => join(plan.copy, file)) : [],
8356
+ apply: async (plan, ctx) => {
8357
+ if (plan.sameVersion && plan.remove.length > 0 && !ctx.force) throw editedRemovals(plan, ctx);
8358
+ for (const file of [...plan.write, ...plan.replace]) await ctx.files.write(join(plan.copy, file), await readFile(join(plan.source, file)));
8359
+ for (const file of plan.remove) await ctx.files.remove(join(plan.copy, file));
8360
+ const { path, block, change } = plan.agentsMd;
8361
+ if (change !== null) {
8362
+ const existing = await orMissing(readFile(path, "utf8")) ?? "";
8363
+ await ctx.files.write(path, withBlock(existing, block, markersOf(cli.name)));
8364
+ }
8365
+ return {
8366
+ copy: plan.copy,
8367
+ written: [...plan.write, ...plan.replace].sort(),
8368
+ removed: [...plan.remove],
8369
+ agentsMd: {
8370
+ path,
8371
+ changed: change !== null
8372
+ },
8373
+ handEdits: [...plan.handEdits]
8374
+ };
8375
+ },
8376
+ render: (data) => renderInstalled(cli, data),
8377
+ renderPlan: (plan) => renderPlan(cli, plan)
8065
8378
  });
8066
8379
  }
8067
8380
  //#endregion
8068
- //#region ../agent-cli/dist/agent.js
8069
- const AGENT_NAME = /^[a-zA-Z0-9_-]+$/u;
8070
- const isSet = (name) => (env) => (env[name] ?? "") !== "";
8071
- const equals = (name, value) => (env) => env[name] === value;
8072
- const MARKERS = [
8073
- (env) => AGENT_NAME.test(env.AI_AGENT ?? ""),
8074
- equals("AGENT", "amp"),
8075
- isSet("CODEX_SANDBOX"),
8076
- isSet("CODEX_CI"),
8077
- isSet("CODEX_THREAD_ID"),
8078
- isSet("GEMINI_CLI"),
8079
- isSet("COPILOT_CLI"),
8080
- isSet("OPENCODE"),
8081
- isSet("ANTIGRAVITY_AGENT"),
8082
- isSet("AUGMENT_AGENT"),
8083
- isSet("REPL_ID"),
8084
- isSet("CLAUDE_CODE_IS_COWORK"),
8085
- isSet("CLAUDECODE"),
8086
- isSet("CLAUDE_CODE"),
8087
- isSet("CURSOR_TRACE_ID"),
8088
- isSet("CURSOR_AGENT"),
8089
- equals("CURSOR_EXTENSION_HOST_ROLE", "agent-exec"),
8090
- equals("TERM_PROGRAM", "kiro"),
8091
- (env) => ["/.pi/agent", "\\.pi\\agent"].some((dir) => (env.PATH ?? "").includes(dir)),
8092
- isSet("GOOSE_PROVIDER")
8093
- ];
8094
- /**
8095
- * Whether a Coding agent, not a person, is running the Agent CLI. Some Coding agents run commands
8096
- * in a PTY, so a TTY alone doesn't mean a person is watching.
8097
- */
8098
- function isAgent(env) {
8099
- return MARKERS.some((marker) => marker(env));
8381
+ //#region ../agent-cli/dist/interrupts.js
8382
+ const INTERRUPTED = "INTERRUPTED";
8383
+ const DEADLINE_EXCEEDED = "DEADLINE_EXCEEDED";
8384
+ /** How long an interrupted Command may take to settle before the run ends without it. */
8385
+ const GRACE_MS = 3e3;
8386
+ const GRACE_OVER = Symbol("grace over");
8387
+ function interruptedError(callerLine) {
8388
+ return new CliError({
8389
+ code: INTERRUPTED,
8390
+ message: "interrupted before it finished",
8391
+ hint: `to finish it, run \`${callerLine}\` again, unless it was stopped on purpose; if a tool timeout stopped it, give it longer`
8392
+ });
8100
8393
  }
8101
- //#endregion
8102
- //#region ../agent-cli/dist/errno.js
8103
- const PERMISSION_DENIED = "PERMISSION_DENIED";
8104
- const DISK_FULL = "DISK_FULL";
8105
- const NOT_FOUND = "NOT_FOUND";
8106
- const ALREADY_EXISTS = "ALREADY_EXISTS";
8107
- const NOT_A_FILE = "NOT_A_FILE";
8108
- function permissionHint({ path, access, callerLine, env, platform }) {
8109
- if (platform === "win32") return `a program may hold ${path} open: close it, then run \`${callerLine}\` again`;
8110
- if (!isAgent(env)) return `check the permissions of ${path} and its folder`;
8111
- 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`}`;
8394
+ function waitHint(callerLine, bound) {
8395
+ return bound === "timeout" ? `the call may have been billed: ask your user before running \`${callerLine}\` again` : `run \`${callerLine}\` again later, when the service it waits on may answer faster`;
8112
8396
  }
8113
- function fileErrorOf(scope) {
8114
- return (error, at) => classifyErrno(error, {
8115
- ...at,
8116
- ...scope
8397
+ function deadlineError(commandLine, ms, hint, bound = "deadline") {
8398
+ return new CliError({
8399
+ code: DEADLINE_EXCEEDED,
8400
+ message: `${commandLine} ran past its ${ms} ms ${bound}`,
8401
+ hint
8117
8402
  });
8118
8403
  }
8119
- function classifyErrno(error, context) {
8120
- const { path } = context;
8121
- switch (errnoOf(error)) {
8122
- case "EACCES":
8123
- case "EPERM":
8124
- case "EROFS":
8125
- case "EBUSY": return new CliError({
8126
- code: PERMISSION_DENIED,
8127
- message: `permission denied: ${path}`,
8128
- hint: permissionHint(context)
8129
- }, { cause: error });
8130
- case "ENOSPC":
8131
- case "EDQUOT": return new CliError({
8132
- code: DISK_FULL,
8133
- message: `no space left on the disk holding ${path}`,
8134
- hint: `free space on that disk, then run \`${context.callerLine}\` again`
8135
- }, { cause: error });
8136
- case "ENOENT":
8137
- case "ENOTDIR":
8138
- case "ENAMETOOLONG": return new CliError({
8139
- code: NOT_FOUND,
8140
- message: `not found: ${path}`,
8141
- hint: `check that ${path} and its folder exist`
8142
- }, { cause: error });
8143
- case "EEXIST": return new CliError({
8144
- code: ALREADY_EXISTS,
8145
- message: `already exists: ${path}`,
8146
- hint: `pass another path, or remove ${path}`
8147
- }, { cause: error });
8148
- case "EISDIR": return new CliError({
8149
- code: NOT_A_FILE,
8150
- message: `not a file: ${path} is a folder`,
8151
- hint: `pass the path of a file, not of the folder ${path}`
8152
- }, { cause: error });
8153
- default: return error;
8404
+ /** One Command run's cancellation: its signal, its deadlines and the steps it must finish. */
8405
+ var RunControl = class {
8406
+ signal;
8407
+ #controller = new AbortController();
8408
+ #commandLine;
8409
+ #callerLine;
8410
+ #outer;
8411
+ #onOuterAbort = () => this.#controller.abort(interruptedError(this.#callerLine));
8412
+ /** Each running must-finish step, tracked until it settles, with its name. */
8413
+ #steps = /* @__PURE__ */ new Map();
8414
+ #progress;
8415
+ #clock;
8416
+ #disposed = new AbortController();
8417
+ /** The reasons this run's deadlines and timeouts aborted with, to tell them in a Command's error. */
8418
+ #abortReasons = /* @__PURE__ */ new WeakSet();
8419
+ #graceCancel = new AbortController();
8420
+ #endGrace = () => {};
8421
+ #graceOver = new Promise((resolve) => {
8422
+ this.#endGrace = resolve;
8423
+ });
8424
+ constructor({ commandLine, callerLine, outer, clock, progress, runDeadline }) {
8425
+ this.#commandLine = commandLine;
8426
+ this.#callerLine = callerLine;
8427
+ this.#outer = outer;
8428
+ this.#clock = clock;
8429
+ this.#progress = progress;
8430
+ this.signal = this.#controller.signal;
8431
+ this.signal.addEventListener("abort", () => this.#onStop(), { once: true });
8432
+ if (outer?.aborted === true) this.#onOuterAbort();
8433
+ else outer?.addEventListener("abort", this.#onOuterAbort, { once: true });
8434
+ if (runDeadline !== void 0) {
8435
+ const { ms, hint } = runDeadline;
8436
+ after(clock, ms, this.#disposed.signal, () => this.#controller.abort(deadlineError(commandLine, ms, hint())));
8437
+ }
8438
+ }
8439
+ deadline(ms) {
8440
+ return AbortSignal.any([this.signal, this.#abortsAfter(ms, "deadline")]);
8441
+ }
8442
+ timeout(ms) {
8443
+ return this.#abortsAfter(ms, "timeout");
8444
+ }
8445
+ #abortsAfter(ms, bound) {
8446
+ const controller = new AbortController();
8447
+ after(this.#clock, ms, this.#disposed.signal, () => {
8448
+ const reason = deadlineError(this.#commandLine, ms, waitHint(this.#callerLine, bound), bound);
8449
+ this.#abortReasons.add(reason);
8450
+ controller.abort(reason);
8451
+ });
8452
+ return controller.signal;
8453
+ }
8454
+ async runToEnd(name, work) {
8455
+ this.#graceCancel.abort();
8456
+ if (this.signal.aborted) this.#finishing(name);
8457
+ const step = work();
8458
+ const settled = step.then(() => {}, () => {});
8459
+ this.#steps.set(settled, name);
8460
+ try {
8461
+ return await step;
8462
+ } finally {
8463
+ this.#steps.delete(settled);
8464
+ this.#startGrace();
8465
+ }
8466
+ }
8467
+ /** Waits for the Command, its must-finish steps, and no longer than the grace once interrupted. */
8468
+ async settle(work) {
8469
+ try {
8470
+ const value = await Promise.race([work, this.#graceOver]);
8471
+ if (this.signal.aborted || value === GRACE_OVER) throw this.signal.reason;
8472
+ return value;
8473
+ } finally {
8474
+ await this.#stepsDone();
8475
+ this.close();
8476
+ }
8477
+ }
8478
+ /**
8479
+ * The error a run ends with: an interrupt or the run deadline outranks what the Command threw,
8480
+ * unless the Command threw its own `INTERRUPTED`, saying what it finished before it stopped.
8481
+ * Otherwise each leaf that is not a `CliError` and whose cause chain holds a deadline's or timeout's
8482
+ * abort is reported as that abort, so a declared error keeps its own code.
8483
+ */
8484
+ errorAfter(error) {
8485
+ if (this.signal.aborted) return error instanceof CliError && error.code === "INTERRUPTED" ? error : this.signal.reason;
8486
+ const leaves = errorsOf(error).map((leaf) => this.#abortIn(leaf) ?? leaf);
8487
+ return leaves.length === 1 ? leaves[0] : new AggregateError(leaves);
8488
+ }
8489
+ #abortIn(error) {
8490
+ if (error instanceof CliError) return void 0;
8491
+ return causesOf(error).find((each) => each instanceof CliError && this.#abortReasons.has(each));
8154
8492
  }
8155
- }
8156
- //#endregion
8157
- //#region ../agent-cli/dist/operator.js
8158
- const NEEDS_OPERATOR = "NEEDS_OPERATOR";
8159
- /** Fails with `NEEDS_OPERATOR` when a Coding agent runs an Operator Command. */
8160
- function checkOperator({ operator, commandLine }, env, callerLine) {
8161
- if (!operator || !isAgent(env)) return;
8162
- throw new CliError({
8163
- code: NEEDS_OPERATOR,
8164
- message: `\`${commandLine}\` must be run by a person at a terminal, not a Coding agent`,
8165
- hint: `ask the operator to type: ! ${callerLine}`
8166
- });
8167
- }
8493
+ #onStop() {
8494
+ for (const name of this.#steps.values()) this.#finishing(name);
8495
+ this.#startGrace();
8496
+ }
8497
+ #finishing(name) {
8498
+ this.#progress(`finishing ${name}`);
8499
+ }
8500
+ #startGrace() {
8501
+ if (this.signal.aborted && this.#steps.size === 0) {
8502
+ this.#graceCancel.abort();
8503
+ this.#graceCancel = new AbortController();
8504
+ after(this.#clock, GRACE_MS, this.#graceCancel.signal, () => this.#endGrace(GRACE_OVER));
8505
+ }
8506
+ }
8507
+ async #stepsDone() {
8508
+ while (this.#steps.size > 0) await Promise.all(this.#steps.keys());
8509
+ }
8510
+ /** Stops the run's timers and its hold on the caller's signal, for a run that never settles. */
8511
+ close() {
8512
+ this.#graceCancel.abort();
8513
+ this.#disposed.abort();
8514
+ this.#outer?.removeEventListener("abort", this.#onOuterAbort);
8515
+ }
8516
+ };
8168
8517
  //#endregion
8169
- //#region ../agent-cli/dist/files.js
8170
- const TEMP_FILE = /^\..+\.agent-cli-(\d+)\.tmp$/;
8171
- /** Where the Runtime writes `path` before moving it into place; a dead writer's is swept. */
8172
- function tempPathOf(path) {
8173
- return join(dirname(path), `.${basename(path)}.agent-cli-${process.pid}.tmp`);
8174
- }
8175
- function isAlive(pid) {
8176
- try {
8177
- process.kill(pid, 0);
8178
- return true;
8179
- } catch (error) {
8180
- return errnoOf(error) === "EPERM";
8518
+ //#region ../agent-cli/dist/built-ins/upgrades.js
8519
+ /** A skipped release's Upgrade steps, or its whole section when it lists none. */
8520
+ const upgradeSchema = union([object({
8521
+ version: string(),
8522
+ steps: array(string())
8523
+ }), object({
8524
+ version: string(),
8525
+ notes: string()
8526
+ })]);
8527
+ const RELEASE_HEADING = /^## v?(\S+)/;
8528
+ const UPGRADE_HEADING = /^### Upgrade from v\S+$/;
8529
+ const HEADING = /^#{1,6} /;
8530
+ const ITEM = /^[-*] /;
8531
+ const NOTHING_TO_DO = /^nothing to do\.?$/i;
8532
+ function releasesIn(changelog) {
8533
+ const releases = [];
8534
+ let current;
8535
+ for (const line of changelog.split(/\r?\n/)) {
8536
+ const heading = RELEASE_HEADING.exec(line);
8537
+ if (heading !== null) {
8538
+ const version = heading[1] ?? "";
8539
+ current = isVersion(version) ? {
8540
+ version,
8541
+ lines: []
8542
+ } : void 0;
8543
+ if (current !== void 0) releases.push(current);
8544
+ } else current?.lines.push(line);
8181
8545
  }
8546
+ return releases;
8182
8547
  }
8183
- async function sweep(folder) {
8184
- const dead = (await readdir(folder)).filter((name) => {
8185
- const pid = TEMP_FILE.exec(name)?.[1];
8186
- return pid !== void 0 && !isAlive(Number(pid));
8187
- });
8188
- await Promise.all(dead.map((name) => rm(join(folder, name), { force: true })));
8548
+ function listedSteps(lines) {
8549
+ const start = lines.findIndex((line) => UPGRADE_HEADING.test(line.trimEnd()));
8550
+ if (start === -1) return [];
8551
+ const end = lines.findIndex((line, i) => i > start && HEADING.test(line));
8552
+ const body = lines.slice(start + 1, end === -1 ? void 0 : end).filter((line) => line.trim() !== "");
8553
+ const starts = body.flatMap((line, i) => ITEM.test(line) ? [i] : []);
8554
+ return starts.map((first, i) => body.slice(first, starts[i + 1]).map((line) => line.trim()).join(" ").replace(ITEM, ""));
8189
8555
  }
8190
- /** The file a symlink at `path` points to, so a replacing write keeps the link. */
8191
- async function targetOf$1(path) {
8556
+ function upgradeOf({ version, lines }) {
8557
+ const listed = listedSteps(lines);
8558
+ if (listed.length === 0) return [{
8559
+ version,
8560
+ notes: lines.join("\n").trim()
8561
+ }];
8562
+ const todo = listed.filter((step) => !NOTHING_TO_DO.test(step));
8563
+ return todo.length === 0 ? [] : [{
8564
+ version,
8565
+ steps: todo
8566
+ }];
8567
+ }
8568
+ /**
8569
+ * What each release after `from` up to `to` asks of the operator, oldest first, from the
8570
+ * CHANGELOG.md in `packageDir`; none when the package ships no CHANGELOG.
8571
+ */
8572
+ async function upgradesOf(packageDir, from, to) {
8573
+ let changelog;
8192
8574
  try {
8193
- return await realpath(path);
8575
+ changelog = await readFile(join(packageDir, "CHANGELOG.md"), "utf8");
8194
8576
  } catch (error) {
8195
- if (isMissingFile(error)) return path;
8577
+ if (isMissingFile(error)) return [];
8196
8578
  throw error;
8197
8579
  }
8580
+ return releasesIn(changelog).filter(({ version }) => compareVersions(version, from) > 0 && compareVersions(version, to) <= 0).sort((a, b) => compareVersions(a.version, b.version)).flatMap(upgradeOf);
8198
8581
  }
8199
- async function writeWithMode(path, data, mode) {
8200
- if (mode === void 0) {
8201
- await writeFile(path, data);
8202
- return;
8203
- }
8204
- await writeFile(path, data, { mode });
8205
- await chmod(path, mode);
8582
+ //#endregion
8583
+ //#region ../agent-cli/dist/built-ins/update.js
8584
+ const updateSchema = object({
8585
+ from: string(),
8586
+ latest: string(),
8587
+ installed: object({
8588
+ version: string(),
8589
+ prefix: string()
8590
+ }).nullable(),
8591
+ upgrades: array(upgradeSchema),
8592
+ next: array(string())
8593
+ });
8594
+ function nothingToInstall({ from, latest }) {
8595
+ return standingOf$1(from, latest) === "current" ? `already on ${from}, the latest release: nothing to install` : `${from} is newer than the latest release ${latest}: nothing to install`;
8206
8596
  }
8207
- /** What `link` answers on a file system without hard links, such as exFAT. */
8208
- const NO_HARD_LINKS = /* @__PURE__ */ new Set([
8209
- "EPERM",
8210
- "ENOTSUP",
8211
- "EXDEV"
8212
- ]);
8213
- /** A link fails atomically when `path` is taken, so no reader ever sees half a file there. */
8214
- async function linkNew(temp, path, data, mode) {
8215
- try {
8216
- await link(temp, path);
8217
- } catch (error) {
8218
- if (!NO_HARD_LINKS.has(errnoOf(error))) throw error;
8219
- await createNew(path, data, mode);
8220
- }
8597
+ function renderUpgrade(upgrade) {
8598
+ return "steps" in upgrade ? [`\nupgrade steps for ${upgrade.version}:`, ...upgrade.steps.map((step) => `- ${step}`)].join("\n") : `\nchanges in ${upgrade.version}:\n${upgrade.notes}`;
8221
8599
  }
8222
- /** A file this call created and could not finish is removed, so a rerun does not find half of it. */
8223
- async function createNew(path, data, mode) {
8224
- const handle = await open(path, "wx", mode);
8225
- try {
8226
- await handle.writeFile(data);
8227
- if (mode !== void 0) await handle.chmod(mode);
8228
- } catch (error) {
8229
- await handle.close();
8230
- await rm(path, { force: true });
8231
- throw error;
8232
- }
8233
- await handle.close();
8600
+ function renderUpdate(cli, install, update) {
8601
+ const { installed } = update;
8602
+ const head = installed === null ? [nothingToInstall(update)] : [`installed ${install.package}@${installed.version} with the npm under ${installed.prefix}, replacing ${update.from}`, ...update.upgrades.map(renderUpgrade)];
8603
+ const next = update.next.map((command) => `next: ${command}, so the ${cli.name} skill describes ${update.latest}`);
8604
+ return (next.length === 0 ? head : [
8605
+ ...head,
8606
+ "",
8607
+ ...next
8608
+ ]).join("\n");
8234
8609
  }
8235
- const RENAME_ATTEMPTS = 10;
8236
- const RENAME_BACKOFF_MS = 10;
8237
- const HELD_BY_ANOTHER_PROGRAM = /* @__PURE__ */ new Set(["EPERM", "EBUSY"]);
8238
- async function renameRetrying(from, to) {
8239
- for (let attempt = 1;; attempt++) try {
8240
- await rename(from, to);
8241
- return;
8242
- } catch (error) {
8243
- if (!(process.platform === "win32" && HELD_BY_ANOTHER_PROGRAM.has(errnoOf(error))) || attempt === RENAME_ATTEMPTS) throw error;
8244
- await setTimeout$1(attempt * RENAME_BACKOFF_MS);
8245
- }
8610
+ async function skillNext(cli, latest, ctx) {
8611
+ const packaged = packagedFolderOf(cli);
8612
+ const finding = packaged === void 0 ? await skillReport(cli, latest, ctx) : await packagedSkillFinding(cli, packaged, ctx);
8613
+ return "fix" in finding && finding.fix !== void 0 ? [finding.fix] : [];
8246
8614
  }
8247
- /** Real files for one run: the first write to each folder sweeps the temp files of dead writers. */
8248
- function realFiles() {
8249
- const swept = /* @__PURE__ */ new Set();
8250
- return {
8251
- exists: async (path) => {
8252
- try {
8253
- await access(path);
8254
- return true;
8255
- } catch (error) {
8256
- if (isNothingThere(error)) return false;
8257
- throw error;
8258
- }
8259
- },
8260
- write: async (path, data, options = {}) => {
8261
- await mkdir(dirname(path), { recursive: true });
8262
- const target = options.exclusive === true ? path : await targetOf$1(path);
8263
- const folder = dirname(target);
8264
- if (!swept.has(folder)) {
8265
- await sweep(folder);
8266
- swept.add(folder);
8267
- }
8268
- const temp = tempPathOf(target);
8269
- try {
8270
- await writeWithMode(temp, data, options.mode);
8271
- if (options.exclusive === true) {
8272
- await linkNew(temp, target, data, options.mode);
8273
- await rm(temp);
8274
- } else await renameRetrying(temp, target);
8275
- } catch (error) {
8276
- await rm(temp, { force: true });
8277
- throw error;
8278
- }
8279
- },
8280
- read: (path) => readFile(path),
8281
- list: (folder) => readdir(folder),
8282
- remove: (path) => rm(path, {
8283
- recursive: true,
8284
- force: true
8285
- }),
8286
- mkdir: async (path, options) => {
8287
- const mode = options?.mode;
8288
- if (options?.exclusive !== true) {
8289
- await mkdir(path, {
8290
- recursive: true,
8291
- mode
8292
- });
8293
- return;
8294
- }
8295
- await mkdir(dirname(path), {
8296
- recursive: true,
8297
- mode
8298
- });
8299
- await mkdir(path, { mode });
8615
+ function interruptedAfterInstall(plan) {
8616
+ return new CliError({
8617
+ code: INTERRUPTED,
8618
+ message: `interrupted, but ${plan.package}@${plan.latest} had already been installed with the npm under ${plan.prefix}, replacing ${plan.from}`,
8619
+ hint: "nothing to run again: run the commands under next"
8620
+ });
8621
+ }
8622
+ /** The built-in `update`, for a CLI installed globally with npm. */
8623
+ function updateCommand(cli, install, { bin, node }) {
8624
+ return defineCommand$1({
8625
+ name: "update",
8626
+ description: "install the latest version of this CLI from its registry",
8627
+ examples: [{
8628
+ command: `${cli.name} update`,
8629
+ note: "install the latest version and show what changed"
8630
+ }, {
8631
+ command: `${cli.name} update --dry-run`,
8632
+ note: "show the version it would install"
8633
+ }],
8634
+ effect: "write",
8635
+ output: updateSchema,
8636
+ plan: async (_input, ctx) => {
8637
+ const npm = globalNpm(cli, install, ctx, node);
8638
+ const copy = await globalInstallOf(bin, npm);
8639
+ return {
8640
+ package: install.package,
8641
+ from: cli.version,
8642
+ latest: await latestVersion(npm),
8643
+ prefix: copy.prefix,
8644
+ packageDir: copy.packageDir
8645
+ };
8300
8646
  },
8301
- stat: async (path) => {
8302
- try {
8303
- const { size, dev, ino } = await stat(path, { bigint: true });
8304
- return {
8305
- size: Number(size),
8306
- id: `${dev}:${ino}`
8307
- };
8308
- } catch (error) {
8309
- if (isNothingThere(error)) return void 0;
8310
- throw error;
8647
+ apply: async (plan, ctx) => {
8648
+ const { from, latest, prefix } = plan;
8649
+ if (standingOf$1(from, latest) !== "behind") return {
8650
+ from,
8651
+ latest,
8652
+ installed: null,
8653
+ upgrades: [],
8654
+ next: await skillNext(cli, latest, ctx)
8655
+ };
8656
+ ctx.signal.throwIfAborted();
8657
+ ctx.progress(`installing ${plan.package}@${latest} with the npm under ${prefix}…`);
8658
+ await ctx.runToEnd(`installing ${plan.package}@${latest}`, () => installGlobally(latest, globalNpm(cli, install, ctx, node)));
8659
+ const update = {
8660
+ from,
8661
+ latest,
8662
+ installed: {
8663
+ version: latest,
8664
+ prefix
8665
+ },
8666
+ upgrades: await upgradesOf(plan.packageDir, from, latest),
8667
+ next: await skillNext(cli, latest, ctx)
8668
+ };
8669
+ if (ctx.signal.aborted) {
8670
+ ctx.deliver(update);
8671
+ throw interruptedAfterInstall(plan);
8311
8672
  }
8312
- }
8313
- };
8673
+ return update;
8674
+ },
8675
+ render: (update) => renderUpdate(cli, install, update),
8676
+ renderPlan: (plan) => standingOf$1(plan.from, plan.latest) === "behind" ? `would install ${plan.package}@${plan.latest} with the npm under ${plan.prefix}, replacing ${plan.from}` : nothingToInstall(plan)
8677
+ });
8314
8678
  }
8679
+ //#endregion
8680
+ //#region ../agent-cli/dist/built-ins/commands.js
8315
8681
  /**
8316
- * `files` resolving paths against `cwd`, its failures classified by errno; `list` drops the
8317
- * Runtime's temp files and sorts.
8682
+ * The Commands the Runtime adds to a multi-Command CLI: `update` only for a Global install, and
8683
+ * `skill install` only for a Packaged skill.
8318
8684
  */
8319
- function classifiedFiles(files, { cwd, fileError }) {
8320
- async function at(access, path, work) {
8321
- const target = resolve(cwd, path);
8322
- try {
8323
- return await work(target);
8324
- } catch (error) {
8325
- throw fileError(error, {
8326
- path: target,
8327
- access
8328
- });
8329
- }
8330
- }
8685
+ function builtInCommands(cli, launch) {
8686
+ const packaged = packagedFolderOf(cli);
8331
8687
  return {
8332
- exists: (path) => at("read", path, (target) => files.exists(target)),
8333
- write: (path, data, options) => at("write", path, (target) => files.write(target, data, options)),
8334
- read: (path) => at("read", path, (target) => files.read(target)),
8335
- list: (folder) => at("read", folder, async (target) => (await files.list(target)).filter((name) => !TEMP_FILE.test(name)).sort()),
8336
- remove: (path) => at("write", path, (target) => files.remove(target)),
8337
- mkdir: (path, options) => at("write", path, (target) => files.mkdir(target, options)),
8338
- stat: (path) => at("read", path, (target) => files.stat(target))
8688
+ doctor: doctorCommand(cli, launch),
8689
+ ...cli.install?.mode === "global" ? { update: updateCommand(cli, cli.install, launch) } : {},
8690
+ ...packaged === void 0 ? {} : { skill: { install: skillInstallCommand(cli, packaged) } }
8339
8691
  };
8340
8692
  }
8341
8693
  //#endregion
8342
- //#region ../agent-cli/dist/outputs.js
8343
- const OUTPUT_EXISTS = "OUTPUT_EXISTS";
8344
- /** Refuses a run that would overwrite existing outputs unless the caller passed `--force`. */
8345
- async function checkOutputs({ invocation, commandLine, force, files }, outputs) {
8346
- if (outputs === void 0 || force) return;
8347
- const found = await Promise.all(outputs.map((path) => files.exists(path)));
8348
- const existing = outputs.filter((_path, index) => found[index]);
8349
- if (existing.length === 0) return;
8350
- const them = existing.length === 1 ? "it" : "them";
8351
- throw new CliError({
8352
- code: OUTPUT_EXISTS,
8353
- message: `\`${commandLine}\` would overwrite ${existing.join(", ")}`,
8354
- hint: `to replace ${them}, run \`${commandLineOf(invocation, { add: [FORCE_FLAG.flag] })}\``
8694
+ //#region ../agent-cli/dist/confirm.js
8695
+ const CONFIRMATION_REQUIRED = "CONFIRMATION_REQUIRED";
8696
+ const CONFIRMATION_MISMATCH = "CONFIRMATION_MISMATCH";
8697
+ function checkConfirmation({ invocation, commandLine, given }, expected) {
8698
+ if (given === expected) return;
8699
+ const hint = `to confirm, run \`${commandLineOf(invocation, {
8700
+ remove: [CONFIRM_FLAG.flag],
8701
+ add: [CONFIRM_FLAG.flag, expected]
8702
+ })}\``;
8703
+ if (given === void 0) throw new UsageError({
8704
+ code: CONFIRMATION_REQUIRED,
8705
+ message: `\`${commandLine}\` is destructive and needs ${CONFIRM_FLAG.flag}`,
8706
+ hint
8707
+ });
8708
+ throw new UsageError({
8709
+ code: CONFIRMATION_MISMATCH,
8710
+ message: `${CONFIRM_FLAG.flag} ${JSON.stringify(given)} does not match the target of \`${commandLine}\``,
8711
+ hint
8355
8712
  });
8356
8713
  }
8357
- /** Guards the writes to declared outputs of `files`, whose paths resolve against `cwd`. */
8358
- function outputsGuard(files, cwd) {
8359
- const unwritten = /* @__PURE__ */ new Set();
8360
- let forceLine = "";
8361
- const exclusiveWrite = async (path, data, options) => {
8362
- const target = resolve(cwd, path);
8363
- unwritten.delete(target);
8364
- try {
8365
- await files.write(path, data, {
8366
- ...options,
8367
- exclusive: true
8368
- });
8369
- } catch (error) {
8370
- unwritten.add(target);
8371
- if (!(error instanceof CliError) || error.code !== "ALREADY_EXISTS") throw error;
8372
- throw new CliError({
8373
- code: ALREADY_EXISTS,
8374
- message: error.message,
8375
- hint: `to replace it, run \`${forceLine}\``
8376
- }, { cause: error });
8377
- }
8378
- };
8379
- return {
8380
- files: {
8381
- ...files,
8382
- write: (path, data, options) => unwritten.has(resolve(cwd, path)) ? exclusiveWrite(path, data, options) : files.write(path, data, options)
8383
- },
8384
- guard: (outputs, line) => {
8385
- forceLine = line;
8386
- for (const path of outputs) unwritten.add(resolve(cwd, path));
8387
- }
8388
- };
8714
+ //#endregion
8715
+ //#region ../agent-cli/dist/cost.js
8716
+ const NOT_CONFIRMED = "NOT_CONFIRMED";
8717
+ /** Refuses a run whose Plan states a cost the caller has not approved with `--yes`. */
8718
+ function checkCost({ invocation, commandLine, yes }, cost) {
8719
+ if (cost === void 0 || yes) return;
8720
+ throw new CliError({
8721
+ code: NOT_CONFIRMED,
8722
+ message: `\`${commandLine}\` needs ${YES_FLAG.flag}: ${cost}`,
8723
+ hint: `to approve the cost, run \`${commandLineOf(invocation, { add: [YES_FLAG.flag] })}\``
8724
+ });
8389
8725
  }
8390
- /**
8391
- * Creates each output folder and a temp file in it, so a sandbox denial fails before `apply`
8392
- * spends anything. The temp file's name is the Runtime's, so a crashed run's leftover is swept.
8393
- */
8394
- async function probeOutputs({ files, cwd, fileError }, outputs) {
8395
- const folders = new Set(outputs.map((path) => dirname(resolve(cwd, path))));
8396
- await Promise.all([...folders].map(async (folder) => {
8397
- const probe = tempPathOf(join(folder, "outputs-probe"));
8398
- try {
8399
- await files.mkdir(folder);
8400
- await files.write(probe, "");
8401
- await files.remove(probe);
8402
- } catch (error) {
8403
- throw fileError(error, {
8404
- path: folder,
8405
- access: "write"
8406
- });
8407
- }
8408
- }));
8726
+ //#endregion
8727
+ //#region ../agent-cli/dist/operator.js
8728
+ const NEEDS_OPERATOR = "NEEDS_OPERATOR";
8729
+ /** Fails with `NEEDS_OPERATOR` when a Coding agent runs an Operator Command. */
8730
+ function checkOperator({ operator, commandLine }, env, callerLine) {
8731
+ if (!operator || !isAgent(env)) return;
8732
+ throw new CliError({
8733
+ code: NEEDS_OPERATOR,
8734
+ message: `\`${commandLine}\` must be run by a person at a terminal, not a Coding agent`,
8735
+ hint: `ask the operator to type: ! ${callerLine}`
8736
+ });
8409
8737
  }
8410
8738
  //#endregion
8411
8739
  //#region ../agent-cli/dist/exit-classes.js
@@ -8508,7 +8836,10 @@ function checkOwnCodes(cli) {
8508
8836
  }
8509
8837
  function defineCli$1(cli) {
8510
8838
  checkOwnCodes(cli);
8511
- if (cli.commands === void 0) return cli;
8839
+ if (cli.commands === void 0) {
8840
+ if (packagedFolderOf(cli) !== void 0) throw new Error(`${cli.name} declares a Packaged skill, which only a CLI with several Commands can install`);
8841
+ return cli;
8842
+ }
8512
8843
  const { commands } = cli;
8513
8844
  const taken = Object.keys(builtInCommands(cli, {
8514
8845
  bin: void 0,
@@ -8675,7 +9006,7 @@ function examplesSection(examples, style) {
8675
9006
  }
8676
9007
  /** The CLI's own help: a listing or its single Command's help, ending with exit codes and examples. */
8677
9008
  function cliHelp(cli, root, style) {
8678
- return lines(...root.kind === "command" ? commandLines(root, style, { examples: false }) : listingLines(`${root.commandLine} ${cli.version}`, root, style, cli.description), ...section("Exit codes", exitCodeRows(cli), style), ...examplesSection(rootExamples(root), style));
9009
+ return lines(...root.kind === "command" ? commandLines(root, style, { examples: false }) : listingLines(`${cli.name} ${cli.version}`, root, style, cli.description), ...section("Exit codes", exitCodeRows(cli), style), ...examplesSection(rootExamples(root), style));
8679
9010
  }
8680
9011
  function groupHelp(group, style) {
8681
9012
  return lines(...listingLines(group.commandLine, group, style));
@@ -8773,12 +9104,13 @@ function errorsData(errors) {
8773
9104
  details: declaration.details === void 0 ? null : toJSONSchema(declaration.details, { unrepresentable: "any" })
8774
9105
  }]));
8775
9106
  }
8776
- function cliHelpJson({ version, description, errors }, root) {
9107
+ function cliHelpJson({ name, version, description, errors }, root) {
8777
9108
  return printed$1({
8778
- name: root.commandLine,
9109
+ name,
8779
9110
  version,
8780
9111
  description,
8781
9112
  ...nodeData(root),
9113
+ name,
8782
9114
  errors: errorsData(errors ?? {})
8783
9115
  });
8784
9116
  }
@@ -8806,7 +9138,14 @@ function printed(value, render, { json, invocation, style, fields }) {
8806
9138
  }
8807
9139
  /** A progress message as stderr carries it: one JSON object per line under `--json` (ADR 0006). */
8808
9140
  function progressLine(message, json) {
8809
- return json ? `${JSON.stringify({ progress: message })}\n` : `${message}\n`;
9141
+ return stderrLine("progress", message, json);
9142
+ }
9143
+ /** A Notice as stderr carries it, a JSON line like progress under `--json`. */
9144
+ function noticeLine(text, json) {
9145
+ return stderrLine("notice", text, json);
9146
+ }
9147
+ function stderrLine(key, text, json) {
9148
+ return json ? `${JSON.stringify({ [key]: text })}\n` : `${text}\n`;
8810
9149
  }
8811
9150
  function errorText(error, format) {
8812
9151
  const text = formatRuntimeError(error, {
@@ -8827,7 +9166,7 @@ function startedOutput(outcome, format, spill) {
8827
9166
  fields: void 0
8828
9167
  }, spill);
8829
9168
  }
8830
- /** A serve Command stopped after it started: stopping it is how it is meant to end. */
9169
+ /** A serve Command stopped, or ended itself, after it started: that is how it is meant to end. */
8831
9170
  const STOPPED_RESULT = {
8832
9171
  stdout: "",
8833
9172
  stderr: "",
@@ -9053,6 +9392,10 @@ function dryRunOutcome(command, plan) {
9053
9392
  ].join("\n")
9054
9393
  };
9055
9394
  }
9395
+ function serveContextOf(ctx, end) {
9396
+ const serveCtx = Object.create(ctx);
9397
+ return Object.assign(serveCtx, { end });
9398
+ }
9056
9399
  async function execute(command, input, ctx, { dryRun, confirm, yes, page, invocation, commandLine, started, files, guardOutputs }) {
9057
9400
  await checkTools(command.tools ?? [], ctx.env, ctx.commandLine());
9058
9401
  if (command.rows !== void 0) return outcomeOf(await command.run({
@@ -9061,8 +9404,14 @@ async function execute(command, input, ctx, { dryRun, confirm, yes, page, invoca
9061
9404
  }, ctx), (data, style) => command.render(data, style));
9062
9405
  if (command.effect === "read") return outcomeOf(await command.run(input, ctx), (data, style) => command.render(data, style));
9063
9406
  if (command.effect === "serve") {
9064
- await started(outcomeOf(await command.start(input, ctx), (value, style) => command.render(value, style)));
9065
- return untilAborted(ctx.signal);
9407
+ let end = () => {};
9408
+ const ended = new Promise((resolve) => {
9409
+ end = resolve;
9410
+ });
9411
+ const outcome = outcomeOf(await command.start(input, serveContextOf(ctx, end)), (value, style) => command.render(value, style));
9412
+ await started(outcome);
9413
+ await Promise.race([ended, untilAborted(ctx.signal)]);
9414
+ return outcome;
9066
9415
  }
9067
9416
  const plan = await command.plan(input, ctx);
9068
9417
  if (dryRun) return dryRunOutcome(command, plan);
@@ -9404,6 +9753,13 @@ function selfOf(name, bin) {
9404
9753
  bin
9405
9754
  ];
9406
9755
  }
9756
+ const unfakedSpawnDetached = async () => {
9757
+ throw new Error("runCli was given a fake spawn but no spawnDetached: pass spawnDetached to runCli, so the test starts no real child");
9758
+ };
9759
+ function spawnDetachedOf({ spawn, spawnDetached }) {
9760
+ if (spawnDetached !== void 0) return spawnDetached;
9761
+ return spawn === void 0 ? realSpawnDetached : unfakedSpawnDetached;
9762
+ }
9407
9763
  function portsOf(cli, replacement) {
9408
9764
  if (replacement !== void 0) return () => replacement;
9409
9765
  return cli.ports ?? (() => ({}));
@@ -9437,11 +9793,19 @@ function portsBaseOf(scope, signal) {
9437
9793
  }, {
9438
9794
  fileError,
9439
9795
  callerLine
9796
+ }),
9797
+ spawnDetached: contextSpawnDetached(spawnDetachedOf(options), {
9798
+ cwd,
9799
+ env,
9800
+ signal
9801
+ }, {
9802
+ fileError,
9803
+ callerLine
9440
9804
  })
9441
9805
  };
9442
9806
  }
9443
9807
  function contextOf(portsBase, control, parts) {
9444
- const { ports, project, force, commandLine, deliver } = parts;
9808
+ const { ports, project, force, commandLine, deliver, notice } = parts;
9445
9809
  return {
9446
9810
  ...portsBase,
9447
9811
  deadline: (ms) => control.deadline(ms),
@@ -9454,7 +9818,8 @@ function contextOf(portsBase, control, parts) {
9454
9818
  force,
9455
9819
  commandLine,
9456
9820
  poll: (check, { intervalMs, signal: stop }) => poll(portsBase.clock, check, intervalMs, stop === void 0 ? control.signal : AbortSignal.any([control.signal, stop])),
9457
- deliver
9821
+ deliver,
9822
+ notice
9458
9823
  };
9459
9824
  }
9460
9825
  /** Leaves the Side task running, so the caller writes the result before it waits for it. */
@@ -9571,6 +9936,7 @@ async function runCommand(cli, routed, options, write, baseFormat) {
9571
9936
  project,
9572
9937
  force: flags.force,
9573
9938
  commandLine: typedLine,
9939
+ notice: (text) => write.stderr(noticeLine(text, routed.json)),
9574
9940
  deliver: (data) => {
9575
9941
  delivered = deliveredOutcome(command, data);
9576
9942
  }
@@ -9586,22 +9952,27 @@ async function runCommand(cli, routed, options, write, baseFormat) {
9586
9952
  let served = false;
9587
9953
  let settled;
9588
9954
  try {
9955
+ const outcome = await control.settle(execute(command, input, ctx, {
9956
+ dryRun: flags.dryRun,
9957
+ confirm: flags.confirm,
9958
+ yes: flags.yes,
9959
+ page: flags.page,
9960
+ invocation,
9961
+ commandLine,
9962
+ started: async (outcome) => {
9963
+ write.stdout(await startedOutput(outcome, format, spill));
9964
+ served = true;
9965
+ },
9966
+ files,
9967
+ guardOutputs: outputs.guard
9968
+ }));
9969
+ if (served) return {
9970
+ result: STOPPED_RESULT,
9971
+ finishSideTask
9972
+ };
9589
9973
  settled = {
9590
9974
  ok: true,
9591
- outcome: await control.settle(execute(command, input, ctx, {
9592
- dryRun: flags.dryRun,
9593
- confirm: flags.confirm,
9594
- yes: flags.yes,
9595
- page: flags.page,
9596
- invocation,
9597
- commandLine,
9598
- started: async (outcome) => {
9599
- write.stdout(await startedOutput(outcome, format, spill));
9600
- served = true;
9601
- },
9602
- files,
9603
- guardOutputs: outputs.guard
9604
- }))
9975
+ outcome
9605
9976
  };
9606
9977
  } catch (error) {
9607
9978
  if (served && control.signal.aborted) return {
@@ -9693,12 +10064,13 @@ async function runWithProgress(cli, argv, options, write, stopNotice) {
9693
10064
  return result;
9694
10065
  }
9695
10066
  async function routeAndRun(cli, argv, options, write, stopNotice) {
10067
+ const runAs = runAsOf(cli);
9696
10068
  const format = {
9697
10069
  cliName: cli.name,
9698
10070
  env: options.env,
9699
10071
  style: styleFor(options.env, options.stdoutIsTTY === true),
9700
10072
  invocation: {
9701
- cliName: cli.name,
10073
+ runAs,
9702
10074
  argv,
9703
10075
  flags: []
9704
10076
  },
@@ -9707,9 +10079,9 @@ async function routeAndRun(cli, argv, options, write, stopNotice) {
9707
10079
  stop: options.signal
9708
10080
  };
9709
10081
  const root = cli.commands === void 0 ? {
9710
- ...commandSpec(cli.name, cli.command),
10082
+ ...commandSpec(runAs, cli.command),
9711
10083
  description: cli.description
9712
- } : groupSpec(cli.name, cli.commands, builtInCommands(cli, {
10084
+ } : groupSpec(runAs, cli.commands, builtInCommands(cli, {
9713
10085
  bin: options.bin,
9714
10086
  node: options.node
9715
10087
  }));
@@ -9776,16 +10148,131 @@ async function runProcess(cli) {
9776
10148
  }
9777
10149
  //#endregion
9778
10150
  //#region package.json
9779
- var version$1 = "0.13.1";
10151
+ var version$1 = "0.14.0";
9780
10152
  var description = "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.";
9781
10153
  //#endregion
9782
10154
  //#region src/build.ts
9783
10155
  const build = {
9784
- "sha": "7b1baa9",
10156
+ "sha": "f114cf0",
9785
10157
  "dirty": false,
9786
- "builtAt": "2026-10-23T13:03:05.000Z"
10158
+ "builtAt": "2026-10-27T02:06:04.000Z"
9787
10159
  };
9788
10160
  //#endregion
10161
+ //#region src/fill.ts
10162
+ /** `text` with each `{{key}}` replaced by its value; `quoted` escapes values that land inside a double-quoted string. */
10163
+ function fill(text, values, quoted) {
10164
+ return text.replaceAll(/\{\{(\w+)\}\}/g, (placeholder, key) => {
10165
+ const value = Object.hasOwn(values, key) ? values[key] : void 0;
10166
+ if (value === void 0) throw new Error(`template placeholder ${placeholder} has no value`);
10167
+ return quoted ? JSON.stringify(value).slice(1, -1) : value;
10168
+ });
10169
+ }
10170
+ //#endregion
10171
+ //#region src/install-mode.ts
10172
+ const PARTIALS_DIR = fileURLToPath(new URL("../partials/", import.meta.url));
10173
+ const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
10174
+ const COMMANDS_ANCHOR = " commands: {";
10175
+ const GLOBAL_INSTALL = ` install: {
10176
+ mode: "global",
10177
+ package: manifest.name,
10178
+ registry: manifest.publishConfig.registry,
10179
+ },
10180
+ `;
10181
+ const NO_SKILL_HUB = "<!-- Set `skill.source` in `defineCli` and `skillHub` in `scripts/release.ts` to the git URL of the Skill Hub each release commits the skill to, then put here the `! npx -y skills add` line that `node dist/bin.js doctor` gives as the `skill` row's fix. -->";
10182
+ function installModeOf(skillHub, dependency, binName) {
10183
+ if (skillHub !== void 0 && dependency) throw new UsageError({
10184
+ code: "SKILL_HOMES_CONFLICT",
10185
+ message: "--skill-hub and --dependency each name where the skill comes from: a Dependency install ships it inside the package",
10186
+ hint: "pass --dependency for a CLI each project pins, or --skill-hub for a global one whose skill each release commits to a Skill Hub"
10187
+ });
10188
+ if ((skillHub !== void 0 || dependency) && !SKILL_NAME.test(binName)) throw new UsageError({
10189
+ code: "SKILL_NAME_INVALID",
10190
+ message: `${binName} cannot name a skill, whose names are lowercase letters, digits and single hyphens`,
10191
+ hint: `choose a name such as my-cli, or leave out ${dependency ? "--dependency" : "--skill-hub"}`
10192
+ });
10193
+ return dependency ? { mode: "dependency" } : {
10194
+ mode: "global",
10195
+ skillHub
10196
+ };
10197
+ }
10198
+ async function partialsOf(mode, values) {
10199
+ const partial = async (name) => fill(await readFile(join(PARTIALS_DIR, mode, `${name}.md`), "utf8"), values, false).trimEnd();
10200
+ const [installLines, skillCheck, installSteps, updateSteps, sandbox] = await Promise.all([
10201
+ partial("installLines"),
10202
+ partial("skillCheck"),
10203
+ partial("installSteps"),
10204
+ partial("updateSteps"),
10205
+ partial("sandbox")
10206
+ ]);
10207
+ return {
10208
+ installLines,
10209
+ skillCheck,
10210
+ installSteps,
10211
+ updateSteps,
10212
+ sandbox
10213
+ };
10214
+ }
10215
+ async function globalSetup(skillHub, packageName, binName) {
10216
+ const skillLine = skillHub === void 0 ? NO_SKILL_HUB : `\`! npx -y skills add ${skillHub} --skill ${binName} -g\``;
10217
+ const runAs = runAsOf({ name: binName });
10218
+ const url = JSON.stringify(skillHub);
10219
+ return {
10220
+ values: {
10221
+ runAs,
10222
+ doctorBin: "",
10223
+ ...await partialsOf("global", {
10224
+ packageName,
10225
+ binName,
10226
+ runAs,
10227
+ skillLine
10228
+ })
10229
+ },
10230
+ edits: skillHub === void 0 ? [] : [{
10231
+ file: "src/cli.ts",
10232
+ find: COMMANDS_ANCHOR,
10233
+ replace: ` skill: { source: ${url} },\n${COMMANDS_ANCHOR}`
10234
+ }, {
10235
+ file: "scripts/release.ts",
10236
+ find: " smoke: {",
10237
+ replace: ` skillHub: ${url},\n smoke: {`
10238
+ }],
10239
+ files: []
10240
+ };
10241
+ }
10242
+ async function dependencySetup(packageName, binName) {
10243
+ const runAs = runAsOf({
10244
+ name: binName,
10245
+ install: {
10246
+ mode: "dependency",
10247
+ package: packageName
10248
+ }
10249
+ });
10250
+ return {
10251
+ values: {
10252
+ runAs,
10253
+ doctorBin: "src/bin.ts",
10254
+ ...await partialsOf("dependency", {
10255
+ packageName,
10256
+ binName,
10257
+ runAs
10258
+ })
10259
+ },
10260
+ edits: [{
10261
+ file: "src/cli.ts",
10262
+ find: GLOBAL_INSTALL,
10263
+ replace: ` install: { mode: "dependency", package: manifest.name },\n`
10264
+ }, {
10265
+ file: "src/cli.ts",
10266
+ find: COMMANDS_ANCHOR,
10267
+ replace: ` skill: { packaged: "skills/${binName}" },\n${COMMANDS_ANCHOR}`
10268
+ }],
10269
+ files: ["skills"]
10270
+ };
10271
+ }
10272
+ async function installSetupOf(install, packageName, binName) {
10273
+ return install.mode === "global" ? globalSetup(install.skillHub, packageName, binName) : dependencySetup(packageName, binName);
10274
+ }
10275
+ //#endregion
9789
10276
  //#region src/steps.ts
9790
10277
  const stepErrors = defineErrors({ STEP_FAILED: { class: "operator" } });
9791
10278
  const INSTALL = [{
@@ -9895,13 +10382,6 @@ async function templateFiles(values) {
9895
10382
  }));
9896
10383
  }
9897
10384
  const QUOTED_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".ts"]);
9898
- function fill(text, values, quoted) {
9899
- return text.replaceAll(/\{\{(\w+)\}\}/g, (placeholder, key) => {
9900
- if (!Object.hasOwn(values, key)) throw new Error(`template placeholder ${placeholder} has no value`);
9901
- const value = values[key];
9902
- return quoted ? JSON.stringify(value).slice(1, -1) : value;
9903
- });
9904
- }
9905
10385
  async function writeTemplate(root, files, values) {
9906
10386
  for (const file of files) {
9907
10387
  const text = await readFile(join(TEMPLATE_DIR, file.source), "utf8");
@@ -9917,17 +10397,6 @@ const version = version$1;
9917
10397
  //#region src/create.ts
9918
10398
  const { defineCommand } = agentCli();
9919
10399
  const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?([a-z0-9][a-z0-9._-]*)$/;
9920
- const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9921
- const NO_SKILL_HUB = "<!-- Set `skill.source` in `defineCli` and `skillHub` in `scripts/release.ts` to the git URL of the Skill Hub each release commits the skill to, then put here the `! npx -y skills add` line that `node dist/bin.js doctor` gives as the `skill` row's fix. -->";
9922
- const SKILL_HUB_ANCHORS = [{
9923
- file: "src/cli.ts",
9924
- before: " commands: {",
9925
- line: (hub) => ` skill: { source: ${JSON.stringify(hub)} },\n`
9926
- }, {
9927
- file: "scripts/release.ts",
9928
- before: " smoke: {",
9929
- line: (hub) => ` skillHub: ${JSON.stringify(hub)},\n`
9930
- }];
9931
10400
  function errorCode(error) {
9932
10401
  return error instanceof Error && "code" in error ? error.code : void 0;
9933
10402
  }
@@ -9940,30 +10409,24 @@ async function isFree(path) {
9940
10409
  throw error;
9941
10410
  }
9942
10411
  }
9943
- function skillInstallLine(skillHub, binName) {
9944
- return skillHub === void 0 ? NO_SKILL_HUB : `\`! npx -y skills add ${skillHub} --skill ${binName} -g\``;
9945
- }
9946
- async function setSkillHub(root, skillHub) {
9947
- for (const { file, before, line } of SKILL_HUB_ANCHORS) {
9948
- const path = join(root, file);
9949
- const text = await readFile(path, "utf8");
9950
- if (!text.includes(before)) throw new Error(`the template's ${file} has no \`${before}\` line to put the Skill Hub before`);
9951
- await writeFile(path, text.replace(before, `${line(skillHub)}${before}`));
9952
- }
10412
+ async function applyEdit(root, { file, find, replace }) {
10413
+ const path = join(root, file);
10414
+ const text = await readFile(path, "utf8");
10415
+ if (!text.includes(find)) throw new Error(`the template's ${file} has no \`${find.trim()}\` to replace`);
10416
+ await writeFile(path, text.replace(find, replace));
9953
10417
  }
9954
10418
  const MANIFEST = record(string(), unknown());
9955
- async function setRepository(root, repository) {
10419
+ async function editManifest(root, edit) {
9956
10420
  const path = join(root, "package.json");
9957
10421
  const manifest = MANIFEST.parse(JSON.parse(await readFile(path, "utf8")));
9958
- const entries = Object.entries(manifest).flatMap((entry) => entry[0] === "description" ? [entry, ["repository", repository]] : [entry]);
10422
+ const entries = edit(Object.entries(manifest));
9959
10423
  await writeFile(path, `${JSON.stringify(Object.fromEntries(entries), null, 2)}\n`);
9960
10424
  }
9961
- function ensureSkillName(binName) {
9962
- if (!SKILL_NAME.test(binName)) throw new UsageError({
9963
- code: "SKILL_NAME_INVALID",
9964
- message: `${binName} cannot name a skill in a Skill Hub, whose names are lowercase letters, digits and single hyphens`,
9965
- hint: "choose a name such as my-cli, or leave out --skill-hub"
9966
- });
10425
+ async function setRepository(root, repository) {
10426
+ await editManifest(root, (entries) => entries.flatMap((entry) => entry[0] === "description" ? [entry, ["repository", repository]] : [entry]));
10427
+ }
10428
+ async function addFiles(root, files) {
10429
+ await editManifest(root, (entries) => entries.map(([key, value]) => key === "files" && Array.isArray(value) ? [key, [...value, ...files]] : [key, value]));
9967
10430
  }
9968
10431
  async function ensureFree(path) {
9969
10432
  if (!await isFree(path)) throw new UsageError({
@@ -9986,20 +10449,22 @@ const create = defineCommand({
9986
10449
  install: boolean().default(true).describe("run pnpm install"),
9987
10450
  git: boolean().default(true).describe("create a git repository with an initial commit"),
9988
10451
  repository: url({ protocol: /^https$/ }).optional().describe("the https URL of the CLI's git repository"),
9989
- skillHub: url({ protocol: /^https$/ }).refine((url) => new URL(url).search === "" && new URL(url).hash === "", "must name the hub's repository with no query or fragment").refine((url) => new URL(url).pathname.endsWith(".git"), "must end in .git").optional().describe("the https git URL of the Skill Hub each release commits the CLI's skill to")
10452
+ skillHub: url({ protocol: /^https$/ }).refine((url) => new URL(url).search === "" && new URL(url).hash === "", "must name the hub's repository with no query or fragment").refine((url) => new URL(url).pathname.endsWith(".git"), "must end in .git").optional().describe("the https git URL of the Skill Hub each release commits the CLI's skill to"),
10453
+ dependency: boolean().default(false).describe("install the CLI per project, as a devDependency, with its skill shipped inside the package")
9990
10454
  }),
9991
- plan: async ({ name, description, install, git, repository, skillHub }, ctx) => {
10455
+ plan: async ({ name, description, install, git, repository, skillHub, dependency }, ctx) => {
9992
10456
  const binName = PACKAGE_NAME.exec(name)?.[1] ?? name;
9993
- if (skillHub !== void 0) ensureSkillName(binName);
10457
+ const mode = installModeOf(skillHub, dependency, binName);
9994
10458
  const path = resolve(ctx.cwd, binName);
9995
10459
  await ensureFree(path);
10460
+ const setup = await installSetupOf(mode, name, binName);
9996
10461
  const values = {
9997
10462
  packageName: name,
9998
10463
  binName,
9999
10464
  description,
10000
10465
  debugVariable: debugVariable(binName),
10001
10466
  runtimeVersion: version,
10002
- skillInstall: skillInstallLine(skillHub, binName)
10467
+ ...setup.values
10003
10468
  };
10004
10469
  return {
10005
10470
  path,
@@ -10011,14 +10476,16 @@ const create = defineCommand({
10011
10476
  }),
10012
10477
  install,
10013
10478
  repository,
10014
- skillHub
10479
+ edits: setup.edits,
10480
+ manifestFiles: setup.files
10015
10481
  };
10016
10482
  },
10017
10483
  apply: async (plan, ctx) => {
10018
10484
  await mkdir(plan.path, { recursive: true });
10019
10485
  await writeTemplate(plan.path, plan.files, plan.values);
10020
10486
  if (plan.repository !== void 0) await setRepository(plan.path, plan.repository);
10021
- if (plan.skillHub !== void 0) await setSkillHub(plan.path, plan.skillHub);
10487
+ for (const edit of plan.edits) await applyEdit(plan.path, edit);
10488
+ if (plan.manifestFiles.length > 0) await addFiles(plan.path, plan.manifestFiles);
10022
10489
  await runSteps(plan.steps, plan.path, ctx);
10023
10490
  return {
10024
10491
  path: plan.path,