@duyquangnvx/create-agent-cli 0.13.1 → 0.15.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
  }
@@ -5860,13 +5869,22 @@ function debugTrace(error, format) {
5860
5869
  function errorsOf(error) {
5861
5870
  return error instanceof AggregateError && error.errors.length > 0 ? error.errors.flatMap(errorsOf) : [error];
5862
5871
  }
5872
+ /** The `details` field that `error`'s code declares for text mode to print, when it holds a string. */
5873
+ function printedDetail(error, format) {
5874
+ if (!(error instanceof CliError)) return void 0;
5875
+ const key = format.errors[error.code]?.print;
5876
+ const { details } = error;
5877
+ if (key === void 0 || typeof details !== "object" || details === null) return void 0;
5878
+ const value = Reflect.get(details, key);
5879
+ return typeof value === "string" ? value : void 0;
5880
+ }
5863
5881
  function errorLines(error, format) {
5864
5882
  const fields = fieldsOf$1(error, format);
5865
- const stack = debugTrace(error, format);
5883
+ const blocks = [printedDetail(error, format), debugTrace(error, format)].filter((block) => block !== void 0);
5866
5884
  const { style } = format;
5867
5885
  const label = style.red(format.isBug(error) ? "internal error:" : "error:");
5868
- const trace = stack === void 0 ? "" : `${stack}\n`;
5869
- return `${label} ${fields.message}\n${trace}${style.cyan("hint:")} ${fields.hint}\n`;
5886
+ const between = blocks.map((block) => `${block}\n`).join("");
5887
+ return `${label} ${fields.message}\n${between}${style.cyan("hint:")} ${fields.hint}\n`;
5870
5888
  }
5871
5889
  function formatRuntimeError(error, format) {
5872
5890
  const errors = errorsOf(error);
@@ -6142,8 +6160,8 @@ const SHELL_SAFE = /^[\w@%+=:,./-]+$/;
6142
6160
  function quoted$1(token) {
6143
6161
  return SHELL_SAFE.test(token) ? token : `'${token.replaceAll("'", `'\\''`)}'`;
6144
6162
  }
6145
- function pasteable(cliName, tokens, stdinArg) {
6146
- const line = [cliName, ...tokens].map(quoted$1).join(" ");
6163
+ function pasteable(runAs, tokens, stdinArg) {
6164
+ const line = [runAs, ...tokens.map(quoted$1)].join(" ");
6147
6165
  return stdinArg === void 0 ? line : `${line} < <${stdinArg} file>`;
6148
6166
  }
6149
6167
  function split(argv) {
@@ -6173,10 +6191,10 @@ function without(flag, takes, tokens) {
6173
6191
  * The caller's command line as typed, ready to paste, with each flag in `remove` dropped along with
6174
6192
  * its value, the words in `add` placed before any `--`, and the stdin arg's file redirected in.
6175
6193
  */
6176
- function commandLineOf({ cliName, argv, flags, stdinArg }, { add = [], remove = [] } = {}) {
6194
+ function commandLineOf({ runAs, argv, flags, stdinArg }, { add = [], remove = [] } = {}) {
6177
6195
  const { options, operands } = split(argv);
6178
6196
  const takes = new Map(flags.map((known) => [known.flag, known.takes]));
6179
- return pasteable(cliName, [
6197
+ return pasteable(runAs, [
6180
6198
  ...remove.reduce((tokens, flag) => without(flag, takes.get(flag) ?? "nothing", tokens), options),
6181
6199
  ...add,
6182
6200
  ...operands
@@ -6189,9 +6207,9 @@ function renamed(token, renames) {
6189
6207
  return value.length === 0 ? name : `${name}=${value.join("=")}`;
6190
6208
  }
6191
6209
  /** 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) {
6210
+ function commandLineRenaming({ runAs, argv, stdinArg }, renames) {
6193
6211
  const { options, operands } = split(argv);
6194
- return pasteable(cliName, [...options.map((token) => renamed(token, renames)), ...operands], stdinArg);
6212
+ return pasteable(runAs, [...options.map((token) => renamed(token, renames)), ...operands], stdinArg);
6195
6213
  }
6196
6214
  //#endregion
6197
6215
  //#region ../agent-cli/dist/usage.js
@@ -6478,6 +6496,15 @@ function isCommand(node) {
6478
6496
  return typeof node.effect === "string";
6479
6497
  }
6480
6498
  //#endregion
6499
+ //#region ../agent-cli/dist/run-as.js
6500
+ /**
6501
+ * The Run-as: how every command line the Runtime gives its caller starts. A Dependency install runs
6502
+ * the copy the project pins, so it is `npx -q --no-install <bin>`; otherwise the bare name (ADR 0011).
6503
+ */
6504
+ function runAsOf(cli) {
6505
+ return cli.install?.mode === "dependency" ? `npx -q --no-install ${cli.name}` : cli.name;
6506
+ }
6507
+ //#endregion
6481
6508
  //#region ../agent-cli/dist/project.js
6482
6509
  const PROJECT_INVALID = "PROJECT_INVALID";
6483
6510
  function invalid(path, problem, callerLine, options) {
@@ -6717,11 +6744,7 @@ async function feed(stdin, input, signal) {
6717
6744
  */
6718
6745
  async function spawnChild(command, args, { cwd, env, signal, onLine, stdin, detached = false }) {
6719
6746
  signal.throwIfAborted();
6720
- const launch = process.platform === "win32" ? await windowsLaunch(command, args, env) : {
6721
- command,
6722
- args,
6723
- shell: false
6724
- };
6747
+ const launch = await launchOf(command, args, env);
6725
6748
  signal.throwIfAborted();
6726
6749
  return new Promise((resolve, reject) => {
6727
6750
  const child = spawn(launch.command, launch.args, {
@@ -6786,6 +6809,38 @@ async function spawnChild(command, args, { cwd, env, signal, onLine, stdin, deta
6786
6809
  });
6787
6810
  });
6788
6811
  }
6812
+ async function launchOf(command, args, env) {
6813
+ return process.platform === "win32" ? windowsLaunch(command, args, env) : {
6814
+ command,
6815
+ args,
6816
+ shell: false
6817
+ };
6818
+ }
6819
+ /**
6820
+ * Starts a child in its own process group (its own console on Windows) with no stdio, and settles
6821
+ * once it has started; the parent neither waits for it nor keeps it from exiting.
6822
+ */
6823
+ const realSpawnDetached = async (command, args, { cwd, env }) => {
6824
+ const launch = await launchOf(command, args, env);
6825
+ return new Promise((resolve, reject) => {
6826
+ const child = spawn(launch.command, launch.args, {
6827
+ cwd,
6828
+ env,
6829
+ shell: launch.shell,
6830
+ windowsHide: true,
6831
+ stdio: "ignore",
6832
+ detached: true
6833
+ });
6834
+ child.unref();
6835
+ child.once("error", reject);
6836
+ child.once("spawn", () => {
6837
+ child.off("error", reject);
6838
+ const { pid } = child;
6839
+ if (pid === void 0) reject(/* @__PURE__ */ new Error(`${command} started without a pid`));
6840
+ else resolve({ pid });
6841
+ });
6842
+ });
6843
+ };
6789
6844
  const realSpawn = (command, args, options) => spawnChild(command, args, options);
6790
6845
  function failedError(command, { exitCode, stderr }) {
6791
6846
  const tail = lastLines(stderr, STDERR_TAIL_LINES$1);
@@ -6855,6 +6910,20 @@ function contextSpawn(run, defaults, scope) {
6855
6910
  return result;
6856
6911
  };
6857
6912
  }
6913
+ /** `ctx.spawnDetached`: `run` bound to the run's cwd and env, its start failures classified by errno. */
6914
+ function contextSpawnDetached(run, defaults, scope) {
6915
+ return async (command, args = [], options = {}) => {
6916
+ defaults.signal.throwIfAborted();
6917
+ try {
6918
+ return await run(command, args, {
6919
+ cwd: options.cwd ?? defaults.cwd,
6920
+ env: options.env ?? defaults.env
6921
+ });
6922
+ } catch (error) {
6923
+ throw startError(command, error, scope);
6924
+ }
6925
+ };
6926
+ }
6858
6927
  //#endregion
6859
6928
  //#region ../agent-cli/dist/built-ins/semver.js
6860
6929
  const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
@@ -7177,17 +7246,19 @@ const LINES = {
7177
7246
  pnpm: {
7178
7247
  upgrade: (packages) => `pnpm up --latest ${packages.join(" ")}`,
7179
7248
  add: (packages) => `pnpm add -D ${packages.join(" ")}`,
7180
- reinstall: "pnpm install",
7181
- exec: (cli) => `pnpm exec ${cli}`
7249
+ install: "pnpm install"
7182
7250
  },
7183
7251
  npm: {
7184
7252
  upgrade: (packages) => `npm install ${packages.map((name) => `${name}@latest`).join(" ")}`,
7185
7253
  add: (packages) => `npm install -D ${packages.join(" ")}`,
7186
- reinstall: "npm install",
7187
- exec: (cli) => `npx ${cli}`
7254
+ install: "npm install"
7188
7255
  }
7189
7256
  };
7190
- const manifestSchema$1 = object({ packageManager: string().optional() });
7257
+ const manifestSchema$1 = object({
7258
+ packageManager: string().optional(),
7259
+ dependencies: record(string(), unknown()).optional(),
7260
+ devDependencies: record(string(), unknown()).optional()
7261
+ });
7191
7262
  const packageSchema = object({ version: string() });
7192
7263
  /** `dir` and every folder above it, nearest first. */
7193
7264
  function ancestorsOf(dir) {
@@ -7203,7 +7274,7 @@ async function exists(path) {
7203
7274
  throw error;
7204
7275
  }
7205
7276
  }
7206
- async function declaredManager(dir) {
7277
+ async function manifestIn(dir) {
7207
7278
  let text;
7208
7279
  try {
7209
7280
  text = await readFile(join(dir, "package.json"), "utf8");
@@ -7217,15 +7288,30 @@ async function declaredManager(dir) {
7217
7288
  } catch {
7218
7289
  return;
7219
7290
  }
7220
- const name = manifestSchema$1.safeParse(manifest).data?.packageManager?.split("@")[0];
7291
+ return manifestSchema$1.safeParse(manifest).data;
7292
+ }
7293
+ async function declaredManager(dir) {
7294
+ const name = (await manifestIn(dir))?.packageManager?.split("@")[0];
7221
7295
  return MANAGERS.find((manager) => manager === name);
7222
7296
  }
7297
+ async function manifestListing(name, cwd, root) {
7298
+ const dirs = ancestorsOf(cwd);
7299
+ const top = root === void 0 ? dirs.length : dirs.indexOf(root.dir) + 1;
7300
+ for (const dir of dirs.slice(0, top)) {
7301
+ const manifest = await manifestIn(dir);
7302
+ if (manifest?.dependencies?.[name] !== void 0 || manifest?.devDependencies?.[name] !== void 0) return join(dir, "package.json");
7303
+ }
7304
+ }
7223
7305
  async function lockfileRootOf(cwd) {
7224
7306
  for (const dir of ancestorsOf(cwd)) for (const [file, kind] of LOCKFILES) if (await exists(join(dir, file))) return {
7225
7307
  dir,
7226
7308
  manager: await declaredManager(dir) ?? kind
7227
7309
  };
7228
7310
  }
7311
+ /** The package manager's project root: the folder of the nearest lockfile, or `cwd` without one. */
7312
+ async function projectRootOf(cwd) {
7313
+ return (await lockfileRootOf(cwd))?.dir ?? cwd;
7314
+ }
7229
7315
  async function versionIn(manifest) {
7230
7316
  try {
7231
7317
  return packageSchema.parse(JSON.parse(await readFile(manifest, "utf8"))).version;
@@ -7234,7 +7320,7 @@ async function versionIn(manifest) {
7234
7320
  throw error;
7235
7321
  }
7236
7322
  }
7237
- async function standingOf(install, bin, cwd) {
7323
+ async function standingOf(install, bin, cwd, root) {
7238
7324
  if (bin === void 0) return { kind: "source" };
7239
7325
  for (const projectDir of ancestorsOf(cwd)) {
7240
7326
  const folder = join(projectDir, "node_modules", install.package);
@@ -7268,14 +7354,18 @@ async function standingOf(install, bin, cwd) {
7268
7354
  runningFrom
7269
7355
  };
7270
7356
  }
7271
- return { kind: "missing" };
7357
+ const manifest = await manifestListing(install.package, cwd, root);
7358
+ return manifest === void 0 ? { kind: "missing" } : {
7359
+ kind: "listed",
7360
+ manifest
7361
+ };
7272
7362
  }
7273
7363
  async function surveyDependency(install, bin, cwd) {
7274
- const [root, standing] = await Promise.all([lockfileRootOf(cwd), standingOf(install, bin, cwd)]);
7364
+ const root = await lockfileRootOf(cwd);
7275
7365
  return {
7276
7366
  cwd,
7277
7367
  root,
7278
- standing
7368
+ standing: await standingOf(install, bin, cwd, root)
7279
7369
  };
7280
7370
  }
7281
7371
  function packagesOf(install) {
@@ -7290,6 +7380,10 @@ function skippedFor(cli, install, standing) {
7290
7380
  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
7381
  };
7292
7382
  }
7383
+ function searchedFrom(cwd, root) {
7384
+ if (root === void 0) return `at or above ${cwd}`;
7385
+ return root.dir === cwd ? `at ${cwd}` : `from ${cwd} up to ${root.dir}`;
7386
+ }
7293
7387
  function problem(message, fix) {
7294
7388
  return fix === void 0 ? {
7295
7389
  state: "problem",
@@ -7306,11 +7400,12 @@ function dependencyInstallFinding(cli, install, { cwd, root, standing }) {
7306
7400
  switch (standing.kind) {
7307
7401
  case "source":
7308
7402
  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);
7403
+ case "missing": return problem(`no package ${searchedFrom(cwd, root)} depends on ${install.package}`, lines?.add(packagesOf(install)));
7404
+ case "listed": return problem(`${standing.manifest} lists ${install.package}, but no copy of it is installed at or above ${cwd}`, lines?.install);
7405
+ case "unreadable": return problem(`${standing.manifest} is not a package.json with a version: ${standing.reason}`, lines?.install);
7311
7406
  case "other": {
7312
7407
  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));
7408
+ return problem(`${cli.name} runs from ${runningFrom}, not from this project's ${install.package} ${copy.version} at ${copy.realDir}`, `${runAsOf(cli)} doctor`);
7314
7409
  }
7315
7410
  case "installed": {
7316
7411
  const { copy } = standing;
@@ -7349,6 +7444,87 @@ function dependencyRelease(cli, install, { cwd, root, standing }) {
7349
7444
  };
7350
7445
  }
7351
7446
  //#endregion
7447
+ //#region ../agent-cli/dist/built-ins/agents-md.js
7448
+ /** One paragraph telling every session that the CLI exists, how to run it and which skill to load. */
7449
+ function agentsBlock(cli) {
7450
+ const { install } = cli;
7451
+ 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` : "";
7452
+ const what = cli.description.replace(/\.$/, "");
7453
+ return `\`${cli.name}\`: ${what}. Run it as \`${runAsOf(cli)}\`${where}. Load the \`${cli.name}\` skill before using it.`;
7454
+ }
7455
+ function markersOf(name) {
7456
+ return {
7457
+ begin: `<!-- BEGIN:${name} -->`,
7458
+ end: `<!-- END:${name} -->`
7459
+ };
7460
+ }
7461
+ /** `text` with the CLI's block between its markers, appended when they are missing; nothing else changes. */
7462
+ function withBlock(cli, text) {
7463
+ const { begin, end } = markersOf(cli.name);
7464
+ const whole = `${begin}\n${agentsBlock(cli)}\n${end}`;
7465
+ const start = text.indexOf(begin);
7466
+ const stop = start === -1 ? -1 : text.indexOf(end, start);
7467
+ if (stop !== -1) return `${text.slice(0, start)}${whole}${text.slice(stop + end.length)}`;
7468
+ if (text === "") return `${whole}\n`;
7469
+ return `${text}${text.endsWith("\n") ? "" : "\n"}\n${whole}\n`;
7470
+ }
7471
+ async function agentsMdAt(cli, root) {
7472
+ const path = join(root, "AGENTS.md");
7473
+ const existing = await orMissing(readFile(path, "utf8"));
7474
+ const block = agentsBlock(cli);
7475
+ if (existing === void 0) return {
7476
+ path,
7477
+ block,
7478
+ change: "create"
7479
+ };
7480
+ if (withBlock(cli, existing) === existing) return {
7481
+ path,
7482
+ block,
7483
+ change: null
7484
+ };
7485
+ return {
7486
+ path,
7487
+ block,
7488
+ change: existing.includes(markersOf(cli.name).begin) ? "rewrite" : "add"
7489
+ };
7490
+ }
7491
+ async function handEditsAt(root) {
7492
+ const claudeMd = join(root, "CLAUDE.md");
7493
+ const text = await orMissing(readFile(claudeMd, "utf8"));
7494
+ if (text === void 0 || text.split(/\r?\n/).some((line) => line.trim() === "@AGENTS.md")) return [];
7495
+ return [`add a line \`@AGENTS.md\` to ${claudeMd}, so Claude Code reads AGENTS.md`];
7496
+ }
7497
+ function agentsRow(cli) {
7498
+ return {
7499
+ name: "agents",
7500
+ advisory: true,
7501
+ run: async (ctx) => {
7502
+ const root = await projectRootOf(ctx.cwd);
7503
+ const { path, change } = await agentsMdAt(cli, root);
7504
+ const fix = `${runAsOf(cli)} skill install`;
7505
+ if (change === "rewrite") return {
7506
+ state: "problem",
7507
+ message: `${path} holds a ${cli.name} block other than the one ${fix} writes`,
7508
+ fix
7509
+ };
7510
+ if (change !== null) return {
7511
+ state: "problem",
7512
+ message: `${path} holds no ${cli.name} block, so sessions in this repo do not know ${cli.name}`,
7513
+ fix
7514
+ };
7515
+ const holds = `${path} holds the ${cli.name} block`;
7516
+ const [edit] = await handEditsAt(root);
7517
+ return edit === void 0 ? {
7518
+ state: "ok",
7519
+ message: holds
7520
+ } : {
7521
+ state: "problem",
7522
+ message: `${holds}; ${edit}`
7523
+ };
7524
+ }
7525
+ };
7526
+ }
7527
+ //#endregion
7352
7528
  //#region ../agent-cli/dist/built-ins/skills.js
7353
7529
  const SKILL_DIRS = [[".claude", "skills"], [".agents", "skills"]];
7354
7530
  const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
@@ -7409,11 +7585,111 @@ function fixFor(cliName, copies, source) {
7409
7585
  }
7410
7586
  async function skillReport(cli, version, ctx) {
7411
7587
  const copies = await skillCopies(cli.name, version, ctx);
7588
+ const source = cli.skill !== void 0 && "source" in cli.skill ? cli.skill.source : void 0;
7412
7589
  return {
7413
7590
  copies,
7414
- fix: fixFor(cli.name, copies, cli.skill?.source)
7591
+ fix: fixFor(cli.name, copies, source)
7592
+ };
7593
+ }
7594
+ //#endregion
7595
+ //#region ../agent-cli/dist/built-ins/packaged-skill.js
7596
+ /** The folder, relative to the package root, of a CLI's Packaged skill, if it declares one. */
7597
+ function packagedFolderOf(cli) {
7598
+ return cli.skill !== void 0 && "packaged" in cli.skill ? cli.skill.packaged : void 0;
7599
+ }
7600
+ /** The project root and the Skill copy under it, as `skill install` writes it. */
7601
+ async function skillCopyOf(cli, cwd) {
7602
+ const root = await projectRootOf(cwd);
7603
+ return {
7604
+ root,
7605
+ copy: join(root, ".claude", "skills", cli.name)
7606
+ };
7607
+ }
7608
+ /** Every file under `dir`, relative to it and sorted; none when `dir` is missing. */
7609
+ async function filesIn(dir) {
7610
+ return (await orMissing(readdir(dir, {
7611
+ recursive: true,
7612
+ withFileTypes: true
7613
+ })) ?? []).filter((entry) => entry.isFile()).map((entry) => relative(dir, join(entry.parentPath, entry.name))).sort();
7614
+ }
7615
+ /** The versions of the package's skill in `source` and of its copy in `copy`. */
7616
+ async function skillVersionsOf(source, copy) {
7617
+ const [shipped, installed] = await Promise.all([readFile(join(source, "SKILL.md"), "utf8"), orMissing(readFile(join(copy, "SKILL.md"), "utf8"))]);
7618
+ const packageVersion = skillVersionIn(shipped) ?? null;
7619
+ const copyVersion = installed === void 0 ? void 0 : skillVersionIn(installed) ?? null;
7620
+ return {
7621
+ packageVersion,
7622
+ copyVersion,
7623
+ same: copyVersion === packageVersion
7624
+ };
7625
+ }
7626
+ async function personalCopyOf(name, copy, ctx) {
7627
+ const home = join(homeOf(ctx.env), ".claude", "skills", name);
7628
+ const real = await orMissing(realpath(home));
7629
+ return real === void 0 || real === await orMissing(realpath(copy)) ? void 0 : home;
7630
+ }
7631
+ /** What the `skill` row of a Packaged skill carries under `--json` beside its message. */
7632
+ const packagedSkillDetailsSchema = object({
7633
+ copy: string(),
7634
+ copyVersion: string().nullable(),
7635
+ packageVersion: string().nullable(),
7636
+ /** A personal copy, which Claude Code prefers over the project's. */
7637
+ home: string().optional()
7638
+ });
7639
+ function versionLabel(version) {
7640
+ return version ?? "(no metadata.version)";
7641
+ }
7642
+ function compared(name, { copy, copyVersion, packageVersion }, same) {
7643
+ const stated = `${copy} ${versionLabel(copyVersion)}`;
7644
+ return same ? `${stated}, the version ${name} ships` : `${stated} differs from the ${versionLabel(packageVersion)} that ${name} ships`;
7645
+ }
7646
+ /** The `skill` row of a Packaged skill: the project root's copy against the package's, and any under home. */
7647
+ async function packagedSkillFinding(cli, folder, ctx) {
7648
+ const { copy } = await skillCopyOf(cli, ctx.cwd);
7649
+ const versions = await skillVersionsOf(join(ctx.packageRoot, folder), copy);
7650
+ const { packageVersion } = versions;
7651
+ const copyVersion = versions.copyVersion ?? null;
7652
+ const home = await personalCopyOf(cli.name, copy, ctx);
7653
+ const skill = {
7654
+ copy,
7655
+ copyVersion,
7656
+ packageVersion,
7657
+ ...home === void 0 ? {} : { home }
7658
+ };
7659
+ const head = versions.copyVersion === void 0 ? `no copy of the ${cli.name} skill at ${copy}` : compared(cli.name, skill, versions.same);
7660
+ const message = home === void 0 ? head : `${head}; remove ${home}: Claude Code prefers a personal skill over the project's`;
7661
+ if (!versions.same) return {
7662
+ state: "problem",
7663
+ message,
7664
+ fix: `${runAsOf(cli)} skill install`,
7665
+ skill
7666
+ };
7667
+ return home === void 0 ? {
7668
+ state: "ok",
7669
+ message,
7670
+ skill
7671
+ } : {
7672
+ state: "problem",
7673
+ message,
7674
+ skill
7415
7675
  };
7416
7676
  }
7677
+ /** Gives a Packaged skill's `skill` row problem as a Notice, since an agent rarely runs `doctor`. */
7678
+ async function noticeSkillCopy(cli, { builtIn, bin }, ctx) {
7679
+ if (builtIn || bin === void 0 || cli.commands === void 0) return;
7680
+ const folder = packagedFolderOf(cli);
7681
+ if (folder === void 0) return;
7682
+ let finding;
7683
+ try {
7684
+ finding = await packagedSkillFinding(cli, folder, ctx);
7685
+ } catch (error) {
7686
+ if (errnoOf(error) === void 0) throw error;
7687
+ ctx.notice(`could not check the ${cli.name} skill: ${messageOf(error)}`);
7688
+ return;
7689
+ }
7690
+ if (finding.state !== "problem") return;
7691
+ ctx.notice(finding.fix === void 0 ? finding.message : `${finding.message}; run \`${finding.fix}\``);
7692
+ }
7417
7693
  //#endregion
7418
7694
  //#region ../agent-cli/dist/built-ins/doctor-rows.js
7419
7695
  function ok(message) {
@@ -7581,9 +7857,12 @@ function describeCopy({ path, version, state }) {
7581
7857
  return `${path}${version === void 0 ? "" : ` ${version}`} (${SKILL_STATE_NOTES[state]})`;
7582
7858
  }
7583
7859
  function skillRow(cli) {
7860
+ const packaged = packagedFolderOf(cli);
7584
7861
  return {
7585
7862
  name: "skill",
7863
+ advisory: true,
7586
7864
  run: async (ctx) => {
7865
+ if (packaged !== void 0) return packagedSkillFinding(cli, packaged, ctx);
7587
7866
  const { copies, fix } = await skillReport(cli, cli.version, ctx);
7588
7867
  const message = copies.length === 0 ? `no copy of the ${cli.name} skill is installed` : copies.map(describeCopy).join("; ");
7589
7868
  return fix === void 0 ? ok(message) : {
@@ -7605,7 +7884,8 @@ function doctorChecks(cli, launch, read) {
7605
7884
  ...toolRows(cli.tools ?? [], cli.commands),
7606
7885
  installRow(cli, launch, survey),
7607
7886
  registryRow(cli, launch, survey),
7608
- skillRow(cli)
7887
+ skillRow(cli),
7888
+ ...packagedFolderOf(cli) === void 0 ? [] : [agentsRow(cli)]
7609
7889
  ];
7610
7890
  }
7611
7891
  //#endregion
@@ -7624,7 +7904,8 @@ const reportSchema = object({
7624
7904
  ]),
7625
7905
  message: string(),
7626
7906
  fix: string().optional(),
7627
- code: string().optional()
7907
+ code: string().optional(),
7908
+ skill: packagedSkillDetailsSchema.optional()
7628
7909
  })),
7629
7910
  next: array(string())
7630
7911
  });
@@ -7694,10 +7975,10 @@ function doctorCommand(cli, launch) {
7694
7975
  name: "doctor",
7695
7976
  description: "check the setup and say what fixes each problem",
7696
7977
  examples: [{
7697
- command: `${cli.name} doctor`,
7978
+ command: `${runAsOf(cli)} doctor`,
7698
7979
  note: "check the setup and list the commands that fix it"
7699
7980
  }, {
7700
- command: `${cli.name} doctor --json`,
7981
+ command: `${runAsOf(cli)} doctor --json`,
7701
7982
  note: "get the rows and fixes as data"
7702
7983
  }],
7703
7984
  effect: "read",
@@ -7725,687 +8006,802 @@ function doctorCommand(cli, launch) {
7725
8006
  });
7726
8007
  }
