@duyquangnvx/create-agent-cli 0.13.0 → 0.14.0

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