7727
8008
  //#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
- });
8009
+ //#region ../agent-cli/dist/agent.js
8010
+ const AGENT_NAME = /^[a-zA-Z0-9_-]+$/u;
8011
+ const isSet = (name) => (env) => (env[name] ?? "") !== "";
8012
+ const equals = (name, value) => (env) => env[name] === value;
8013
+ const MARKERS = [
8014
+ (env) => AGENT_NAME.test(env.AI_AGENT ?? ""),
8015
+ equals("AGENT", "amp"),
8016
+ isSet("CODEX_SANDBOX"),
8017
+ isSet("CODEX_CI"),
8018
+ isSet("CODEX_THREAD_ID"),
8019
+ isSet("GEMINI_CLI"),
8020
+ isSet("COPILOT_CLI"),
8021
+ isSet("OPENCODE"),
8022
+ isSet("ANTIGRAVITY_AGENT"),
8023
+ isSet("AUGMENT_AGENT"),
8024
+ isSet("REPL_ID"),
8025
+ isSet("CLAUDE_CODE_IS_COWORK"),
8026
+ isSet("CLAUDECODE"),
8027
+ isSet("CLAUDE_CODE"),
8028
+ isSet("CURSOR_TRACE_ID"),
8029
+ isSet("CURSOR_AGENT"),
8030
+ equals("CURSOR_EXTENSION_HOST_ROLE", "agent-exec"),
8031
+ equals("TERM_PROGRAM", "kiro"),
8032
+ (env) => ["/.pi/agent", "\\.pi\\agent"].some((dir) => (env.PATH ?? "").includes(dir)),
8033
+ isSet("GOOSE_PROVIDER")
8034
+ ];
8035
+ /**
8036
+ * Whether a Coding agent, not a person, is running the Agent CLI. Some Coding agents run commands
8037
+ * in a PTY, so a TTY alone doesn't mean a person is watching.
8038
+ */
8039
+ function isAgent(env) {
8040
+ return MARKERS.some((marker) => marker(env));
7740
8041
  }
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`;
8042
+ //#endregion
8043
+ //#region ../agent-cli/dist/errno.js
8044
+ const PERMISSION_DENIED = "PERMISSION_DENIED";
8045
+ const DISK_FULL = "DISK_FULL";
8046
+ const NOT_FOUND = "NOT_FOUND";
8047
+ const ALREADY_EXISTS = "ALREADY_EXISTS";
8048
+ const NOT_A_FILE = "NOT_A_FILE";
8049
+ function permissionHint({ path, access, callerLine, env, platform }) {
8050
+ if (platform === "win32") return `a program may hold ${path} open: close it, then run \`${callerLine}\` again`;
8051
+ if (!isAgent(env)) return `check the permissions of ${path} and its folder`;
8052
+ 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
8053
  }
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
8054
+ function fileErrorOf(scope) {
8055
+ return (error, at) => classifyErrno(error, {
8056
+ ...at,
8057
+ ...scope
7749
8058
  });
7750
8059
  }
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
- }
8060
+ function classifyErrno(error, context) {
8061
+ const { path } = context;
8062
+ switch (errnoOf(error)) {
8063
+ case "EACCES":
8064
+ case "EPERM":
8065
+ case "EROFS":
8066
+ case "EBUSY": return new CliError({
8067
+ code: PERMISSION_DENIED,
8068
+ message: `permission denied: ${path}`,
8069
+ hint: permissionHint(context)
8070
+ }, { cause: error });
8071
+ case "ENOSPC":
8072
+ case "EDQUOT": return new CliError({
8073
+ code: DISK_FULL,
8074
+ message: `no space left on the disk holding ${path}`,
8075
+ hint: `free space on that disk, then run \`${context.callerLine}\` again`
8076
+ }, { cause: error });
8077
+ case "ENOENT":
8078
+ case "ENOTDIR":
8079
+ case "ENAMETOOLONG": return new CliError({
8080
+ code: NOT_FOUND,
8081
+ message: `not found: ${path}`,
8082
+ hint: `check that ${path} and its folder exist`
8083
+ }, { cause: error });
8084
+ case "EEXIST": return new CliError({
8085
+ code: ALREADY_EXISTS,
8086
+ message: `already exists: ${path}`,
8087
+ hint: `pass another path, or remove ${path}`
8088
+ }, { cause: error });
8089
+ case "EISDIR": return new CliError({
8090
+ code: NOT_A_FILE,
8091
+ message: `not a file: ${path} is a folder`,
8092
+ hint: `pass the path of a file, not of the folder ${path}`
8093
+ }, { cause: error });
8094
+ default: return error;
7785
8095
  }
7786
- deadline(ms) {
7787
- return AbortSignal.any([this.signal, this.#abortsAfter(ms, "deadline")]);
8096
+ }
8097
+ //#endregion
8098
+ //#region ../agent-cli/dist/files.js
8099
+ const TEMP_FILE = /^\..+\.agent-cli-(\d+)\.tmp$/;
8100
+ /** Where the Runtime writes `path` before moving it into place; a dead writer's is swept. */
8101
+ function tempPathOf(path) {
8102
+ return join(dirname(path), `.${basename(path)}.agent-cli-${process.pid}.tmp`);
8103
+ }
8104
+ function isAlive(pid) {
8105
+ try {
8106
+ process.kill(pid, 0);
8107
+ return true;
8108
+ } catch (error) {
8109
+ return errnoOf(error) === "EPERM";
7788
8110
  }
7789
- timeout(ms) {
7790
- return this.#abortsAfter(ms, "timeout");
7791
- }
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;
7800
- }
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
8111
  }
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, ""));
7902
- }
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
- }];
8112
+ async function sweep(folder) {
8113
+ const dead = (await readdir(folder)).filter((name) => {
8114
+ const pid = TEMP_FILE.exec(name)?.[1];
8115
+ return pid !== void 0 && !isAlive(Number(pid));
8116
+ });
8117
+ await Promise.all(dead.map((name) => rm(join(folder, name), { force: true })));
7914
8118
  }
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;
8119
+ /** The file a symlink at `path` points to, so a replacing write keeps the link. */
8120
+ async function targetOf$1(path) {
7921
8121
  try {
7922
- changelog = await readFile(join(packageDir, "CHANGELOG.md"), "utf8");
8122
+ return await realpath(path);
7923
8123
  } catch (error) {
7924
- if (isMissingFile(error)) return [];
8124
+ if (isMissingFile(error)) return path;
7925
8125
  throw error;
7926
8126
  }
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`;
7943
8127
  }
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}`;
8128
+ async function writeWithMode(path, data, mode) {
8129
+ if (mode === void 0) {
8130
+ await writeFile(path, data);
8131
+ return;
8132
+ }
8133
+ await writeFile(path, data, { mode });
8134
+ await chmod(path, mode);
7946
8135
  }
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");
8136
+ /** What `link` answers on a file system without hard links, such as exFAT. */
8137
+ const NO_HARD_LINKS = /* @__PURE__ */ new Set([
8138
+ "EPERM",
8139
+ "ENOTSUP",
8140
+ "EXDEV"
8141
+ ]);
8142
+ /** A link fails atomically when `path` is taken, so no reader ever sees half a file there. */
8143
+ async function linkNew(temp, path, data, mode) {
8144
+ try {
8145
+ await link(temp, path);
8146
+ } catch (error) {
8147
+ if (!NO_HARD_LINKS.has(errnoOf(error))) throw error;
8148
+ await createNew(path, data, mode);
8149
+ }
7956
8150
  }
7957
- async function skillNext(cli, latest, ctx) {
7958
- const { fix } = await skillReport(cli, latest, ctx);
7959
- return fix === void 0 ? [] : [fix];
8151
+ /** A file this call created and could not finish is removed, so a rerun does not find half of it. */
8152
+ async function createNew(path, data, mode) {
8153
+ const handle = await open(path, "wx", mode);
8154
+ try {
8155
+ await handle.writeFile(data);
8156
+ if (mode !== void 0) await handle.chmod(mode);
8157
+ } catch (error) {
8158
+ await handle.close();
8159
+ await rm(path, { force: true });
8160
+ throw error;
8161
+ }
8162
+ await handle.close();
7960
8163
  }
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"
7966
- });
8164
+ const RENAME_ATTEMPTS = 10;
8165
+ const RENAME_BACKOFF_MS = 10;
8166
+ const HELD_BY_ANOTHER_PROGRAM = /* @__PURE__ */ new Set(["EPERM", "EBUSY"]);
8167
+ async function renameRetrying(from, to) {
8168
+ for (let attempt = 1;; attempt++) try {
8169
+ await rename(from, to);
8170
+ return;
8171
+ } catch (error) {
8172
+ if (!(process.platform === "win32" && HELD_BY_ANOTHER_PROGRAM.has(errnoOf(error))) || attempt === RENAME_ATTEMPTS) throw error;
8173
+ await setTimeout$1(attempt * RENAME_BACKOFF_MS);
8174
+ }
7967
8175
  }
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
- };
8176
+ /** Real files for one run: the first write to each folder sweeps the temp files of dead writers. */
8177
+ function realFiles() {
8178
+ const swept = /* @__PURE__ */ new Set();
8179
+ return {
8180
+ exists: async (path) => {
8181
+ try {
8182
+ await access(path);
8183
+ return true;
8184
+ } catch (error) {
8185
+ if (isNothingThere(error)) return false;
8186
+ throw error;
8187
+ }
7992
8188
  },
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);
8189
+ write: async (path, data, options = {}) => {
8190
+ await mkdir(dirname(path), { recursive: true });
8191
+ const target = options.exclusive === true ? path : await targetOf$1(path);
8192
+ const folder = dirname(target);
8193
+ if (!swept.has(folder)) {
8194
+ await sweep(folder);
8195
+ swept.add(folder);
8196
+ }
8197
+ const temp = tempPathOf(target);
8198
+ try {
8199
+ await writeWithMode(temp, data, options.mode);
8200
+ if (options.exclusive === true) {
8201
+ await linkNew(temp, target, data, options.mode);
8202
+ await rm(temp);
8203
+ } else await renameRetrying(temp, target);
8204
+ } catch (error) {
8205
+ await rm(temp, { force: true });
8206
+ throw error;
8018
8207
  }
8019
- return update;
8020
8208
  },
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)
8209
+ read: (path) => readFile(path),
8210
+ list: (folder) => readdir(folder),
8211
+ remove: (path) => rm(path, {
8212
+ recursive: true,
8213
+ force: true
8214
+ }),
8215
+ mkdir: async (path, options) => {
8216
+ const mode = options?.mode;
8217
+ if (options?.exclusive !== true) {
8218
+ await mkdir(path, {
8219
+ recursive: true,
8220
+ mode
8221
+ });
8222
+ return;
8223
+ }
8224
+ await mkdir(dirname(path), {
8225
+ recursive: true,
8226
+ mode
8227
+ });
8228
+ await mkdir(path, { mode });
8229
+ },
8230
+ stat: async (path) => {
8231
+ try {
8232
+ const { size, dev, ino } = await stat(path, { bigint: true });
8233
+ return {
8234
+ size: Number(size),
8235
+ id: `${dev}:${ino}`
8236
+ };
8237
+ } catch (error) {
8238
+ if (isNothingThere(error)) return void 0;
8239
+ throw error;
8240
+ }
8241
+ }
8242
+ };
8243
+ }
8244
+ /**
8245
+ * `files` resolving paths against `cwd`, its failures classified by errno; `list` drops the
8246
+ * Runtime's temp files and sorts.
8247
+ */
8248
+ function classifiedFiles(files, { cwd, fileError }) {
8249
+ async function at(access, path, work) {
8250
+ const target = resolve(cwd, path);
8251
+ try {
8252
+ return await work(target);
8253
+ } catch (error) {
8254
+ throw fileError(error, {
8255
+ path: target,
8256
+ access
8257
+ });
8258
+ }
8259
+ }
8260
+ return {
8261
+ exists: (path) => at("read", path, (target) => files.exists(target)),
8262
+ write: (path, data, options) => at("write", path, (target) => files.write(target, data, options)),
8263
+ read: (path) => at("read", path, (target) => files.read(target)),
8264
+ list: (folder) => at("read", folder, async (target) => (await files.list(target)).filter((name) => !TEMP_FILE.test(name)).sort()),
8265
+ remove: (path) => at("write", path, (target) => files.remove(target)),
8266
+ mkdir: (path, options) => at("write", path, (target) => files.mkdir(target, options)),
8267
+ stat: (path) => at("read", path, (target) => files.stat(target))
8268
+ };
8269
+ }
8270
+ //#endregion
8271
+ //#region ../agent-cli/dist/outputs.js
8272
+ const OUTPUT_EXISTS = "OUTPUT_EXISTS";
8273
+ /** Refuses a run that would overwrite existing outputs unless the caller passed `--force`. */
8274
+ async function checkOutputs({ invocation, commandLine, force, files }, outputs) {
8275
+ if (outputs === void 0 || force) return;
8276
+ const found = await Promise.all(outputs.map((path) => files.exists(path)));
8277
+ const existing = outputs.filter((_path, index) => found[index]);
8278
+ if (existing.length === 0) return;
8279
+ const them = existing.length === 1 ? "it" : "them";
8280
+ throw new CliError({
8281
+ code: OUTPUT_EXISTS,
8282
+ message: `\`${commandLine}\` would overwrite ${existing.join(", ")}`,
8283
+ hint: `to replace ${them}, run \`${commandLineOf(invocation, { add: [FORCE_FLAG.flag] })}\``
8023
8284
  });
8024
8285
  }
8286
+ /** Guards the writes to declared outputs of `files`, whose paths resolve against `cwd`. */
8287
+ function outputsGuard(files, cwd) {
8288
+ const unwritten = /* @__PURE__ */ new Set();
8289
+ let forceLine = "";
8290
+ const exclusiveWrite = async (path, data, options) => {
8291
+ const target = resolve(cwd, path);
8292
+ unwritten.delete(target);
8293
+ try {
8294
+ await files.write(path, data, {
8295
+ ...options,
8296
+ exclusive: true
8297
+ });
8298
+ } catch (error) {
8299
+ unwritten.add(target);
8300
+ if (!(error instanceof CliError) || error.code !== "ALREADY_EXISTS") throw error;
8301
+ throw new CliError({
8302
+ code: ALREADY_EXISTS,
8303
+ message: error.message,
8304
+ hint: `to replace it, run \`${forceLine}\``
8305
+ }, { cause: error });
8306
+ }
8307
+ };
8308
+ return {
8309
+ files: {
8310
+ ...files,
8311
+ write: (path, data, options) => unwritten.has(resolve(cwd, path)) ? exclusiveWrite(path, data, options) : files.write(path, data, options)
8312
+ },
8313
+ guard: (outputs, line) => {
8314
+ forceLine = line;
8315
+ for (const path of outputs) unwritten.add(resolve(cwd, path));
8316
+ }
8317
+ };
8318
+ }
8319
+ /**
8320
+ * Creates each output folder and a temp file in it, so a sandbox denial fails before `apply`
8321
+ * spends anything. The temp file's name is the Runtime's, so a crashed run's leftover is swept.
8322
+ */
8323
+ async function probeOutputs({ files, cwd, fileError }, outputs) {
8324
+ const folders = new Set(outputs.map((path) => dirname(resolve(cwd, path))));
8325
+ await Promise.all([...folders].map(async (folder) => {
8326
+ const probe = tempPathOf(join(folder, "outputs-probe"));
8327
+ try {
8328
+ await files.mkdir(folder);
8329
+ await files.write(probe, "");
8330
+ await files.remove(probe);
8331
+ } catch (error) {
8332
+ throw fileError(error, {
8333
+ path: folder,
8334
+ access: "write"
8335
+ });
8336
+ }
8337
+ }));
8338
+ }
8025
8339
  //#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) {
8340
+ //#region ../agent-cli/dist/built-ins/skill-install.js
8341
+ const installSchema = object({
8342
+ copy: string(),
8343
+ written: array(string()),
8344
+ removed: array(string()),
8345
+ agentsMd: object({
8346
+ path: string(),
8347
+ changed: boolean()
8348
+ }),
8349
+ handEdits: array(string())
8350
+ });
8351
+ async function sameContent(left, right) {
8352
+ const [a, b] = await Promise.all([readFile(left), readFile(right)]);
8353
+ return a.equals(b);
8354
+ }
8355
+ async function planOf(cli, folder, cwd, packageRoot) {
8356
+ const source = join(packageRoot, folder);
8357
+ const { root, copy } = await skillCopyOf(cli, cwd);
8358
+ const [shipped, present] = await Promise.all([filesIn(source), filesIn(copy)]);
8359
+ const differs = await Promise.all(shipped.map(async (file) => present.includes(file) && !await sameContent(join(source, file), join(copy, file))));
8029
8360
  return {
8030
- doctor: doctorCommand(cli, launch),
8031
- ...cli.install?.mode === "global" ? { update: updateCommand(cli, cli.install, launch) } : {}
8032
- };
8361
+ source,
8362
+ copy,
8363
+ write: shipped.filter((file) => !present.includes(file)),
8364
+ replace: shipped.filter((_file, index) => differs[index]),
8365
+ remove: present.filter((file) => !shipped.includes(file)),
8366
+ sameVersion: (await skillVersionsOf(source, copy)).same,
8367
+ agentsMd: await agentsMdAt(cli, root),
8368
+ handEdits: await handEditsAt(root)
8369
+ };
8370
+ }
8371
+ const BLOCK_VERBS = {
8372
+ create: "create",
8373
+ add: "add the block to",
8374
+ rewrite: "rewrite the block in"
8375
+ };
8376
+ function renderPlan(cli, plan) {
8377
+ const files = [
8378
+ ...plan.write.map((file) => ` write ${file}`),
8379
+ ...plan.replace.map((file) => ` replace ${file}`),
8380
+ ...plan.remove.map((file) => ` remove ${file}`)
8381
+ ];
8382
+ const { path, block, change } = plan.agentsMd;
8383
+ return [
8384
+ files.length === 0 ? `the ${cli.name} skill at ${plan.copy} already matches the package` : `would copy the ${cli.name} skill to ${plan.copy}:`,
8385
+ ...files,
8386
+ change === null ? `${path} already holds the ${cli.name} block` : `would ${BLOCK_VERBS[change]} ${path}:\n${block}`,
8387
+ ...plan.handEdits.map((edit) => `by hand: ${edit}`)
8388
+ ].join("\n");
8389
+ }
8390
+ function renderInstalled(cli, data) {
8391
+ const { written, removed, agentsMd } = data;
8392
+ return [
8393
+ 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}`,
8394
+ agentsMd.changed ? `wrote the ${cli.name} block in ${agentsMd.path}` : `${agentsMd.path} already holds the ${cli.name} block`,
8395
+ ...data.handEdits.map((edit) => `by hand: ${edit}`)
8396
+ ].join("\n");
8033
8397
  }
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
8398
+ function editedRemovals(plan, ctx) {
8399
+ const them = plan.remove.length === 1 ? "it" : "them";
8400
+ return new CliError({
8401
+ code: OUTPUT_EXISTS,
8402
+ message: `\`${ctx.commandLine()}\` would remove ${plan.remove.map((file) => join(plan.copy, file)).join(", ")}`,
8403
+ hint: `to remove ${them}, run \`${ctx.commandLine({ add: [FORCE_FLAG.flag] })}\``
8053
8404
  });
8054
8405
  }
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] })}\``
8406
+ /** The built-in `skill install`, for a CLI whose skill ships in its package. */
8407
+ function skillInstallCommand(cli, folder) {
8408
+ const runAs = runAsOf(cli);
8409
+ return defineCommand$1({
8410
+ name: "install",
8411
+ description: "copy this CLI's skill into the project and write its AGENTS.md block",
8412
+ examples: [{
8413
+ command: `${runAs} skill install`,
8414
+ note: "install the skill this version ships"
8415
+ }, {
8416
+ command: `${runAs} skill install --dry-run`,
8417
+ note: "show what it would change"
8418
+ }],
8419
+ effect: "write",
8420
+ output: installSchema,
8421
+ force: "replace or remove hand-edited files of a skill copy at the package's version",
8422
+ plan: (_input, ctx) => planOf(cli, folder, ctx.cwd, ctx.packageRoot),
8423
+ outputs: (plan) => plan.sameVersion ? [...plan.write, ...plan.replace].map((file) => join(plan.copy, file)) : [],
8424
+ apply: async (plan, ctx) => {
8425
+ if (plan.sameVersion && plan.remove.length > 0 && !ctx.force) throw editedRemovals(plan, ctx);
8426
+ for (const file of [...plan.write, ...plan.replace]) await ctx.files.write(join(plan.copy, file), await readFile(join(plan.source, file)));
8427
+ for (const file of plan.remove) await ctx.files.remove(join(plan.copy, file));
8428
+ const { path, change } = plan.agentsMd;
8429
+ if (change !== null) {
8430
+ const existing = await orMissing(readFile(path, "utf8")) ?? "";
8431
+ await ctx.files.write(path, withBlock(cli, existing));
8432
+ }
8433
+ return {
8434
+ copy: plan.copy,
8435
+ written: [...plan.write, ...plan.replace].sort(),
8436
+ removed: [...plan.remove],
8437
+ agentsMd: {
8438
+ path,
8439
+ changed: change !== null
8440
+ },
8441
+ handEdits: [...plan.handEdits]
8442
+ };
8443
+ },
8444
+ render: (data) => renderInstalled(cli, data),
8445
+ renderPlan: (plan) => renderPlan(cli, plan)
8065
8446
  });
8066
8447
  }
8067
8448
  //#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));
8449
+ //#region ../agent-cli/dist/interrupts.js
8450
+ const INTERRUPTED = "INTERRUPTED";
8451
+ const DEADLINE_EXCEEDED = "DEADLINE_EXCEEDED";
8452
+ /** How long an interrupted Command may take to settle before the run ends without it. */
8453
+ const GRACE_MS = 3e3;
8454
+ const GRACE_OVER = Symbol("grace over");
8455
+ function interruptedError(callerLine) {
8456
+ return new CliError({
8457
+ code: INTERRUPTED,
8458
+ message: "interrupted before it finished",
8459
+ hint: `to finish it, run \`${callerLine}\` again, unless it was stopped on purpose; if a tool timeout stopped it, give it longer`
8460
+ });
8100
8461
  }
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`}`;
8462
+ function waitHint(callerLine, bound) {
8463
+ 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
8464
  }
8113
- function fileErrorOf(scope) {
8114
- return (error, at) => classifyErrno(error, {
8115
- ...at,
8116
- ...scope
8465
+ function deadlineError(commandLine, ms, hint, bound = "deadline") {
8466
+ return new CliError({
8467
+ code: DEADLINE_EXCEEDED,
8468
+ message: `${commandLine} ran past its ${ms} ms ${bound}`,
8469
+ hint
8117
8470
  });
8118
8471
  }
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;
8472
+ /** One Command run's cancellation: its signal, its deadlines and the steps it must finish. */
8473
+ var RunControl = class {
8474
+ signal;
8475
+ #controller = new AbortController();
8476
+ #commandLine;
8477
+ #callerLine;
8478
+ #outer;
8479
+ #onOuterAbort = () => this.#controller.abort(interruptedError(this.#callerLine));
8480
+ /** Each running must-finish step, tracked until it settles, with its name. */
8481
+ #steps = /* @__PURE__ */ new Map();
8482
+ #progress;
8483
+ #clock;
8484
+ #disposed = new AbortController();
8485
+ /** The reasons this run's deadlines and timeouts aborted with, to tell them in a Command's error. */
8486
+ #abortReasons = /* @__PURE__ */ new WeakSet();
8487
+ #graceCancel = new AbortController();
8488
+ #endGrace = () => {};
8489
+ #graceOver = new Promise((resolve) => {
8490
+ this.#endGrace = resolve;
8491
+ });
8492
+ constructor({ commandLine, callerLine, outer, clock, progress, runDeadline }) {
8493
+ this.#commandLine = commandLine;
8494
+ this.#callerLine = callerLine;
8495
+ this.#outer = outer;
8496
+ this.#clock = clock;
8497
+ this.#progress = progress;
8498
+ this.signal = this.#controller.signal;
8499
+ this.signal.addEventListener("abort", () => this.#onStop(), { once: true });
8500
+ if (outer?.aborted === true) this.#onOuterAbort();
8501
+ else outer?.addEventListener("abort", this.#onOuterAbort, { once: true });
8502
+ if (runDeadline !== void 0) {
8503
+ const { ms, hint } = runDeadline;
8504
+ after(clock, ms, this.#disposed.signal, () => this.#controller.abort(deadlineError(commandLine, ms, hint())));
8505
+ }
8506
+ }
8507
+ deadline(ms) {
8508
+ return AbortSignal.any([this.signal, this.#abortsAfter(ms, "deadline")]);
8509
+ }
8510
+ timeout(ms) {
8511
+ return this.#abortsAfter(ms, "timeout");
8512
+ }
8513
+ #abortsAfter(ms, bound) {
8514
+ const controller = new AbortController();
8515
+ after(this.#clock, ms, this.#disposed.signal, () => {
8516
+ const reason = deadlineError(this.#commandLine, ms, waitHint(this.#callerLine, bound), bound);
8517
+ this.#abortReasons.add(reason);
8518
+ controller.abort(reason);
8519
+ });
8520
+ return controller.signal;
8521
+ }
8522
+ async runToEnd(name, work) {
8523
+ this.#graceCancel.abort();
8524
+ if (this.signal.aborted) this.#finishing(name);
8525
+ const step = work();
8526
+ const settled = step.then(() => {}, () => {});
8527
+ this.#steps.set(settled, name);
8528
+ try {
8529
+ return await step;
8530
+ } finally {
8531
+ this.#steps.delete(settled);
8532
+ this.#startGrace();
8533
+ }
8534
+ }
8535
+ /** Waits for the Command, its must-finish steps, and no longer than the grace once interrupted. */
8536
+ async settle(work) {
8537
+ try {
8538
+ const value = await Promise.race([work, this.#graceOver]);
8539
+ if (this.signal.aborted || value === GRACE_OVER) throw this.signal.reason;
8540
+ return value;
8541
+ } finally {
8542
+ await this.#stepsDone();
8543
+ this.close();
8544
+ }
8545
+ }
8546
+ /**
8547
+ * The error a run ends with: an interrupt or the run deadline outranks what the Command threw,
8548
+ * unless the Command threw its own `INTERRUPTED`, saying what it finished before it stopped.
8549
+ * Otherwise each leaf that is not a `CliError` and whose cause chain holds a deadline's or timeout's
8550
+ * abort is reported as that abort, so a declared error keeps its own code.
8551
+ */
8552
+ errorAfter(error) {
8553
+ if (this.signal.aborted) return error instanceof CliError && error.code === "INTERRUPTED" ? error : this.signal.reason;
8554
+ const leaves = errorsOf(error).map((leaf) => this.#abortIn(leaf) ?? leaf);
8555
+ return leaves.length === 1 ? leaves[0] : new AggregateError(leaves);
8556
+ }
8557
+ #abortIn(error) {
8558
+ if (error instanceof CliError) return void 0;
8559
+ return causesOf(error).find((each) => each instanceof CliError && this.#abortReasons.has(each));
8154
8560
  }
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
- }
8561
+ #onStop() {
8562
+ for (const name of this.#steps.values()) this.#finishing(name);
8563
+ this.#startGrace();
8564
+ }
8565
+ #finishing(name) {
8566
+ this.#progress(`finishing ${name}`);
8567
+ }
8568
+ #startGrace() {
8569
+ if (this.signal.aborted && this.#steps.size === 0) {
8570
+ this.#graceCancel.abort();
8571
+ this.#graceCancel = new AbortController();
8572
+ after(this.#clock, GRACE_MS, this.#graceCancel.signal, () => this.#endGrace(GRACE_OVER));
8573
+ }
8574
+ }
8575
+ async #stepsDone() {
8576
+ while (this.#steps.size > 0) await Promise.all(this.#steps.keys());
8577
+ }
8578
+ /** Stops the run's timers and its hold on the caller's signal, for a run that never settles. */
8579
+ close() {
8580
+ this.#graceCancel.abort();
8581
+ this.#disposed.abort();
8582
+ this.#outer?.removeEventListener("abort", this.#onOuterAbort);
8583
+ }
8584
+ };
8168
8585
  //#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";
8586
+ //#region ../agent-cli/dist/built-ins/upgrades.js
8587
+ /** A skipped release's Upgrade steps, or its whole section when it lists none. */
8588
+ const upgradeSchema = union([object({
8589
+ version: string(),
8590
+ steps: array(string())
8591
+ }), object({
8592
+ version: string(),
8593
+ notes: string()
8594
+ })]);
8595
+ const RELEASE_HEADING = /^## v?(\S+)/;
8596
+ const UPGRADE_HEADING = /^### Upgrade from v\S+$/;
8597
+ const HEADING = /^#{1,6} /;
8598
+ const ITEM = /^[-*] /;
8599
+ const NOTHING_TO_DO = /^nothing to do\.?$/i;
8600
+ function releasesIn(changelog) {
8601
+ const releases = [];
8602
+ let current;
8603
+ for (const line of changelog.split(/\r?\n/)) {
8604
+ const heading = RELEASE_HEADING.exec(line);
8605
+ if (heading !== null) {
8606
+ const version = heading[1] ?? "";
8607
+ current = isVersion(version) ? {
8608
+ version,
8609
+ lines: []
8610
+ } : void 0;
8611
+ if (current !== void 0) releases.push(current);
8612
+ } else current?.lines.push(line);
8181
8613
  }
8614
+ return releases;
8182
8615
  }
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 })));
8616
+ function listedSteps(lines) {
8617
+ const start = lines.findIndex((line) => UPGRADE_HEADING.test(line.trimEnd()));
8618
+ if (start === -1) return [];
8619
+ const end = lines.findIndex((line, i) => i > start && HEADING.test(line));
8620
+ const body = lines.slice(start + 1, end === -1 ? void 0 : end).filter((line) => line.trim() !== "");
8621
+ const starts = body.flatMap((line, i) => ITEM.test(line) ? [i] : []);
8622
+ return starts.map((first, i) => body.slice(first, starts[i + 1]).map((line) => line.trim()).join(" ").replace(ITEM, ""));
8189
8623
  }
8190
- /** The file a symlink at `path` points to, so a replacing write keeps the link. */
8191
- async function targetOf$1(path) {
8624
+ function upgradeOf({ version, lines }) {
8625
+ const listed = listedSteps(lines);
8626
+ if (listed.length === 0) return [{
8627
+ version,
8628
+ notes: lines.join("\n").trim()
8629
+ }];
8630
+ const todo = listed.filter((step) => !NOTHING_TO_DO.test(step));
8631
+ return todo.length === 0 ? [] : [{
8632
+ version,
8633
+ steps: todo
8634
+ }];
8635
+ }
8636
+ /**
8637
+ * What each release after `from` up to `to` asks of the operator, oldest first, from the
8638
+ * CHANGELOG.md in `packageDir`; none when the package ships no CHANGELOG.
8639
+ */
8640
+ async function upgradesOf(packageDir, from, to) {
8641
+ let changelog;
8192
8642
  try {
8193
- return await realpath(path);
8643
+ changelog = await readFile(join(packageDir, "CHANGELOG.md"), "utf8");
8194
8644
  } catch (error) {
8195
- if (isMissingFile(error)) return path;
8645
+ if (isMissingFile(error)) return [];
8196
8646
  throw error;
8197
8647
  }
8648
+ return releasesIn(changelog).filter(({ version }) => compareVersions(version, from) > 0 && compareVersions(version, to) <= 0).sort((a, b) => compareVersions(a.version, b.version)).flatMap(upgradeOf);
8198
8649
  }
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);
8650
+ //#endregion
8651
+ //#region ../agent-cli/dist/built-ins/update.js
8652
+ const updateSchema = object({
8653
+ from: string(),
8654
+ latest: string(),
8655
+ installed: object({
8656
+ version: string(),
8657
+ prefix: string()
8658
+ }).nullable(),
8659
+ upgrades: array(upgradeSchema),
8660
+ next: array(string())
8661
+ });
8662
+ function nothingToInstall({ from, latest }) {
8663
+ 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
8664
  }
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
- }
8665
+ function renderUpgrade(upgrade) {
8666
+ return "steps" in upgrade ? [`\nupgrade steps for ${upgrade.version}:`, ...upgrade.steps.map((step) => `- ${step}`)].join("\n") : `\nchanges in ${upgrade.version}:\n${upgrade.notes}`;
8221
8667
  }
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();
8668
+ function renderUpdate(cli, install, update) {
8669
+ const { installed } = update;
8670
+ const head = installed === null ? [nothingToInstall(update)] : [`installed ${install.package}@${installed.version} with the npm under ${installed.prefix}, replacing ${update.from}`, ...update.upgrades.map(renderUpgrade)];
8671
+ const next = update.next.map((command) => `next: ${command}, so the ${cli.name} skill describes ${update.latest}`);
8672
+ return (next.length === 0 ? head : [
8673
+ ...head,
8674
+ "",
8675
+ ...next
8676
+ ]).join("\n");
8234
8677
  }
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
- }
8678
+ async function skillNext(cli, latest, ctx) {
8679
+ const packaged = packagedFolderOf(cli);
8680
+ const finding = packaged === void 0 ? await skillReport(cli, latest, ctx) : await packagedSkillFinding(cli, packaged, ctx);
8681
+ return "fix" in finding && finding.fix !== void 0 ? [finding.fix] : [];
8246
8682
  }
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 });
8683
+ function interruptedAfterInstall(plan) {
8684
+ return new CliError({
8685
+ code: INTERRUPTED,
8686
+ message: `interrupted, but ${plan.package}@${plan.latest} had already been installed with the npm under ${plan.prefix}, replacing ${plan.from}`,
8687
+ hint: "nothing to run again: run the commands under next"
8688
+ });
8689
+ }
8690
+ /** The built-in `update`, for a CLI installed globally with npm. */
8691
+ function updateCommand(cli, install, { bin, node }) {
8692
+ return defineCommand$1({
8693
+ name: "update",
8694
+ description: "install the latest version of this CLI from its registry",
8695
+ examples: [{
8696
+ command: `${cli.name} update`,
8697
+ note: "install the latest version and show what changed"
8698
+ }, {
8699
+ command: `${cli.name} update --dry-run`,
8700
+ note: "show the version it would install"
8701
+ }],
8702
+ effect: "write",
8703
+ output: updateSchema,
8704
+ plan: async (_input, ctx) => {
8705
+ const npm = globalNpm(cli, install, ctx, node);
8706
+ const copy = await globalInstallOf(bin, npm);
8707
+ return {
8708
+ package: install.package,
8709
+ from: cli.version,
8710
+ latest: await latestVersion(npm),
8711
+ prefix: copy.prefix,
8712
+ packageDir: copy.packageDir
8713
+ };
8300
8714
  },
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;
8715
+ apply: async (plan, ctx) => {
8716
+ const { from, latest, prefix } = plan;
8717
+ if (standingOf$1(from, latest) !== "behind") return {
8718
+ from,
8719
+ latest,
8720
+ installed: null,
8721
+ upgrades: [],
8722
+ next: await skillNext(cli, latest, ctx)
8723
+ };
8724
+ ctx.signal.throwIfAborted();
8725
+ ctx.progress(`installing ${plan.package}@${latest} with the npm under ${prefix}…`);
8726
+ await ctx.runToEnd(`installing ${plan.package}@${latest}`, () => installGlobally(latest, globalNpm(cli, install, ctx, node)));
8727
+ const update = {
8728
+ from,
8729
+ latest,
8730
+ installed: {
8731
+ version: latest,
8732
+ prefix
8733
+ },
8734
+ upgrades: await upgradesOf(plan.packageDir, from, latest),
8735
+ next: await skillNext(cli, latest, ctx)
8736
+ };
8737
+ if (ctx.signal.aborted) {
8738
+ ctx.deliver(update);
8739
+ throw interruptedAfterInstall(plan);
8311
8740
  }
8312
- }
8313
- };
8741
+ return update;
8742
+ },
8743
+ render: (update) => renderUpdate(cli, install, update),
8744
+ 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)
8745
+ });
8314
8746
  }
8747
+ //#endregion
8748
+ //#region ../agent-cli/dist/built-ins/commands.js
8315
8749
  /**
8316
- * `files` resolving paths against `cwd`, its failures classified by errno; `list` drops the
8317
- * Runtime's temp files and sorts.
8750
+ * The Commands the Runtime adds to a multi-Command CLI: `update` only for a Global install, and
8751
+ * `skill install` only for a Packaged skill.
8318
8752
  */
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
- }
8753
+ function builtInCommands(cli, launch) {
8754
+ const packaged = packagedFolderOf(cli);
8331
8755
  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))
8756
+ doctor: doctorCommand(cli, launch),
8757
+ ...cli.install?.mode === "global" ? { update: updateCommand(cli, cli.install, launch) } : {},
8758
+ ...packaged === void 0 ? {} : { skill: { install: skillInstallCommand(cli, packaged) } }
8339
8759
  };
8340
8760
  }
8341
8761
  //#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] })}\``
8762
+ //#region ../agent-cli/dist/confirm.js
8763
+ const CONFIRMATION_REQUIRED = "CONFIRMATION_REQUIRED";
8764
+ const CONFIRMATION_MISMATCH = "CONFIRMATION_MISMATCH";
8765
+ function checkConfirmation({ invocation, commandLine, given }, expected) {
8766
+ if (given === expected) return;
8767
+ const hint = `to confirm, run \`${commandLineOf(invocation, {
8768
+ remove: [CONFIRM_FLAG.flag],
8769
+ add: [CONFIRM_FLAG.flag, expected]
8770
+ })}\``;
8771
+ if (given === void 0) throw new UsageError({
8772
+ code: CONFIRMATION_REQUIRED,
8773
+ message: `\`${commandLine}\` is destructive and needs ${CONFIRM_FLAG.flag}`,
8774
+ hint
8775
+ });
8776
+ throw new UsageError({
8777
+ code: CONFIRMATION_MISMATCH,
8778
+ message: `${CONFIRM_FLAG.flag} ${JSON.stringify(given)} does not match the target of \`${commandLine}\``,
8779
+ hint
8355
8780
  });
8356
8781
  }
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
- };
8782
+ //#endregion
8783
+ //#region ../agent-cli/dist/cost.js
8784
+ const NOT_CONFIRMED = "NOT_CONFIRMED";
8785
+ /** Refuses a run whose Plan states a cost the caller has not approved with `--yes`. */
8786
+ function checkCost({ invocation, commandLine, yes }, cost) {
8787
+ if (cost === void 0 || yes) return;
8788
+ throw new CliError({
8789
+ code: NOT_CONFIRMED,
8790
+ message: `\`${commandLine}\` needs ${YES_FLAG.flag}: ${cost}`,
8791
+ hint: `to approve the cost, run \`${commandLineOf(invocation, { add: [YES_FLAG.flag] })}\``
8792
+ });
8389
8793
  }
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
- }));
8794
+ //#endregion
8795
+ //#region ../agent-cli/dist/operator.js
8796
+ const NEEDS_OPERATOR = "NEEDS_OPERATOR";
8797
+ /** Fails with `NEEDS_OPERATOR` when a Coding agent runs an Operator Command. */
8798
+ function checkOperator({ operator, commandLine }, env, callerLine) {
8799
+ if (!operator || !isAgent(env)) return;
8800
+ throw new CliError({
8801
+ code: NEEDS_OPERATOR,
8802
+ message: `\`${commandLine}\` must be run by a person at a terminal, not a Coding agent`,
8803
+ hint: `ask the operator to type: ! ${callerLine}`
8804
+ });
8409
8805
  }
8410
8806
  //#endregion
8411
8807
  //#region ../agent-cli/dist/exit-classes.js
@@ -8508,7 +8904,10 @@ function checkOwnCodes(cli) {
8508
8904
  }
8509
8905
  function defineCli$1(cli) {
8510
8906
  checkOwnCodes(cli);
8511
- if (cli.commands === void 0) return cli;
8907
+ if (cli.commands === void 0) {
8908
+ if (packagedFolderOf(cli) !== void 0) throw new Error(`${cli.name} declares a Packaged skill, which only a CLI with several Commands can install`);
8909
+ return cli;
8910
+ }
8512
8911
  const { commands } = cli;
8513
8912
  const taken = Object.keys(builtInCommands(cli, {
8514
8913
  bin: void 0,
@@ -8529,7 +8928,10 @@ function agentCli() {
8529
8928
  }
8530
8929
  //#endregion
8531
8930
  //#region ../agent-cli/dist/error-table.js
8532
- /** Keeps each code's literal class and `details` schema, so `cliError` can type its throw sites. */
8931
+ /**
8932
+ * Keeps each code's literal class and `details` schema, so `cliError` can type its throw sites;
8933
+ * `print` must name a string key of the code's `details`.
8934
+ */
8533
8935
  function defineErrors(table) {
8534
8936
  return table;
8535
8937
  }
@@ -8675,7 +9077,7 @@ function examplesSection(examples, style) {
8675
9077
  }
8676
9078
  /** The CLI's own help: a listing or its single Command's help, ending with exit codes and examples. */
8677
9079
  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));
9080
+ 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
9081
  }
8680
9082
  function groupHelp(group, style) {
8681
9083
  return lines(...listingLines(group.commandLine, group, style));
@@ -8773,12 +9175,13 @@ function errorsData(errors) {
8773
9175
  details: declaration.details === void 0 ? null : toJSONSchema(declaration.details, { unrepresentable: "any" })
8774
9176
  }]));
8775
9177
  }
8776
- function cliHelpJson({ version, description, errors }, root) {
9178
+ function cliHelpJson({ name, version, description, errors }, root) {
8777
9179
  return printed$1({
8778
- name: root.commandLine,
9180
+ name,
8779
9181
  version,
8780
9182
  description,
8781
9183
  ...nodeData(root),
9184
+ name,
8782
9185
  errors: errorsData(errors ?? {})
8783
9186
  });
8784
9187
  }
@@ -8806,7 +9209,14 @@ function printed(value, render, { json, invocation, style, fields }) {
8806
9209
  }
8807
9210
  /** A progress message as stderr carries it: one JSON object per line under `--json` (ADR 0006). */
8808
9211
  function progressLine(message, json) {
8809
- return json ? `${JSON.stringify({ progress: message })}\n` : `${message}\n`;
9212
+ return stderrLine("progress", message, json);
9213
+ }
9214
+ /** A Notice as stderr carries it, a JSON line like progress under `--json`. */
9215
+ function noticeLine(text, json) {
9216
+ return stderrLine("notice", text, json);
9217
+ }
9218
+ function stderrLine(key, text, json) {
9219
+ return json ? `${JSON.stringify({ [key]: text })}\n` : `${text}\n`;
8810
9220
  }
8811
9221
  function errorText(error, format) {
8812
9222
  const text = formatRuntimeError(error, {
@@ -8827,7 +9237,7 @@ function startedOutput(outcome, format, spill) {
8827
9237
  fields: void 0
8828
9238
  }, spill);
8829
9239
  }
8830
- /** A serve Command stopped after it started: stopping it is how it is meant to end. */
9240
+ /** A serve Command stopped, or ended itself, after it started: that is how it is meant to end. */
8831
9241
  const STOPPED_RESULT = {
8832
9242
  stdout: "",
8833
9243
  stderr: "",
@@ -9053,6 +9463,10 @@ function dryRunOutcome(command, plan) {
9053
9463
  ].join("\n")
9054
9464
  };
9055
9465
  }
9466
+ function serveContextOf(ctx, end) {
9467
+ const serveCtx = Object.create(ctx);
9468
+ return Object.assign(serveCtx, { end });
9469
+ }
9056
9470
  async function execute(command, input, ctx, { dryRun, confirm, yes, page, invocation, commandLine, started, files, guardOutputs }) {
9057
9471
  await checkTools(command.tools ?? [], ctx.env, ctx.commandLine());
9058
9472
  if (command.rows !== void 0) return outcomeOf(await command.run({
@@ -9061,8 +9475,14 @@ async function execute(command, input, ctx, { dryRun, confirm, yes, page, invoca
9061
9475
  }, ctx), (data, style) => command.render(data, style));
9062
9476
  if (command.effect === "read") return outcomeOf(await command.run(input, ctx), (data, style) => command.render(data, style));
9063
9477
  if (command.effect === "serve") {
9064
- await started(outcomeOf(await command.start(input, ctx), (value, style) => command.render(value, style)));
9065
- return untilAborted(ctx.signal);
9478
+ let end = () => {};
9479
+ const ended = new Promise((resolve) => {
9480
+ end = resolve;
9481
+ });
9482
+ const outcome = outcomeOf(await command.start(input, serveContextOf(ctx, end)), (value, style) => command.render(value, style));
9483
+ await started(outcome);
9484
+ await Promise.race([ended, untilAborted(ctx.signal)]);
9485
+ return outcome;
9066
9486
  }
9067
9487
  const plan = await command.plan(input, ctx);
9068
9488
  if (dryRun) return dryRunOutcome(command, plan);
@@ -9404,6 +9824,13 @@ function selfOf(name, bin) {
9404
9824
  bin
9405
9825
  ];
9406
9826
  }
9827
+ const unfakedSpawnDetached = async () => {
9828
+ throw new Error("runCli was given a fake spawn but no spawnDetached: pass spawnDetached to runCli, so the test starts no real child");
9829
+ };
9830
+ function spawnDetachedOf({ spawn, spawnDetached }) {
9831
+ if (spawnDetached !== void 0) return spawnDetached;
9832
+ return spawn === void 0 ? realSpawnDetached : unfakedSpawnDetached;
9833
+ }
9407
9834
  function portsOf(cli, replacement) {
9408
9835
  if (replacement !== void 0) return () => replacement;
9409
9836
  return cli.ports ?? (() => ({}));
@@ -9437,11 +9864,19 @@ function portsBaseOf(scope, signal) {
9437
9864
  }, {
9438
9865
  fileError,
9439
9866
  callerLine
9867
+ }),
9868
+ spawnDetached: contextSpawnDetached(spawnDetachedOf(options), {
9869
+ cwd,
9870
+ env,
9871
+ signal
9872
+ }, {
9873
+ fileError,
9874
+ callerLine
9440
9875
  })
9441
9876
  };
9442
9877
  }
9443
9878
  function contextOf(portsBase, control, parts) {
9444
- const { ports, project, force, commandLine, deliver } = parts;
9879
+ const { ports, project, force, commandLine, deliver, notice } = parts;
9445
9880
  return {
9446
9881
  ...portsBase,
9447
9882
  deadline: (ms) => control.deadline(ms),
@@ -9454,7 +9889,8 @@ function contextOf(portsBase, control, parts) {
9454
9889
  force,
9455
9890
  commandLine,
9456
9891
  poll: (check, { intervalMs, signal: stop }) => poll(portsBase.clock, check, intervalMs, stop === void 0 ? control.signal : AbortSignal.any([control.signal, stop])),
9457
- deliver
9892
+ deliver,
9893
+ notice
9458
9894
  };
9459
9895
  }
9460
9896
  /** Leaves the Side task running, so the caller writes the result before it waits for it. */
@@ -9571,6 +10007,7 @@ async function runCommand(cli, routed, options, write, baseFormat) {
9571
10007
  project,
9572
10008
  force: flags.force,
9573
10009
  commandLine: typedLine,
10010
+ notice: (text) => write.stderr(noticeLine(text, routed.json)),
9574
10011
  deliver: (data) => {
9575
10012
  delivered = deliveredOutcome(command, data);
9576
10013
  }
@@ -9586,22 +10023,30 @@ async function runCommand(cli, routed, options, write, baseFormat) {
9586
10023
  let served = false;
9587
10024
  let settled;
9588
10025
  try {
10026
+ const outcome = await control.settle(noticeSkillCopy(cli, {
10027
+ builtIn,
10028
+ bin: options.bin
10029
+ }, ctx).then(() => execute(command, input, ctx, {
10030
+ dryRun: flags.dryRun,
10031
+ confirm: flags.confirm,
10032
+ yes: flags.yes,
10033
+ page: flags.page,
10034
+ invocation,
10035
+ commandLine,
10036
+ started: async (outcome) => {
10037
+ write.stdout(await startedOutput(outcome, format, spill));
10038
+ served = true;
10039
+ },
10040
+ files,
10041
+ guardOutputs: outputs.guard
10042
+ })));
10043
+ if (served) return {
10044
+ result: STOPPED_RESULT,
10045
+ finishSideTask
10046
+ };
9589
10047
  settled = {
9590
10048
  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
- }))
10049
+ outcome
9605
10050
  };
9606
10051
  } catch (error) {
9607
10052
  if (served && control.signal.aborted) return {
@@ -9693,12 +10138,13 @@ async function runWithProgress(cli, argv, options, write, stopNotice) {
9693
10138
  return result;
9694
10139
  }
9695
10140
  async function routeAndRun(cli, argv, options, write, stopNotice) {
10141
+ const runAs = runAsOf(cli);
9696
10142
  const format = {
9697
10143
  cliName: cli.name,
9698
10144
  env: options.env,
9699
10145
  style: styleFor(options.env, options.stdoutIsTTY === true),
9700
10146
  invocation: {
9701
- cliName: cli.name,
10147
+ runAs,
9702
10148
  argv,
9703
10149
  flags: []
9704
10150
  },
@@ -9707,9 +10153,9 @@ async function routeAndRun(cli, argv, options, write, stopNotice) {
9707
10153
  stop: options.signal
9708
10154
  };
9709
10155
  const root = cli.commands === void 0 ? {
9710
- ...commandSpec(cli.name, cli.command),
10156
+ ...commandSpec(runAs, cli.command),
9711
10157
  description: cli.description
9712
- } : groupSpec(cli.name, cli.commands, builtInCommands(cli, {
10158
+ } : groupSpec(runAs, cli.commands, builtInCommands(cli, {
9713
10159
  bin: options.bin,
9714
10160
  node: options.node
9715
10161
  }));
@@ -9776,16 +10222,131 @@ async function runProcess(cli) {
9776
10222
  }
9777
10223
  //#endregion
9778
10224
  //#region package.json
9779
- var version$1 = "0.13.1";
10225
+ var version$1 = "0.15.0";
9780
10226
  var description = "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.";
9781
10227
  //#endregion
9782
10228
  //#region src/build.ts
9783
10229
  const build = {
9784
- "sha": "7b1baa9",
10230
+ "sha": "cd3df50",
9785
10231
  "dirty": false,
9786
- "builtAt": "2026-10-23T13:03:05.000Z"
10232
+ "builtAt": "2026-10-27T04:23:15.000Z"
9787
10233
  };
9788
10234
  //#endregion
10235
+ //#region src/fill.ts
10236
+ /** `text` with each `{{key}}` replaced by its value; `quoted` escapes values that land inside a double-quoted string. */
10237
+ function fill(text, values, quoted) {
10238
+ return text.replaceAll(/\{\{(\w+)\}\}/g, (placeholder, key) => {
10239
+ const value = Object.hasOwn(values, key) ? values[key] : void 0;
10240
+ if (value === void 0) throw new Error(`template placeholder ${placeholder} has no value`);
10241
+ return quoted ? JSON.stringify(value).slice(1, -1) : value;
10242
+ });
10243
+ }
10244
+ //#endregion
10245
+ //#region src/install-mode.ts
10246
+ const PARTIALS_DIR = fileURLToPath(new URL("../partials/", import.meta.url));
10247
+ const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
10248
+ const COMMANDS_ANCHOR = " commands: {";
10249
+ const GLOBAL_INSTALL = ` install: {
10250
+ mode: "global",
10251
+ package: manifest.name,
10252
+ registry: manifest.publishConfig.registry,
10253
+ },
10254
+ `;
10255
+ 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. -->";
10256
+ function installModeOf(skillHub, dependency, binName) {
10257
+ if (skillHub !== void 0 && dependency) throw new UsageError({
10258
+ code: "SKILL_HOMES_CONFLICT",
10259
+ message: "--skill-hub and --dependency each name where the skill comes from: a Dependency install ships it inside the package",
10260
+ 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"
10261
+ });
10262
+ if ((skillHub !== void 0 || dependency) && !SKILL_NAME.test(binName)) throw new UsageError({
10263
+ code: "SKILL_NAME_INVALID",
10264
+ message: `${binName} cannot name a skill, whose names are lowercase letters, digits and single hyphens`,
10265
+ hint: `choose a name such as my-cli, or leave out ${dependency ? "--dependency" : "--skill-hub"}`
10266
+ });
10267
+ return dependency ? { mode: "dependency" } : {
10268
+ mode: "global",
10269
+ skillHub
10270
+ };
10271
+ }
10272
+ async function partialsOf(mode, values) {
10273
+ const partial = async (name) => fill(await readFile(join(PARTIALS_DIR, mode, `${name}.md`), "utf8"), values, false).trimEnd();
10274
+ const [installLines, skillCheck, installSteps, updateSteps, sandbox] = await Promise.all([
10275
+ partial("installLines"),
10276
+ partial("skillCheck"),
10277
+ partial("installSteps"),
10278
+ partial("updateSteps"),
10279
+ partial("sandbox")
10280
+ ]);
10281
+ return {
10282
+ installLines,
10283
+ skillCheck,
10284
+ installSteps,
10285
+ updateSteps,
10286
+ sandbox
10287
+ };
10288
+ }
10289
+ async function globalSetup(skillHub, packageName, binName) {
10290
+ const skillLine = skillHub === void 0 ? NO_SKILL_HUB : `\`! npx -y skills add ${skillHub} --skill ${binName} -g\``;
10291
+ const runAs = runAsOf({ name: binName });
10292
+ const url = JSON.stringify(skillHub);
10293
+ return {
10294
+ values: {
10295
+ runAs,
10296
+ doctorBin: "",
10297
+ ...await partialsOf("global", {
10298
+ packageName,
10299
+ binName,
10300
+ runAs,
10301
+ skillLine
10302
+ })
10303
+ },
10304
+ edits: skillHub === void 0 ? [] : [{
10305
+ file: "src/cli.ts",
10306
+ find: COMMANDS_ANCHOR,
10307
+ replace: ` skill: { source: ${url} },\n${COMMANDS_ANCHOR}`
10308
+ }, {
10309
+ file: "scripts/release.ts",
10310
+ find: " smoke: {",
10311
+ replace: ` skillHub: ${url},\n smoke: {`
10312
+ }],
10313
+ files: []
10314
+ };
10315
+ }
10316
+ async function dependencySetup(packageName, binName) {
10317
+ const runAs = runAsOf({
10318
+ name: binName,
10319
+ install: {
10320
+ mode: "dependency",
10321
+ package: packageName
10322
+ }
10323
+ });
10324
+ return {
10325
+ values: {
10326
+ runAs,
10327
+ doctorBin: "src/bin.ts",
10328
+ ...await partialsOf("dependency", {
10329
+ packageName,
10330
+ binName,
10331
+ runAs
10332
+ })
10333
+ },
10334
+ edits: [{
10335
+ file: "src/cli.ts",
10336
+ find: GLOBAL_INSTALL,
10337
+ replace: ` install: { mode: "dependency", package: manifest.name },\n`
10338
+ }, {
10339
+ file: "src/cli.ts",
10340
+ find: COMMANDS_ANCHOR,
10341
+ replace: ` skill: { packaged: "skills/${binName}" },\n${COMMANDS_ANCHOR}`
10342
+ }],
10343
+ files: ["skills"]
10344
+ };
10345
+ }
10346
+ async function installSetupOf(install, packageName, binName) {
10347
+ return install.mode === "global" ? globalSetup(install.skillHub, packageName, binName) : dependencySetup(packageName, binName);
10348
+ }
10349
+ //#endregion
9789
10350
  //#region src/steps.ts
9790
10351
  const stepErrors = defineErrors({ STEP_FAILED: { class: "operator" } });
9791
10352
  const INSTALL = [{
@@ -9895,13 +10456,6 @@ async function templateFiles(values) {
9895
10456
  }));
9896
10457
  }
9897
10458
  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
10459
  async function writeTemplate(root, files, values) {
9906
10460
  for (const file of files) {
9907
10461
  const text = await readFile(join(TEMPLATE_DIR, file.source), "utf8");
@@ -9917,17 +10471,6 @@ const version = version$1;
9917
10471
  //#region src/create.ts
9918
10472
  const { defineCommand } = agentCli();
9919
10473
  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
10474
  function errorCode(error) {
9932
10475
  return error instanceof Error && "code" in error ? error.code : void 0;
9933
10476
  }
@@ -9940,30 +10483,24 @@ async function isFree(path) {
9940
10483
  throw error;
9941
10484
  }
9942
10485
  }
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
- }
10486
+ async function applyEdit(root, { file, find, replace }) {
10487
+ const path = join(root, file);
10488
+ const text = await readFile(path, "utf8");
10489
+ if (!text.includes(find)) throw new Error(`the template's ${file} has no \`${find.trim()}\` to replace`);
10490
+ await writeFile(path, text.replace(find, replace));
9953
10491
  }
9954
10492
  const MANIFEST = record(string(), unknown());
9955
- async function setRepository(root, repository) {
10493
+ async function editManifest(root, edit) {
9956
10494
  const path = join(root, "package.json");
9957
10495
  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]);
10496
+ const entries = edit(Object.entries(manifest));
9959
10497
  await writeFile(path, `${JSON.stringify(Object.fromEntries(entries), null, 2)}\n`);
9960
10498
  }
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
- });
10499
+ async function setRepository(root, repository) {
10500
+ await editManifest(root, (entries) => entries.flatMap((entry) => entry[0] === "description" ? [entry, ["repository", repository]] : [entry]));
10501
+ }
10502
+ async function addFiles(root, files) {
10503
+ await editManifest(root, (entries) => entries.map(([key, value]) => key === "files" && Array.isArray(value) ? [key, [...value, ...files]] : [key, value]));
9967
10504
  }
9968
10505
  async function ensureFree(path) {
9969
10506
  if (!await isFree(path)) throw new UsageError({
@@ -9986,20 +10523,22 @@ const create = defineCommand({
9986
10523
  install: boolean().default(true).describe("run pnpm install"),
9987
10524
  git: boolean().default(true).describe("create a git repository with an initial commit"),
9988
10525
  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")
10526
+ 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"),
10527
+ dependency: boolean().default(false).describe("install the CLI per project, as a devDependency, with its skill shipped inside the package")
9990
10528
  }),
9991
- plan: async ({ name, description, install, git, repository, skillHub }, ctx) => {
10529
+ plan: async ({ name, description, install, git, repository, skillHub, dependency }, ctx) => {
9992
10530
  const binName = PACKAGE_NAME.exec(name)?.[1] ?? name;
9993
- if (skillHub !== void 0) ensureSkillName(binName);
10531
+ const mode = installModeOf(skillHub, dependency, binName);
9994
10532
  const path = resolve(ctx.cwd, binName);
9995
10533
  await ensureFree(path);
10534
+ const setup = await installSetupOf(mode, name, binName);
9996
10535
  const values = {
9997
10536
  packageName: name,
9998
10537
  binName,
9999
10538
  description,
10000
10539
  debugVariable: debugVariable(binName),
10001
10540
  runtimeVersion: version,
10002
- skillInstall: skillInstallLine(skillHub, binName)
10541
+ ...setup.values
10003
10542
  };
10004
10543
  return {
10005
10544
  path,
@@ -10011,14 +10550,16 @@ const create = defineCommand({
10011
10550
  }),
10012
10551
  install,
10013
10552
  repository,
10014
- skillHub
10553
+ edits: setup.edits,
10554
+ manifestFiles: setup.files
10015
10555
  };
10016
10556
  },
10017
10557
  apply: async (plan, ctx) => {
10018
10558
  await mkdir(plan.path, { recursive: true });
10019
10559
  await writeTemplate(plan.path, plan.files, plan.values);
10020
10560
  if (plan.repository !== void 0) await setRepository(plan.path, plan.repository);
10021
- if (plan.skillHub !== void 0) await setSkillHub(plan.path, plan.skillHub);
10561
+ for (const edit of plan.edits) await applyEdit(plan.path, edit);
10562
+ if (plan.manifestFiles.length > 0) await addFiles(plan.path, plan.manifestFiles);
10022
10563
  await runSteps(plan.steps, plan.path, ctx);
10023
10564
  return {
10024
10565
  path: plan.path,