@duyquangnvx/create-agent-cli 0.22.0 → 0.24.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
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
2
3
  import { spawn } from "node:child_process";
3
4
  import { constants, homedir, tmpdir } from "node:os";
4
5
  import { parseArgs, parseEnv } from "node:util";
5
6
  import { constants as constants$1, existsSync, readFileSync, realpathSync } from "node:fs";
6
7
  import { access, chmod, link, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
7
- import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
8
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
9
9
  import { fileURLToPath } from "node:url";
10
10
  //#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/util.js
@@ -5959,8 +5959,12 @@ async function orMissing(pending) {
5959
5959
  function isNothingThere(error) {
5960
5960
  return isMissingFile(error) || errnoOf(error) === "ENOTDIR";
5961
5961
  }
5962
+ /** The CLI's own environment variable, such as `TOOL_DEBUG` for `cliVariable("tool", "DEBUG")`. */
5963
+ function cliVariable(cliName, suffix) {
5964
+ return `${cliName.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_${suffix}`;
5965
+ }
5962
5966
  function debugVariable(cliName) {
5963
- return `${cliName.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_DEBUG`;
5967
+ return cliVariable(cliName, "DEBUG");
5964
5968
  }
5965
5969
  function messageOf(error) {
5966
5970
  return error instanceof Error ? error.message : String(error);
@@ -7081,9 +7085,9 @@ function secretRows(secrets, commands) {
7081
7085
  /** The bundler's stamp, absent when the CLI runs unbundled. Read per call, so a test can define it. */
7082
7086
  function buildStamp() {
7083
7087
  return {
7084
- "sha": "4664298",
7088
+ "sha": "45caa4e",
7085
7089
  "dirty": false,
7086
- "builtAt": "2026-11-02T04:03:08.000Z"
7090
+ "builtAt": "2026-11-05T07:43:22.000Z"
7087
7091
  };
7088
7092
  }
7089
7093
  /** The first line of `--version`: the version, and the commit and time of a stamped build. */
@@ -7233,6 +7237,16 @@ function installCommandLine(registry, version = "latest") {
7233
7237
  ...registry.flags
7234
7238
  ].join(" ");
7235
7239
  }
7240
+ /** The line that sets a Global install up, run before the CLI is on the machine. */
7241
+ function setupCommandLine({ package: name, flags }) {
7242
+ return [
7243
+ "npx",
7244
+ "-y",
7245
+ ...flags,
7246
+ `${name}@latest`,
7247
+ "setup"
7248
+ ].join(" ");
7249
+ }
7236
7250
  function npmOf(ctx, windows) {
7237
7251
  const beside = ctx.node === void 0 ? void 0 : join(dirname(ctx.node), windows ? "npm.cmd" : "npm");
7238
7252
  if (beside === void 0 || !existsSync(beside)) return {
@@ -7276,13 +7290,13 @@ function failureOf(call, answer, ctx) {
7276
7290
  const code = NPM_CODE.exec(answer.stderr)?.[1];
7277
7291
  const output = tail$1(answer.stderr);
7278
7292
  const next = [ctx.rerun];
7279
- if (code !== void 0 && NETWORK_CODES.has(code)) return new CliError({
7293
+ if (code !== void 0 && NETWORK_CODES.has(code)) return runtimeError({
7280
7294
  code: REGISTRY_UNREACHABLE,
7281
7295
  message: `npm could not reach ${registry} (${code})${output}`,
7282
7296
  hint: `${unreachableHint}, then ${RUN_AGAIN}`,
7283
7297
  next
7284
7298
  });
7285
- if (code !== void 0 && AUTH_CODES.has(code)) return new CliError({
7299
+ if (code !== void 0 && AUTH_CODES.has(code)) return runtimeError({
7286
7300
  code: REGISTRY_AUTH,
7287
7301
  message: `${registry} refused npm's credentials (${code})${output}`,
7288
7302
  hint: authHint(code, ctx),
@@ -7295,7 +7309,7 @@ function failureOf(call, answer, ctx) {
7295
7309
  hint: `fix what npm reports, then ${RUN_AGAIN} with the first Next step, or install it yourself with the second`,
7296
7310
  next: [...next, wholeLine(installCommandLine(ctx.registry))]
7297
7311
  });
7298
- return new CliError({
7312
+ return runtimeError({
7299
7313
  code: NPM_FAILED,
7300
7314
  message: `npm ${call} failed with exit code ${answer.exitCode}${output}`,
7301
7315
  hint: `fix what npm reports, then ${RUN_AGAIN}`,
@@ -7361,15 +7375,18 @@ async function installGlobally(version, ctx) {
7361
7375
  ...ctx.registry.flags
7362
7376
  ], ctx, null);
7363
7377
  }
7364
- async function latestVersion(ctx) {
7365
- const { package: name, label: registry, flags } = ctx.registry;
7366
- const answer = await runNpm("view", [
7378
+ function latestViewArgs({ package: name, flags }) {
7379
+ return [
7367
7380
  "view",
7368
7381
  `${name}@latest`,
7369
7382
  "version",
7370
7383
  "--json",
7371
7384
  ...flags
7372
- ], ctx);
7385
+ ];
7386
+ }
7387
+ async function latestVersion(ctx) {
7388
+ const { package: name, label: registry } = ctx.registry;
7389
+ const answer = await runNpm("view", latestViewArgs(ctx.registry), ctx);
7373
7390
  const version = versionInViewAnswer(answer);
7374
7391
  if (version === void 0) throw noLatest(ctx, `npm answered no version for ${name}@latest on ${registry}: ${answer.trim()}`);
7375
7392
  return version;
@@ -7611,184 +7628,8 @@ function dependencyInstall(cli, install) {
7611
7628
  runAs,
7612
7629
  builtIns: () => ({}),
7613
7630
  exitClasses: [],
7614
- check: (launch) => checkOf$1(cli, install, runAs, launch)
7615
- };
7616
- }
7617
- //#endregion
7618
- //#region ../agent-cli/dist/built-ins/skill/hub.js
7619
- const SKILL_DIRS = [[".claude", "skills"], [".agents", "skills"]];
7620
- const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
7621
- const METADATA_VERSION = /^metadata:[ \t]*\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+version:[ \t]*["']?([^"'\s]+)["']?[ \t]*$/m;
7622
- /** The version a SKILL.md's frontmatter `metadata.version` states. */
7623
- function skillVersionIn(skill) {
7624
- const frontmatter = FRONTMATTER.exec(skill)?.[1];
7625
- return frontmatter === void 0 ? void 0 : METADATA_VERSION.exec(`${frontmatter}\n`)?.[1];
7626
- }
7627
- function homeOf(env) {
7628
- return env.HOME ?? env.USERPROFILE ?? homedir();
7629
- }
7630
- async function copyAt(path, version) {
7631
- let skill;
7632
- try {
7633
- skill = await readFile(join(path, "SKILL.md"), "utf8");
7634
- } catch (error) {
7635
- if (isMissingFile(error)) return;
7636
- return {
7637
- path,
7638
- version: void 0,
7639
- state: "unreadable"
7640
- };
7641
- }
7642
- const stated = skillVersionIn(skill);
7643
- if (stated === void 0 || !isVersion(stated) || !isVersion(version)) return {
7644
- path,
7645
- version: stated,
7646
- state: "unversioned"
7647
- };
7648
- return {
7649
- path,
7650
- version: stated,
7651
- state: standingOf$1(stated, version)
7652
- };
7653
- }
7654
- async function skillCopies(cliName, version, { cwd, env }) {
7655
- const paths = [cwd, homeOf(env)].flatMap((root) => SKILL_DIRS.map((dir) => join(root, ...dir, cliName)));
7656
- const seen = /* @__PURE__ */ new Set();
7657
- const copies = [];
7658
- for (const path of paths) {
7659
- const copy = await copyAt(path, version);
7660
- const real = copy === void 0 ? void 0 : await realpath(path);
7661
- if (copy !== void 0 && real !== void 0 && !seen.has(real)) {
7662
- seen.add(real);
7663
- copies.push(copy);
7664
- }
7665
- }
7666
- return copies;
7667
- }
7668
- const has = (copies, wanted) => copies.some(({ state }) => state === wanted);
7669
- function addLineOf(cliName, from) {
7670
- return "add" in from ? from.add : `npx -y skills add ${from.source} --skill ${cliName} -g`;
7671
- }
7672
- function fixFor(cliName, copies, from) {
7673
- const update = `npx -y skills update ${cliName}`;
7674
- const add = from === void 0 ? void 0 : addLineOf(cliName, from);
7675
- if (copies.length === 0) return add;
7676
- if (has(copies, "unversioned") && add !== void 0) return has(copies, "behind") ? `${update} && ${add}` : add;
7677
- return has(copies, "behind") || has(copies, "unversioned") ? update : void 0;
7678
- }
7679
- const SKILL_STATE_NOTES = {
7680
- current: "the same as this build",
7681
- behind: "older than this build",
7682
- ahead: "newer than this build",
7683
- unversioned: "states no metadata.version",
7684
- unreadable: "could not be read"
7685
- };
7686
- function describeCopy({ path, version, state }) {
7687
- return `${path}${version === void 0 ? "" : ` ${version}`} (${SKILL_STATE_NOTES[state]})`;
7688
- }
7689
- /** A skill on a Skill Hub, which `from` says how to install; without it, no line adds it. */
7690
- function hubSkill(cli, from) {
7691
- const findingOf = (copies) => {
7692
- const fix = fixFor(cli.name, copies, from);
7693
- const message = copies.length === 0 ? `no copy of the ${cli.name} skill is installed` : copies.map(describeCopy).join("; ");
7694
- return fix === void 0 ? {
7695
- state: "ok",
7696
- message
7697
- } : {
7698
- state: "ok",
7699
- message,
7700
- fix
7701
- };
7702
- };
7703
- return {
7704
- kind: "hub",
7705
- builtIns: {},
7706
- rows: [{
7707
- name: "skill",
7708
- advisory: true,
7709
- run: async (ctx) => findingOf(await skillCopies(cli.name, cli.version, ctx))
7710
- }],
7711
- notice: async () => {},
7712
- differing: async (place) => (await skillCopies(cli.name, cli.version, place)).filter(({ state }) => state !== "current" && state !== "unreadable").map(describeCopy),
7713
- nextAt: async (version, ctx) => {
7714
- const copies = await skillCopies(cli.name, version, ctx);
7715
- const fix = fixFor(cli.name, copies, from);
7716
- return fix === void 0 ? [] : [fix];
7717
- }
7718
- };
7719
- }
7720
- //#endregion
7721
- //#region ../agent-cli/dist/built-ins/agents-md.js
7722
- function markersOf(name) {
7723
- return {
7724
- begin: `<!-- BEGIN:${name} -->`,
7725
- end: `<!-- END:${name} -->`
7726
- };
7727
- }
7728
- /** `text` with the CLI's Agents section between its markers, appended when they are missing; nothing else changes. */
7729
- function withSection({ name, text: section }, text) {
7730
- const { begin, end } = markersOf(name);
7731
- const whole = `${begin}\n${section}\n${end}`;
7732
- const start = text.indexOf(begin);
7733
- const stop = start === -1 ? -1 : text.indexOf(end, start);
7734
- if (stop !== -1) return `${text.slice(0, start)}${whole}${text.slice(stop + end.length)}`;
7735
- if (text === "") return `${whole}\n`;
7736
- return `${text}${text.endsWith("\n") ? "" : "\n"}\n${whole}\n`;
7737
- }
7738
- async function agentsMdAt(agents, root) {
7739
- const path = join(root, "AGENTS.md");
7740
- const existing = await orMissing(readFile(path, "utf8"));
7741
- if (existing === void 0) return {
7742
- path,
7743
- agents,
7744
- change: "create"
7745
- };
7746
- if (withSection(agents, existing) === existing) return {
7747
- path,
7748
- agents,
7749
- change: null
7750
- };
7751
- return {
7752
- path,
7753
- agents,
7754
- change: existing.includes(markersOf(agents.name).begin) ? "rewrite" : "add"
7755
- };
7756
- }
7757
- async function handEditsAt(root) {
7758
- const claudeMd = join(root, "CLAUDE.md");
7759
- const text = await orMissing(readFile(claudeMd, "utf8"));
7760
- if (text === void 0 || text.split(/\r?\n/).some((line) => line.trim() === "@AGENTS.md")) return [];
7761
- return [`add a line \`@AGENTS.md\` to ${claudeMd}, so Claude Code reads AGENTS.md`];
7762
- }
7763
- function agentsRow(agents, install) {
7764
- const { name } = agents;
7765
- return {
7766
- name: "agents",
7767
- advisory: true,
7768
- run: async (ctx) => {
7769
- const root = await projectRootOf(ctx.cwd);
7770
- const { path, change } = await agentsMdAt(agents, root);
7771
- const fix = `${install.runAs} skill install`;
7772
- if (change === "rewrite") return {
7773
- state: "problem",
7774
- message: `${path} holds a ${name} Agents section other than the one ${fix} writes`,
7775
- fix
7776
- };
7777
- if (change !== null) return {
7778
- state: "problem",
7779
- message: `${path} holds no ${name} Agents section, so sessions in this repo do not know ${name}`,
7780
- fix
7781
- };
7782
- const holds = `${path} holds the ${name} Agents section`;
7783
- const [edit] = await handEditsAt(root);
7784
- return edit === void 0 ? {
7785
- state: "ok",
7786
- message: holds
7787
- } : {
7788
- state: "problem",
7789
- message: `${holds}; ${edit}`
7790
- };
7791
- }
7631
+ check: (launch) => checkOf$1(cli, install, runAs, launch),
7632
+ notice: async () => {}
7792
7633
  };
7793
7634
  }
7794
7635
  //#endregion
@@ -7854,259 +7695,208 @@ function classifyErrno(error, context) {
7854
7695
  }
7855
7696
  }
7856
7697
  //#endregion
7857
- //#region ../agent-cli/dist/context/files.js
7858
- const TEMP_FILE = /^\..+\.agent-cli-(\d+)\.tmp$/;
7859
- /** Where the Runtime writes `path` before moving it into place; a dead writer's is swept. */
7860
- function tempPathOf(path) {
7861
- return join(dirname(path), `.${basename(path)}.agent-cli-${process.pid}.tmp`);
7698
+ //#region ../agent-cli/dist/context/interrupts.js
7699
+ const INTERRUPTED = "INTERRUPTED";
7700
+ const DEADLINE_EXCEEDED = "DEADLINE_EXCEEDED";
7701
+ /** How long an interrupted Command may take to settle before the run ends without it. */
7702
+ const GRACE_MS = 3e3;
7703
+ const GRACE_OVER = Symbol("grace over");
7704
+ function interruptedError(rerun) {
7705
+ return new CliError({
7706
+ code: INTERRUPTED,
7707
+ message: "interrupted before it finished",
7708
+ hint: "to finish it, run the Next step, unless it was stopped on purpose; if a tool timeout stopped it, give it longer",
7709
+ next: [rerun]
7710
+ });
7862
7711
  }
7863
- function isAlive(pid) {
7864
- try {
7865
- process.kill(pid, 0);
7866
- return true;
7867
- } catch (error) {
7868
- return errnoOf(error) === "EPERM";
7869
- }
7712
+ const BOUND_WORDS = {
7713
+ deadline: "deadline",
7714
+ ignoreStop: "timeout"
7715
+ };
7716
+ function waitHint(bound) {
7717
+ return bound === "ignoreStop" ? "the call may have been billed: ask your user before running the Next step" : "run the Next step later, when the service it waits on may answer faster";
7870
7718
  }
7871
- async function sweep(folder) {
7872
- const dead = (await readdir(folder)).filter((name) => {
7873
- const pid = TEMP_FILE.exec(name)?.[1];
7874
- return pid !== void 0 && !isAlive(Number(pid));
7719
+ function deadlineError(commandLine, ms, advice, bound = "deadline") {
7720
+ return new CliError({
7721
+ code: DEADLINE_EXCEEDED,
7722
+ message: `${commandLine} ran past its ${ms} ms ${BOUND_WORDS[bound]}`,
7723
+ ...advice
7875
7724
  });
7876
- await Promise.all(dead.map((name) => rm(join(folder, name), { force: true })));
7877
- }
7878
- /** The file a symlink at `path` points to, so a replacing write keeps the link. */
7879
- async function targetOf$1(path) {
7880
- try {
7881
- return await realpath(path);
7882
- } catch (error) {
7883
- if (isMissingFile(error)) return path;
7884
- throw error;
7885
- }
7886
7725
  }
7887
- async function writeWithMode(path, data, mode) {
7888
- if (mode === void 0) {
7889
- await writeFile(path, data);
7890
- return;
7726
+ /** One Command run's cancellation: its signal, its deadlines and the steps it must finish. */
7727
+ var RunControl = class {
7728
+ signal;
7729
+ #controller = new AbortController();
7730
+ #commandLine;
7731
+ #rerun;
7732
+ #outer;
7733
+ #onOuterAbort = () => this.#controller.abort(interruptedError(this.#rerun));
7734
+ /** Each running must-finish step, tracked until it settles, with its name. */
7735
+ #steps = /* @__PURE__ */ new Map();
7736
+ #progress;
7737
+ #clock;
7738
+ #disposed = new AbortController();
7739
+ /** The reasons this run's deadlines aborted with, to tell them in a Command's error. */
7740
+ #abortReasons = /* @__PURE__ */ new WeakSet();
7741
+ #graceCancel = new AbortController();
7742
+ #endGrace = () => {};
7743
+ #graceOver = new Promise((resolve) => {
7744
+ this.#endGrace = resolve;
7745
+ });
7746
+ constructor({ commandLine, rerun, outer, clock, progress, runDeadline }) {
7747
+ this.#commandLine = commandLine;
7748
+ this.#rerun = rerun;
7749
+ this.#outer = outer;
7750
+ this.#clock = clock;
7751
+ this.#progress = progress;
7752
+ this.signal = this.#controller.signal;
7753
+ this.signal.addEventListener("abort", () => this.#onStop(), { once: true });
7754
+ if (outer?.aborted === true) this.#onOuterAbort();
7755
+ else outer?.addEventListener("abort", this.#onOuterAbort, { once: true });
7756
+ if (runDeadline !== void 0) {
7757
+ const { ms, advice } = runDeadline;
7758
+ after(clock, ms, this.#disposed.signal, () => this.#controller.abort(deadlineError(commandLine, ms, advice())));
7759
+ }
7891
7760
  }
7892
- await writeFile(path, data, { mode });
7893
- await chmod(path, mode);
7894
- }
7895
- /** What `link` answers on a file system without hard links, such as exFAT. */
7896
- const NO_HARD_LINKS = /* @__PURE__ */ new Set([
7897
- "EPERM",
7898
- "ENOTSUP",
7899
- "EXDEV"
7900
- ]);
7901
- /** A link fails atomically when `path` is taken, so no reader ever sees half a file there. */
7902
- async function linkNew(temp, path, data, mode) {
7903
- try {
7904
- await link(temp, path);
7905
- } catch (error) {
7906
- if (!NO_HARD_LINKS.has(errnoOf(error))) throw error;
7907
- await createNew(path, data, mode);
7761
+ deadline(ms, options = {}) {
7762
+ return options.ignoreStop === true ? this.#abortsAfter(ms, "ignoreStop") : AbortSignal.any([this.signal, this.#abortsAfter(ms, "deadline")]);
7908
7763
  }
7909
- }
7910
- /** A file this call created and could not finish is removed, so a rerun does not find half of it. */
7911
- async function createNew(path, data, mode) {
7912
- const handle = await open(path, "wx", mode);
7913
- try {
7914
- await handle.writeFile(data);
7915
- if (mode !== void 0) await handle.chmod(mode);
7916
- } catch (error) {
7917
- await handle.close();
7918
- await rm(path, { force: true });
7919
- throw error;
7764
+ #abortsAfter(ms, bound) {
7765
+ const controller = new AbortController();
7766
+ after(this.#clock, ms, this.#disposed.signal, () => {
7767
+ const advice = {
7768
+ hint: waitHint(bound),
7769
+ next: [this.#rerun]
7770
+ };
7771
+ const reason = deadlineError(this.#commandLine, ms, advice, bound);
7772
+ this.#abortReasons.add(reason);
7773
+ controller.abort(reason);
7774
+ });
7775
+ return controller.signal;
7920
7776
  }
7921
- await handle.close();
7922
- }
7923
- const RENAME_ATTEMPTS = 10;
7924
- const RENAME_BACKOFF_MS = 10;
7925
- const HELD_BY_ANOTHER_PROGRAM = /* @__PURE__ */ new Set(["EPERM", "EBUSY"]);
7926
- async function renameRetrying(from, to) {
7927
- for (let attempt = 1;; attempt++) try {
7928
- await rename(from, to);
7929
- return;
7930
- } catch (error) {
7931
- if (!(process.platform === "win32" && HELD_BY_ANOTHER_PROGRAM.has(errnoOf(error))) || attempt === RENAME_ATTEMPTS) throw error;
7932
- await setTimeout$1(attempt * RENAME_BACKOFF_MS);
7933
- }
7934
- }
7935
- /** Real files for one run: the first write to each folder sweeps the temp files of dead writers. */
7936
- function realFiles() {
7937
- const swept = /* @__PURE__ */ new Set();
7938
- return {
7939
- exists: async (path) => {
7940
- try {
7941
- await access(path);
7942
- return true;
7943
- } catch (error) {
7944
- if (isNothingThere(error)) return false;
7945
- throw error;
7946
- }
7947
- },
7948
- write: async (path, data, options = {}) => {
7949
- await mkdir(dirname(path), { recursive: true });
7950
- const target = options.exclusive === true ? path : await targetOf$1(path);
7951
- const folder = dirname(target);
7952
- if (!swept.has(folder)) {
7953
- await sweep(folder);
7954
- swept.add(folder);
7955
- }
7956
- const temp = tempPathOf(target);
7957
- try {
7958
- await writeWithMode(temp, data, options.mode);
7959
- if (options.exclusive === true) {
7960
- await linkNew(temp, target, data, options.mode);
7961
- await rm(temp);
7962
- } else await renameRetrying(temp, target);
7963
- } catch (error) {
7964
- await rm(temp, { force: true });
7965
- throw error;
7966
- }
7967
- },
7968
- read: (path) => readFile(path),
7969
- list: (folder) => readdir(folder),
7970
- remove: (path) => rm(path, {
7971
- recursive: true,
7972
- force: true
7973
- }),
7974
- mkdir: async (path, options) => {
7975
- const mode = options?.mode;
7976
- if (options?.exclusive !== true) {
7977
- await mkdir(path, {
7978
- recursive: true,
7979
- mode
7980
- });
7981
- return;
7982
- }
7983
- await mkdir(dirname(path), {
7984
- recursive: true,
7985
- mode
7986
- });
7987
- await mkdir(path, { mode });
7988
- },
7989
- stat: async (path) => {
7990
- try {
7991
- const { size, dev, ino } = await stat(path, { bigint: true });
7992
- return {
7993
- size: Number(size),
7994
- id: `${dev}:${ino}`
7995
- };
7996
- } catch (error) {
7997
- if (isNothingThere(error)) return void 0;
7998
- throw error;
7999
- }
7777
+ async runToEnd(name, work) {
7778
+ this.#graceCancel.abort();
7779
+ if (this.signal.aborted) this.#finishing(name);
7780
+ const step = work();
7781
+ const settled = step.then(() => {}, () => {});
7782
+ this.#steps.set(settled, name);
7783
+ try {
7784
+ return await step;
7785
+ } finally {
7786
+ this.#steps.delete(settled);
7787
+ this.#startGrace();
8000
7788
  }
8001
- };
8002
- }
8003
- /**
8004
- * `files` resolving paths against `cwd`, its failures classified by errno; `list` drops the
8005
- * Runtime's temp files and sorts.
8006
- */
8007
- function classifiedFiles(files, { cwd, fileError }) {
8008
- async function at(access, path, work) {
8009
- const target = resolve(cwd, path);
7789
+ }
7790
+ /** Waits for the Command, its must-finish steps, and no longer than the grace once interrupted. */
7791
+ async settle(work) {
8010
7792
  try {
8011
- return await work(target);
8012
- } catch (error) {
8013
- throw fileError(error, {
8014
- path: target,
8015
- access
8016
- });
7793
+ const value = await Promise.race([work, this.#graceOver]);
7794
+ if (this.signal.aborted || value === GRACE_OVER) throw this.signal.reason;
7795
+ return value;
7796
+ } finally {
7797
+ await this.#stepsDone();
7798
+ this.close();
8017
7799
  }
8018
7800
  }
8019
- return {
8020
- exists: (path) => at("read", path, (target) => files.exists(target)),
8021
- write: (path, data, options) => at("write", path, (target) => files.write(target, data, options)),
8022
- read: (path) => at("read", path, (target) => files.read(target)),
8023
- list: (folder) => at("read", folder, async (target) => (await files.list(target)).filter((name) => !TEMP_FILE.test(name)).sort()),
8024
- remove: (path) => at("write", path, (target) => files.remove(target)),
8025
- mkdir: (path, options) => at("write", path, (target) => files.mkdir(target, options)),
8026
- stat: (path) => at("read", path, (target) => files.stat(target))
8027
- };
8028
- }
7801
+ /**
7802
+ * The error a run ends with: an interrupt or the run deadline outranks what the Command threw,
7803
+ * unless the Command threw its own `INTERRUPTED`, saying what it finished before it stopped.
7804
+ * Otherwise each leaf that is not a `CliError` and whose cause chain holds a deadline's abort is
7805
+ * reported as that abort, so a declared error keeps its own code.
7806
+ */
7807
+ errorAfter(error) {
7808
+ if (this.signal.aborted) return error instanceof CliError && error.code === "INTERRUPTED" ? error : this.signal.reason;
7809
+ const leaves = errorsOf(error).map((leaf) => this.#abortIn(leaf) ?? leaf);
7810
+ return leaves.length === 1 ? leaves[0] : new AggregateError(leaves);
7811
+ }
7812
+ #abortIn(error) {
7813
+ if (error instanceof CliError) return void 0;
7814
+ return causesOf(error).find((each) => each instanceof CliError && this.#abortReasons.has(each));
7815
+ }
7816
+ #onStop() {
7817
+ for (const name of this.#steps.values()) this.#finishing(name);
7818
+ this.#startGrace();
7819
+ }
7820
+ #finishing(name) {
7821
+ this.#progress(`finishing ${name}`);
7822
+ }
7823
+ #startGrace() {
7824
+ if (this.signal.aborted && this.#steps.size === 0) {
7825
+ this.#graceCancel.abort();
7826
+ this.#graceCancel = new AbortController();
7827
+ after(this.#clock, GRACE_MS, this.#graceCancel.signal, () => this.#endGrace(GRACE_OVER));
7828
+ }
7829
+ }
7830
+ async #stepsDone() {
7831
+ while (this.#steps.size > 0) await Promise.all(this.#steps.keys());
7832
+ }
7833
+ /** Stops the run's timers and its hold on the caller's signal, for a run that never settles. */
7834
+ close() {
7835
+ this.#graceCancel.abort();
7836
+ this.#disposed.abort();
7837
+ this.#outer?.removeEventListener("abort", this.#onOuterAbort);
7838
+ }
7839
+ };
8029
7840
  //#endregion
8030
- //#region ../agent-cli/dist/context/outputs.js
8031
- const OUTPUT_EXISTS = "OUTPUT_EXISTS";
8032
- /** Refuses a run that would overwrite existing outputs unless the caller passed `--force`. */
8033
- async function checkOutputs({ rerun, commandLine, force, files }, outputs) {
8034
- if (outputs === void 0 || force) return;
8035
- const found = await Promise.all(outputs.map((path) => files.exists(path)));
8036
- const existing = outputs.filter((_path, index) => found[index]);
8037
- if (existing.length === 0) return;
8038
- const them = existing.length === 1 ? "it" : "them";
8039
- throw new CliError({
8040
- code: OUTPUT_EXISTS,
8041
- message: `\`${commandLine}\` would overwrite ${existing.join(", ")}`,
8042
- hint: `once the user agrees to replace ${them}, run the Next step`,
8043
- next: [rerun({ add: [FORCE_FLAG.flag] })]
7841
+ //#region ../agent-cli/dist/built-ins/install/global-npm.js
7842
+ const NOT_GLOBAL_INSTALL = "NOT_GLOBAL_INSTALL";
7843
+ /** Where npm puts global packages under its global prefix. */
7844
+ function globalModulesDir(prefix) {
7845
+ return process.platform === "win32" ? join(prefix, "node_modules") : join(prefix, "lib", "node_modules");
7846
+ }
7847
+ function notGlobal(ctx, message) {
7848
+ return runtimeError({
7849
+ code: NOT_GLOBAL_INSTALL,
7850
+ message,
7851
+ hint: "install it by running the Next step, so its update Command can replace it",
7852
+ next: [wholeLine(setupCommandLine(ctx.registry))]
8044
7853
  });
8045
7854
  }
8046
- /** Guards the writes to declared outputs of `files`, whose paths resolve against `cwd`. */
8047
- function outputsGuard(files, cwd) {
8048
- const unwritten = /* @__PURE__ */ new Set();
8049
- let forceStep = [];
8050
- const exclusiveWrite = async (path, data, options) => {
8051
- const target = resolve(cwd, path);
8052
- unwritten.delete(target);
8053
- try {
8054
- await files.write(path, data, {
8055
- ...options,
8056
- exclusive: true
8057
- });
8058
- } catch (error) {
8059
- unwritten.add(target);
8060
- if (!(error instanceof CliError) || error.code !== "ALREADY_EXISTS") throw error;
8061
- throw new CliError({
8062
- code: ALREADY_EXISTS,
8063
- message: error.message,
8064
- hint: "once the user agrees to replace it, run the Next step",
8065
- next: [forceStep]
8066
- }, { cause: error });
8067
- }
8068
- };
7855
+ /**
7856
+ * The global npm install this copy runs from, found by asking npm for its global prefix.
7857
+ * `bin` is the script that started the CLI; throws `NOT_GLOBAL_INSTALL` when it is not inside that install.
7858
+ */
7859
+ async function globalInstallOf(bin, ctx) {
7860
+ if (bin === void 0) throw notGlobal(ctx, `${ctx.cliName} runs from source, not from an npm install`);
7861
+ const prefix = await globalPrefix(ctx);
7862
+ const packageDir = join(globalModulesDir(prefix), ctx.registry.package);
7863
+ const installed = await orMissing(realpath(packageDir));
7864
+ const here = await orMissing(realpath(bin)) ?? bin;
7865
+ if (installed === void 0 || !isInside(installed, here)) throw notGlobal(ctx, `${ctx.cliName} runs from ${here}, not from npm's global install at ${packageDir}`);
7866
+ if ((await lstat(packageDir)).isSymbolicLink()) throw notGlobal(ctx, `${ctx.cliName} runs from a link to ${installed} (npm link), not from an npm install`);
8069
7867
  return {
8070
- files: {
8071
- ...files,
8072
- write: (path, data, options) => unwritten.has(resolve(cwd, path)) ? exclusiveWrite(path, data, options) : files.write(path, data, options)
8073
- },
8074
- guard: (outputs, step) => {
8075
- forceStep = step;
8076
- for (const path of outputs) unwritten.add(resolve(cwd, path));
8077
- }
7868
+ prefix,
7869
+ packageDir
8078
7870
  };
8079
7871
  }
8080
- /**
8081
- * Creates each output folder and a temp file in it, so a sandbox denial fails before `apply`
8082
- * spends anything. The temp file's name is the Runtime's, so a crashed run's leftover is swept.
8083
- */
8084
- async function probeOutputs({ files, cwd, fileError }, outputs) {
8085
- const folders = new Set(outputs.map((path) => dirname(resolve(cwd, path))));
8086
- await Promise.all([...folders].map(async (folder) => {
8087
- const probe = tempPathOf(join(folder, "outputs-probe"));
8088
- try {
8089
- await files.mkdir(folder);
8090
- await files.write(probe, "");
8091
- await files.remove(probe);
8092
- } catch (error) {
8093
- throw fileError(error, {
8094
- path: folder,
8095
- access: "write"
8096
- });
8097
- }
8098
- }));
7872
+ const NPMJS = "https://registry.npmjs.org/";
7873
+ /** The flag that points npm at the CLI's registry; none for npmjs, npm's default. */
7874
+ function registryFlags({ package: name, registry }) {
7875
+ if (registry === NPMJS) return [];
7876
+ const scope = scopeOf(name);
7877
+ return [scope === void 0 ? `--registry=${registry}` : `--${scope}:registry=${registry}`];
8099
7878
  }
8100
- //#endregion
8101
- //#region ../agent-cli/dist/built-ins/skill/install-command.js
8102
- /** The project root and the Skill copy under it, as `skill install` writes it. */
8103
- async function skillCopyOf(cli, cwd) {
8104
- const root = await projectRootOf(cwd);
7879
+ /** The line that sets a Global install up, run before the CLI is on the machine. */
7880
+ function setupLineOf(install) {
7881
+ return setupCommandLine(globalRegistry(install));
7882
+ }
7883
+ function globalRegistry(install) {
7884
+ const { package: name, registry } = install;
8105
7885
  return {
8106
- root,
8107
- copy: join(root, ".claude", "skills", cli.name)
7886
+ package: name,
7887
+ label: registry,
7888
+ flags: registryFlags(install),
7889
+ unreachableHint: "check the network or VPN",
7890
+ loginHint: `log in to ${registry} (\`npm login --registry=${registry}\`) or set its token in ~/.npmrc`,
7891
+ missingHint: `check that ${name} is published on ${registry}`
8108
7892
  };
8109
7893
  }
7894
+ /** npm for a Global install, run in the working directory. */
7895
+ function globalNpm(cli, install, ctx, node) {
7896
+ return npmContext(cli, globalRegistry(install), ctx, node, ctx.cwd);
7897
+ }
7898
+ //#endregion
7899
+ //#region ../agent-cli/dist/built-ins/skill/sync.js
8110
7900
  /** Every file under `dir`, relative to it and sorted; none when `dir` is missing. */
8111
7901
  async function filesIn(dir) {
8112
7902
  return (await orMissing(readdir(dir, {
@@ -8114,34 +7904,11 @@ async function filesIn(dir) {
8114
7904
  withFileTypes: true
8115
7905
  })) ?? []).filter((entry) => entry.isFile()).map((entry) => relative(dir, join(entry.parentPath, entry.name))).sort();
8116
7906
  }
8117
- /** The versions of the package's skill in `source` and of its copy in `copy`. */
8118
- async function skillVersionsOf(source, copy) {
8119
- const [shipped, installed] = await Promise.all([readFile(join(source, "SKILL.md"), "utf8"), orMissing(readFile(join(copy, "SKILL.md"), "utf8"))]);
8120
- const packageVersion = skillVersionIn(shipped) ?? null;
8121
- const copyVersion = installed === void 0 ? void 0 : skillVersionIn(installed) ?? null;
8122
- return {
8123
- packageVersion,
8124
- copyVersion,
8125
- same: copyVersion === packageVersion
8126
- };
8127
- }
8128
- const installSchema = object({
8129
- copy: string(),
8130
- written: array(string()),
8131
- removed: array(string()),
8132
- agentsMd: object({
8133
- path: string(),
8134
- changed: boolean()
8135
- }).nullable(),
8136
- handEdits: array(string())
8137
- });
8138
7907
  async function sameContent(left, right) {
8139
7908
  const [a, b] = await Promise.all([readFile(left), readFile(right)]);
8140
7909
  return a.equals(b);
8141
7910
  }
8142
- async function planOf(cli, folder, agents, cwd, packageRoot) {
8143
- const source = join(packageRoot, folder);
8144
- const { root, copy } = await skillCopyOf(cli, cwd);
7911
+ async function syncOf(source, copy) {
8145
7912
  const [shipped, present] = await Promise.all([filesIn(source), filesIn(copy)]);
8146
7913
  const differs = await Promise.all(shipped.map(async (file) => present.includes(file) && !await sameContent(join(source, file), join(copy, file))));
8147
7914
  return {
@@ -8149,604 +7916,1000 @@ async function planOf(cli, folder, agents, cwd, packageRoot) {
8149
7916
  copy,
8150
7917
  write: shipped.filter((file) => !present.includes(file)),
8151
7918
  replace: shipped.filter((_file, index) => differs[index]),
8152
- remove: present.filter((file) => !shipped.includes(file)),
8153
- sameVersion: (await skillVersionsOf(source, copy)).same,
8154
- agentsMd: agents === void 0 ? null : await agentsMdAt(agents, root),
8155
- handEdits: agents === void 0 ? [] : await handEditsAt(root)
7919
+ remove: present.filter((file) => !shipped.includes(file))
7920
+ };
7921
+ }
7922
+ async function applySync(sync, ctx) {
7923
+ for (const file of [...sync.write, ...sync.replace]) await ctx.files.write(join(sync.copy, file), await readFile(join(sync.source, file)));
7924
+ for (const file of sync.remove) await ctx.files.remove(join(sync.copy, file));
7925
+ }
7926
+ //#endregion
7927
+ //#region ../agent-cli/dist/built-ins/skill/home.js
7928
+ const SKILL_DIRS = [[".claude", "skills"], [".agents", "skills"]];
7929
+ const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
7930
+ const METADATA_VERSION = /^metadata:[ \t]*\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+version:[ \t]*["']?([^"'\s]+)["']?[ \t]*$/m;
7931
+ /** The version a SKILL.md's frontmatter `metadata.version` states. */
7932
+ function skillVersionIn(skill) {
7933
+ const frontmatter = FRONTMATTER.exec(skill)?.[1];
7934
+ return frontmatter === void 0 ? void 0 : METADATA_VERSION.exec(`${frontmatter}\n`)?.[1];
7935
+ }
7936
+ /** How the row words a copy that states a version, which only a skill from before CLI docs does. */
7937
+ function statedVersionNote(version) {
7938
+ return `states metadata.version ${version}: an older skill that CLI docs replace`;
7939
+ }
7940
+ function homeOf(env) {
7941
+ return env.HOME ?? env.USERPROFILE ?? homedir();
7942
+ }
7943
+ /** The home folders of the Skill copy; the skills installer may have made one a link to the other. */
7944
+ async function homeCopiesOf(name, env) {
7945
+ const paths = SKILL_DIRS.map((dir) => join(homeOf(env), ...dir, name));
7946
+ const reals = await Promise.all(paths.map(async (path) => orMissing(realpath(path))));
7947
+ return paths.filter((_path, index) => {
7948
+ const real = reals[index];
7949
+ return real === void 0 || reals.indexOf(real) === index;
7950
+ });
7951
+ }
7952
+ const homeSyncSchema = object({
7953
+ copies: array(string()),
7954
+ written: array(string()),
7955
+ removed: array(string())
7956
+ });
7957
+ /**
7958
+ * Makes each home Skill copy match the folder in the package at `packageDir`. Null when the CLI
7959
+ * declares no skill, or that package ships none, which must not empty a copy.
7960
+ */
7961
+ async function syncHome(cli, packageDir, ctx) {
7962
+ if (cli.skill === void 0) return null;
7963
+ const source = join(packageDir, cli.skill.folder);
7964
+ if ((await filesIn(source)).length === 0) return null;
7965
+ const copies = await homeCopiesOf(cli.name, ctx.env);
7966
+ const syncs = await Promise.all(copies.map((copy) => syncOf(source, copy)));
7967
+ for (const sync of syncs) await applySync(sync, ctx);
7968
+ return {
7969
+ copies,
7970
+ written: syncs.flatMap((sync) => [...sync.write, ...sync.replace]),
7971
+ removed: syncs.flatMap((sync) => sync.remove)
7972
+ };
7973
+ }
7974
+ /** What a dry run says of the copies, since the release's files may not be on disk yet. */
7975
+ function renderPlannedSync(name, copies) {
7976
+ return copies.length === 0 ? [] : [`would make the ${name} skill at ${copies.join(", ")} match the release`];
7977
+ }
7978
+ function renderHomeSync(name, sync) {
7979
+ if (sync === null) return [];
7980
+ const at = sync.copies.join(", ");
7981
+ return sync.written.length + sync.removed.length === 0 ? [`the ${name} skill at ${at} already matches the package`] : [`copied the ${name} skill to ${at}: wrote ${sync.written.length} files, removed ${sync.removed.length}`];
7982
+ }
7983
+ async function homeCopyAt(path) {
7984
+ try {
7985
+ return {
7986
+ path,
7987
+ state: "readable",
7988
+ stated: skillVersionIn(await readFile(join(path, "SKILL.md"), "utf8"))
7989
+ };
7990
+ } catch (error) {
7991
+ return {
7992
+ path,
7993
+ state: isMissingFile(error) ? "missing" : "unreadable"
7994
+ };
7995
+ }
7996
+ }
7997
+ function describeCopy(copy) {
7998
+ if (copy.state === "missing") return `no copy at ${copy.path}`;
7999
+ if (copy.state !== "readable") return `${copy.path} (could not be read)`;
8000
+ return copy.stated === void 0 ? copy.path : `${copy.path} (${statedVersionNote(copy.stated)})`;
8001
+ }
8002
+ async function findingOf$1(cli, folder, install, ctx) {
8003
+ const paths = await homeCopiesOf(cli.name, ctx.env);
8004
+ const [copies, shipped] = await Promise.all([Promise.all(paths.map(homeCopyAt)), orMissing(readFile(join(ctx.packageRoot, folder, "SKILL.md"), "utf8"))]);
8005
+ const message = copies.map(describeCopy).join("; ");
8006
+ const shipsStated = shipped !== void 0 && skillVersionIn(shipped) !== void 0;
8007
+ return copies.every((copy) => copy.state === "readable" && (copy.stated === void 0 || shipsStated)) ? {
8008
+ state: "ok",
8009
+ message
8010
+ } : {
8011
+ state: "problem",
8012
+ message,
8013
+ fix: `${install.runAs} setup`
8014
+ };
8015
+ }
8016
+ /** A skill the package ships, which `setup` and `update` copy under home. */
8017
+ function homeSkill(cli, { folder }, install) {
8018
+ return {
8019
+ builtIns: {},
8020
+ rows: [{
8021
+ name: "skill",
8022
+ advisory: true,
8023
+ run: (ctx) => findingOf$1(cli, folder, install, ctx)
8024
+ }]
8025
+ };
8026
+ }
8027
+ //#endregion
8028
+ //#region ../agent-cli/dist/built-ins/upgrades.js
8029
+ /** A skipped release's Upgrade steps, or its whole section when it lists none. */
8030
+ const upgradeSchema = union([object({
8031
+ version: string(),
8032
+ steps: array(string())
8033
+ }), object({
8034
+ version: string(),
8035
+ notes: string()
8036
+ })]);
8037
+ const RELEASE_HEADING = /^## v?(\S+)/;
8038
+ const UPGRADE_HEADING = /^### Upgrade from v\S+$/;
8039
+ const HEADING = /^#{1,6} /;
8040
+ const ITEM = /^[-*] /;
8041
+ const NOTHING_TO_DO = /^nothing to do\.?$/i;
8042
+ function releasesIn(changelog) {
8043
+ const releases = [];
8044
+ let current;
8045
+ for (const line of changelog.split(/\r?\n/)) {
8046
+ const heading = RELEASE_HEADING.exec(line);
8047
+ if (heading !== null) {
8048
+ const version = heading[1] ?? "";
8049
+ current = isVersion(version) ? {
8050
+ version,
8051
+ lines: []
8052
+ } : void 0;
8053
+ if (current !== void 0) releases.push(current);
8054
+ } else current?.lines.push(line);
8055
+ }
8056
+ return releases;
8057
+ }
8058
+ function listedSteps(lines) {
8059
+ const start = lines.findIndex((line) => UPGRADE_HEADING.test(line.trimEnd()));
8060
+ if (start === -1) return [];
8061
+ const end = lines.findIndex((line, i) => i > start && HEADING.test(line));
8062
+ const body = lines.slice(start + 1, end === -1 ? void 0 : end).filter((line) => line.trim() !== "");
8063
+ const starts = body.flatMap((line, i) => ITEM.test(line) ? [i] : []);
8064
+ return starts.map((first, i) => body.slice(first, starts[i + 1]).map((line) => line.trim()).join(" ").replace(ITEM, ""));
8065
+ }
8066
+ function upgradeOf({ version, lines }) {
8067
+ const listed = listedSteps(lines);
8068
+ if (listed.length === 0) return [{
8069
+ version,
8070
+ notes: lines.join("\n").trim()
8071
+ }];
8072
+ const todo = listed.filter((step) => !NOTHING_TO_DO.test(step));
8073
+ return todo.length === 0 ? [] : [{
8074
+ version,
8075
+ steps: todo
8076
+ }];
8077
+ }
8078
+ /**
8079
+ * What each release after `from` up to `to` asks of the operator, oldest first, from the
8080
+ * CHANGELOG.md in `packageDir`; none when the package ships no CHANGELOG.
8081
+ */
8082
+ async function upgradesOf(packageDir, from, to) {
8083
+ let changelog;
8084
+ try {
8085
+ changelog = await readFile(join(packageDir, "CHANGELOG.md"), "utf8");
8086
+ } catch (error) {
8087
+ if (isMissingFile(error)) return [];
8088
+ throw error;
8089
+ }
8090
+ return releasesIn(changelog).filter(({ version }) => compareVersions(version, from) > 0 && compareVersions(version, to) <= 0).sort((a, b) => compareVersions(a.version, b.version)).flatMap(upgradeOf);
8091
+ }
8092
+ function renderUpgrade(upgrade) {
8093
+ return "steps" in upgrade ? [`\nupgrade steps for ${upgrade.version}:`, ...upgrade.steps.map((step) => `- ${step}`)].join("\n") : `\nchanges in ${upgrade.version}:\n${upgrade.notes}`;
8094
+ }
8095
+ //#endregion
8096
+ //#region ../agent-cli/dist/built-ins/setup.js
8097
+ const NOT_ON_PATH = "NOT_ON_PATH";
8098
+ const setupSchema = object({
8099
+ version: string(),
8100
+ prefix: string(),
8101
+ installed: boolean(),
8102
+ skill: homeSyncSchema.nullable(),
8103
+ upgrades: array(upgradeSchema),
8104
+ docs: string().nullable()
8105
+ });
8106
+ const installedManifest = object({ version: string() });
8107
+ async function versionAt(packageDir) {
8108
+ const text = await orMissing(readFile(join(packageDir, "package.json"), "utf8"));
8109
+ if (text === void 0) return null;
8110
+ try {
8111
+ const parsed = installedManifest.safeParse(JSON.parse(text));
8112
+ return parsed.success ? parsed.data.version : null;
8113
+ } catch (error) {
8114
+ if (error instanceof SyntaxError) return null;
8115
+ throw error;
8116
+ }
8117
+ }
8118
+ function npmOfSetup(cli, install, ctx, node) {
8119
+ return {
8120
+ ...globalNpm(cli, install, ctx, node),
8121
+ rerun: wholeLine(setupLineOf(install))
8156
8122
  };
8157
8123
  }
8158
- const SECTION_VERBS = {
8159
- create: "create",
8160
- add: "add the Agents section to",
8161
- rewrite: "rewrite the Agents section in"
8162
- };
8163
- function renderPlan(cli, plan) {
8164
- const files = [
8165
- ...plan.write.map((file) => ` write ${file}`),
8166
- ...plan.replace.map((file) => ` replace ${file}`),
8167
- ...plan.remove.map((file) => ` remove ${file}`)
8168
- ];
8169
- const { agentsMd } = plan;
8170
- return [
8171
- files.length === 0 ? `the ${cli.name} skill at ${plan.copy} already matches the package` : `would copy the ${cli.name} skill to ${plan.copy}:`,
8172
- ...files,
8173
- ...agentsMd === null ? [] : [agentsMd.change === null ? `${agentsMd.path} already holds the ${cli.name} Agents section` : `would ${SECTION_VERBS[agentsMd.change]} ${agentsMd.path}:\n${agentsMd.agents.text}`],
8174
- ...plan.handEdits.map((edit) => `by hand: ${edit}`)
8175
- ].join("\n");
8124
+ function needsInstall({ from, latest }) {
8125
+ return from === null || standingOf$1(from, latest) === "behind";
8176
8126
  }
8177
- function renderInstalled(cli, data) {
8178
- const { written, removed, agentsMd } = data;
8179
- return [
8180
- 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}`,
8181
- ...agentsMd === null ? [] : [agentsMd.changed ? `wrote the ${cli.name} Agents section in ${agentsMd.path}` : `${agentsMd.path} already holds the ${cli.name} Agents section`],
8182
- ...data.handEdits.map((edit) => `by hand: ${edit}`)
8183
- ].join("\n");
8127
+ function binDirOf(prefix) {
8128
+ return process.platform === "win32" ? prefix : join(prefix, "bin");
8184
8129
  }
8185
- function editedRemovals(plan, ctx) {
8186
- const them = plan.remove.length === 1 ? "it" : "them";
8187
- return new CliError({
8188
- code: OUTPUT_EXISTS,
8189
- message: `skill install would remove ${plan.remove.map((file) => join(plan.copy, file)).join(", ")}`,
8190
- hint: `once the user agrees to remove ${them}, run the Next step`,
8191
- next: [ctx.rerun({ add: [FORCE_FLAG.flag] })]
8130
+ async function canonical(path) {
8131
+ return normalize(await realpath(path).catch(() => path));
8132
+ }
8133
+ async function assertOnPath(cli, prefix, node, ctx) {
8134
+ const binDir = binDirOf(prefix);
8135
+ const wanted = await canonical(binDir);
8136
+ const entries = pathOf(ctx.env).split(delimiter).filter(Boolean);
8137
+ if ((await Promise.all(entries.map(canonical))).includes(wanted)) return;
8138
+ throw runtimeError({
8139
+ code: NOT_ON_PATH,
8140
+ message: `${cli.name} is installed in ${binDir}, by the npm of ${node ?? "the Node on PATH"}, a folder not on this shell's PATH`,
8141
+ hint: `add ${binDir} to PATH, or ask your user which Node their terminal runs and run this under it`,
8142
+ next: []
8192
8143
  });
8193
8144
  }
8194
- /** The built-in `skill install`, for a CLI whose skill ships in its package. */
8195
- function skillInstallCommand(cli, folder, agents) {
8145
+ const REFUSED = /* @__PURE__ */ new Set([
8146
+ PERMISSION_DENIED,
8147
+ DISK_FULL,
8148
+ INSTALL_FAILED
8149
+ ]);
8150
+ function withSetupLine(error, install) {
8151
+ if (!(error instanceof CliError) || !REFUSED.has(error.code)) return error;
8152
+ return runtimeError({
8153
+ code: error.code,
8154
+ message: error.message,
8155
+ hint: error.code === "INSTALL_FAILED" ? "fix what npm reports, then run the Next step; if a sandbox refused it, ask your user to type ! then that line" : error.hint,
8156
+ next: [wholeLine(setupLineOf(install))]
8157
+ }, { cause: error });
8158
+ }
8159
+ function renderSetup(cli, install, setup) {
8160
+ const { version, prefix, docs } = setup;
8161
+ return [
8162
+ setup.installed ? `installed ${install.package}@${version} with the npm under ${prefix}` : `${install.package}@${version} is already installed under ${prefix}`,
8163
+ ...renderHomeSync(cli.name, setup.skill),
8164
+ ...setup.upgrades.map(renderUpgrade),
8165
+ ...docs === null ? [] : [`docs ${docs}`, `Read that folder before using ${cli.name}.`]
8166
+ ].join("\n");
8167
+ }
8168
+ /** The built-in `setup`, which runs from any copy, such as npx's, so it never asks to be the global one. */
8169
+ function setupCommand(cli, install, { node }) {
8196
8170
  return defineCommand$1({
8197
- description: agents === void 0 ? "copy this CLI's skill into the project" : "copy this CLI's skill into the project and write its Agents section",
8171
+ description: "install this CLI and its skill on this machine",
8198
8172
  examples: [{
8199
- command: `${cli.name} skill install`,
8200
- note: "install the skill this version ships"
8173
+ command: `${cli.name} setup`,
8174
+ note: "install the latest version and the skill"
8201
8175
  }, {
8202
- command: `${cli.name} skill install --dry-run`,
8176
+ command: `${cli.name} setup --dry-run`,
8203
8177
  note: "show what it would change"
8204
8178
  }],
8205
8179
  effect: "write",
8206
- output: installSchema,
8207
- force: "replace or remove hand-edited files of a skill copy at the package's version",
8208
- plan: (_input, ctx) => planOf(cli, folder, agents, ctx.cwd, ctx.packageRoot),
8209
- outputs: (plan) => plan.sameVersion ? [...plan.write, ...plan.replace].map((file) => join(plan.copy, file)) : [],
8210
- apply: async (plan, ctx) => {
8211
- if (plan.sameVersion && plan.remove.length > 0 && !ctx.force) throw editedRemovals(plan, ctx);
8212
- for (const file of [...plan.write, ...plan.replace]) await ctx.files.write(join(plan.copy, file), await readFile(join(plan.source, file)));
8213
- for (const file of plan.remove) await ctx.files.remove(join(plan.copy, file));
8214
- const { agentsMd } = plan;
8215
- if (agentsMd !== null && agentsMd.change !== null) {
8216
- const existing = await orMissing(readFile(agentsMd.path, "utf8")) ?? "";
8217
- await ctx.files.write(agentsMd.path, withSection(agentsMd.agents, existing));
8218
- }
8180
+ output: setupSchema,
8181
+ plan: async (_input, ctx) => {
8182
+ const npm = npmOfSetup(cli, install, ctx, node);
8183
+ const prefix = await globalPrefix(npm);
8184
+ const packageDir = join(globalModulesDir(prefix), install.package);
8185
+ if ((await orMissing(lstat(packageDir)))?.isSymbolicLink() === true) throw runtimeError({
8186
+ code: NOT_GLOBAL_INSTALL,
8187
+ message: `${packageDir} is a link (npm link), which an install would replace`,
8188
+ hint: "remove the link with npm unlink, then run the Next step",
8189
+ next: [wholeLine(setupLineOf(install))]
8190
+ });
8219
8191
  return {
8220
- copy: plan.copy,
8221
- written: [...plan.write, ...plan.replace].sort(),
8222
- removed: [...plan.remove],
8223
- agentsMd: agentsMd === null ? null : {
8224
- path: agentsMd.path,
8225
- changed: agentsMd.change !== null
8226
- },
8227
- handEdits: [...plan.handEdits]
8192
+ package: install.package,
8193
+ from: await versionAt(packageDir),
8194
+ latest: await latestVersion(npm),
8195
+ prefix,
8196
+ packageDir,
8197
+ copies: cli.skill === void 0 ? [] : await homeCopiesOf(cli.name, ctx.env)
8228
8198
  };
8229
8199
  },
8230
- render: (data) => renderInstalled(cli, data),
8231
- renderPlan: (plan) => renderPlan(cli, plan)
8200
+ apply: async (plan, ctx) => {
8201
+ const { from, latest, prefix, packageDir } = plan;
8202
+ const installs = needsInstall(plan);
8203
+ const skill = await ctx.runToEnd(`installing ${plan.package}@${latest}`, async () => {
8204
+ if (installs) {
8205
+ ctx.progress(`installing ${plan.package}@${latest} with the npm under ${prefix}…`);
8206
+ await installGlobally(latest, npmOfSetup(cli, install, ctx, node));
8207
+ }
8208
+ return syncHome(cli, packageDir, ctx);
8209
+ }).catch((error) => {
8210
+ throw withSetupLine(error, install);
8211
+ });
8212
+ const setup = {
8213
+ version: installs || from === null ? latest : from,
8214
+ prefix,
8215
+ installed: installs,
8216
+ skill,
8217
+ upgrades: installs && from !== null ? await upgradesOf(packageDir, from, latest) : [],
8218
+ docs: cli.docs === void 0 ? null : join(packageDir, cli.docs)
8219
+ };
8220
+ ctx.deliver(setup);
8221
+ if (ctx.signal.aborted) throw new CliError({
8222
+ code: INTERRUPTED,
8223
+ message: `interrupted, but ${plan.package}@${setup.version} and its skill are already in place`,
8224
+ hint: "nothing to run again"
8225
+ });
8226
+ await assertOnPath(cli, prefix, node, ctx);
8227
+ return setup;
8228
+ },
8229
+ render: (setup) => renderSetup(cli, install, setup),
8230
+ renderPlan: (plan) => [needsInstall(plan) ? `would install ${plan.package}@${plan.latest} with the npm under ${plan.prefix}` : `${plan.package}@${plan.from} is already installed under ${plan.prefix}`, ...renderPlannedSync(cli.name, plan.copies)].join("\n")
8232
8231
  });
8233
8232
  }
8234
8233
  //#endregion
8235
- //#region ../agent-cli/dist/built-ins/skill/packaged.js
8236
- async function personalCopyOf(name, copy, ctx) {
8237
- const home = join(homeOf(ctx.env), ".claude", "skills", name);
8238
- const real = await orMissing(realpath(home));
8239
- return real === void 0 || real === await orMissing(realpath(copy)) ? void 0 : home;
8240
- }
8241
- /** What the `skill` row of a Packaged skill carries under `--json` beside its message. */
8242
- const packagedSkillDetailsSchema = object({
8243
- copy: string(),
8244
- copyVersion: string().nullable(),
8245
- packageVersion: string().nullable(),
8246
- /** A personal copy, which Claude Code prefers over the project's. */
8247
- home: string().optional()
8234
+ //#region ../agent-cli/dist/built-ins/update.js
8235
+ const updateSchema = object({
8236
+ from: string(),
8237
+ latest: string(),
8238
+ installed: object({
8239
+ version: string(),
8240
+ prefix: string()
8241
+ }).nullable(),
8242
+ skill: homeSyncSchema.nullable(),
8243
+ upgrades: array(upgradeSchema)
8248
8244
  });
8249
- function versionLabel(version) {
8250
- return version ?? "(no metadata.version)";
8251
- }
8252
- function compared(name, { copy, copyVersion, packageVersion }, same) {
8253
- const stated = `${copy} ${versionLabel(copyVersion)}`;
8254
- return same ? `${stated}, the version ${name} ships` : `${stated} differs from the ${versionLabel(packageVersion)} that ${name} ships`;
8255
- }
8256
- /** The project root's copy against the package's, and any under home. */
8257
- async function findingOf(cli, folder, install, ctx) {
8258
- const { copy } = await skillCopyOf(cli, ctx.cwd);
8259
- const versions = await skillVersionsOf(join(ctx.packageRoot, folder), copy);
8260
- const { packageVersion } = versions;
8261
- const copyVersion = versions.copyVersion ?? null;
8262
- const home = await personalCopyOf(cli.name, copy, ctx);
8263
- const skill = {
8264
- copy,
8265
- copyVersion,
8266
- packageVersion,
8267
- ...home === void 0 ? {} : { home }
8268
- };
8269
- const head = versions.copyVersion === void 0 ? `no copy of the ${cli.name} skill at ${copy}` : compared(cli.name, skill, versions.same);
8270
- const message = home === void 0 ? head : `${head}; remove ${home}: Claude Code prefers a personal skill over the project's`;
8271
- if (!versions.same) return {
8272
- state: "problem",
8273
- message,
8274
- fix: `${install.runAs} skill install`,
8275
- skill
8276
- };
8277
- return home === void 0 ? {
8278
- state: "ok",
8279
- message,
8280
- skill
8281
- } : {
8282
- state: "problem",
8283
- message,
8284
- skill
8285
- };
8286
- }
8287
- async function notice(cli, folder, install, { builtIn, bin }, ctx) {
8288
- if (builtIn || bin === void 0 || cli.commands === void 0) return;
8289
- let finding;
8290
- try {
8291
- finding = await findingOf(cli, folder, install, ctx);
8292
- } catch (error) {
8293
- if (errnoOf(error) === void 0) throw error;
8294
- ctx.notice(`could not check the ${cli.name} skill: ${messageOf(error)}`);
8295
- return;
8296
- }
8297
- if (finding.state !== "problem") return;
8298
- ctx.notice(finding.fix === void 0 ? finding.message : `${finding.message}; run \`${finding.fix}\``);
8299
- }
8300
- /** A skill shipped in the package at `packaged`, which `skill install` copies to the project root. */
8301
- function packagedSkill(cli, { packaged: folder, agents: text }, install) {
8302
- const agents = text === void 0 ? void 0 : {
8303
- name: cli.name,
8304
- text
8305
- };
8306
- return {
8307
- kind: "packaged",
8308
- builtIns: { skill: { install: skillInstallCommand(cli, folder, agents) } },
8309
- rows: [{
8310
- name: "skill",
8311
- advisory: true,
8312
- run: (ctx) => findingOf(cli, folder, install, ctx)
8313
- }, ...agents === void 0 ? [] : [agentsRow(agents, install)]],
8314
- notice: (launch, ctx) => notice(cli, folder, install, launch, ctx),
8315
- differing: async ({ cwd, packageRoot }) => {
8316
- const { copy } = await skillCopyOf(cli, cwd);
8317
- const { copyVersion, packageVersion, same } = await skillVersionsOf(join(packageRoot, folder), copy);
8318
- return copyVersion === void 0 || same ? [] : [compared(cli.name, {
8319
- copy,
8320
- copyVersion,
8321
- packageVersion
8322
- }, false)];
8323
- },
8324
- nextAt: async (_version, ctx) => {
8325
- const { fix } = await findingOf(cli, folder, install, ctx);
8326
- return fix === void 0 ? [] : [fix];
8327
- }
8328
- };
8245
+ function nothingToInstall({ from, latest }) {
8246
+ 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`;
8329
8247
  }
8330
- //#endregion
8331
- //#region ../agent-cli/dist/built-ins/skill/skill.js
8332
- /** A Packaged skill, or else a Skill Hub's, also for a CLI that declares no `skill`. */
8333
- function skillOf$1(cli, install) {
8334
- const { skill } = cli;
8335
- return skill !== void 0 && "packaged" in skill ? packagedSkill(cli, skill, install) : hubSkill(cli, skill);
8248
+ function renderUpdate(cli, install, update) {
8249
+ const { installed } = update;
8250
+ const skill = renderHomeSync(cli.name, update.skill);
8251
+ return installed === null ? [nothingToInstall(update), ...skill].join("\n") : [
8252
+ `installed ${install.package}@${installed.version} with the npm under ${installed.prefix}, replacing ${update.from}`,
8253
+ ...skill,
8254
+ ...update.upgrades.map(renderUpgrade)
8255
+ ].join("\n");
8336
8256
  }
8337
- //#endregion
8338
- //#region ../agent-cli/dist/context/interrupts.js
8339
- const INTERRUPTED = "INTERRUPTED";
8340
- const DEADLINE_EXCEEDED = "DEADLINE_EXCEEDED";
8341
- /** How long an interrupted Command may take to settle before the run ends without it. */
8342
- const GRACE_MS = 3e3;
8343
- const GRACE_OVER = Symbol("grace over");
8344
- function interruptedError(rerun) {
8257
+ function interruptedAfterInstall(plan) {
8345
8258
  return new CliError({
8346
8259
  code: INTERRUPTED,
8347
- message: "interrupted before it finished",
8348
- hint: "to finish it, run the Next step, unless it was stopped on purpose; if a tool timeout stopped it, give it longer",
8349
- next: [rerun]
8350
- });
8351
- }
8352
- const BOUND_WORDS = {
8353
- deadline: "deadline",
8354
- ignoreStop: "timeout"
8355
- };
8356
- function waitHint(bound) {
8357
- return bound === "ignoreStop" ? "the call may have been billed: ask your user before running the Next step" : "run the Next step later, when the service it waits on may answer faster";
8358
- }
8359
- function deadlineError(commandLine, ms, advice, bound = "deadline") {
8360
- return new CliError({
8361
- code: DEADLINE_EXCEEDED,
8362
- message: `${commandLine} ran past its ${ms} ms ${BOUND_WORDS[bound]}`,
8363
- ...advice
8260
+ message: `interrupted, but ${plan.package}@${plan.latest} had already been installed with the npm under ${plan.prefix}, replacing ${plan.from}`,
8261
+ hint: "nothing to run again"
8364
8262
  });
8365
8263
  }
8366
- /** One Command run's cancellation: its signal, its deadlines and the steps it must finish. */
8367
- var RunControl = class {
8368
- signal;
8369
- #controller = new AbortController();
8370
- #commandLine;
8371
- #rerun;
8372
- #outer;
8373
- #onOuterAbort = () => this.#controller.abort(interruptedError(this.#rerun));
8374
- /** Each running must-finish step, tracked until it settles, with its name. */
8375
- #steps = /* @__PURE__ */ new Map();
8376
- #progress;
8377
- #clock;
8378
- #disposed = new AbortController();
8379
- /** The reasons this run's deadlines aborted with, to tell them in a Command's error. */
8380
- #abortReasons = /* @__PURE__ */ new WeakSet();
8381
- #graceCancel = new AbortController();
8382
- #endGrace = () => {};
8383
- #graceOver = new Promise((resolve) => {
8384
- this.#endGrace = resolve;
8385
- });
8386
- constructor({ commandLine, rerun, outer, clock, progress, runDeadline }) {
8387
- this.#commandLine = commandLine;
8388
- this.#rerun = rerun;
8389
- this.#outer = outer;
8390
- this.#clock = clock;
8391
- this.#progress = progress;
8392
- this.signal = this.#controller.signal;
8393
- this.signal.addEventListener("abort", () => this.#onStop(), { once: true });
8394
- if (outer?.aborted === true) this.#onOuterAbort();
8395
- else outer?.addEventListener("abort", this.#onOuterAbort, { once: true });
8396
- if (runDeadline !== void 0) {
8397
- const { ms, advice } = runDeadline;
8398
- after(clock, ms, this.#disposed.signal, () => this.#controller.abort(deadlineError(commandLine, ms, advice())));
8399
- }
8400
- }
8401
- deadline(ms, options = {}) {
8402
- return options.ignoreStop === true ? this.#abortsAfter(ms, "ignoreStop") : AbortSignal.any([this.signal, this.#abortsAfter(ms, "deadline")]);
8403
- }
8404
- #abortsAfter(ms, bound) {
8405
- const controller = new AbortController();
8406
- after(this.#clock, ms, this.#disposed.signal, () => {
8407
- const advice = {
8408
- hint: waitHint(bound),
8409
- next: [this.#rerun]
8264
+ /** The built-in `update`, for a CLI installed globally with npm. */
8265
+ function updateCommand(cli, install, { bin, node }) {
8266
+ return defineCommand$1({
8267
+ description: "install the latest version of this CLI from its registry",
8268
+ examples: [{
8269
+ command: `${cli.name} update`,
8270
+ note: "install the latest version and show what changed"
8271
+ }, {
8272
+ command: `${cli.name} update --dry-run`,
8273
+ note: "show the version it would install"
8274
+ }],
8275
+ effect: "write",
8276
+ output: updateSchema,
8277
+ plan: async (_input, ctx) => {
8278
+ const npm = globalNpm(cli, install, ctx, node);
8279
+ const copy = await globalInstallOf(bin, npm);
8280
+ return {
8281
+ package: install.package,
8282
+ from: cli.version,
8283
+ latest: await latestVersion(npm),
8284
+ prefix: copy.prefix,
8285
+ packageDir: copy.packageDir,
8286
+ copies: cli.skill === void 0 ? [] : await homeCopiesOf(cli.name, ctx.env)
8410
8287
  };
8411
- const reason = deadlineError(this.#commandLine, ms, advice, bound);
8412
- this.#abortReasons.add(reason);
8413
- controller.abort(reason);
8414
- });
8415
- return controller.signal;
8416
- }
8417
- async runToEnd(name, work) {
8418
- this.#graceCancel.abort();
8419
- if (this.signal.aborted) this.#finishing(name);
8420
- const step = work();
8421
- const settled = step.then(() => {}, () => {});
8422
- this.#steps.set(settled, name);
8423
- try {
8424
- return await step;
8425
- } finally {
8426
- this.#steps.delete(settled);
8427
- this.#startGrace();
8428
- }
8288
+ },
8289
+ apply: async (plan, ctx) => {
8290
+ const { from, latest, prefix } = plan;
8291
+ if (standingOf$1(from, latest) !== "behind") return {
8292
+ from,
8293
+ latest,
8294
+ installed: null,
8295
+ skill: await syncHome(cli, plan.packageDir, ctx),
8296
+ upgrades: []
8297
+ };
8298
+ ctx.signal.throwIfAborted();
8299
+ ctx.progress(`installing ${plan.package}@${latest} with the npm under ${prefix}…`);
8300
+ const skill = await ctx.runToEnd(`installing ${plan.package}@${latest}`, async () => {
8301
+ await installGlobally(latest, globalNpm(cli, install, ctx, node));
8302
+ return syncHome(cli, plan.packageDir, ctx);
8303
+ });
8304
+ const update = {
8305
+ from,
8306
+ latest,
8307
+ installed: {
8308
+ version: latest,
8309
+ prefix
8310
+ },
8311
+ skill,
8312
+ upgrades: await upgradesOf(plan.packageDir, from, latest)
8313
+ };
8314
+ if (ctx.signal.aborted) {
8315
+ ctx.deliver(update);
8316
+ throw interruptedAfterInstall(plan);
8317
+ }
8318
+ return update;
8319
+ },
8320
+ render: (update) => renderUpdate(cli, install, update),
8321
+ 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), ...renderPlannedSync(cli.name, plan.copies)].join("\n")
8322
+ });
8323
+ }
8324
+ //#endregion
8325
+ //#region ../agent-cli/dist/context/files.js
8326
+ const TEMP_FILE = /^\..+\.agent-cli-(\d+)\.tmp$/;
8327
+ /** Where the Runtime writes `path` before moving it into place; a dead writer's is swept. */
8328
+ function tempPathOf(path) {
8329
+ return join(dirname(path), `.${basename(path)}.agent-cli-${process.pid}.tmp`);
8330
+ }
8331
+ function isAlive(pid) {
8332
+ try {
8333
+ process.kill(pid, 0);
8334
+ return true;
8335
+ } catch (error) {
8336
+ return errnoOf(error) === "EPERM";
8429
8337
  }
8430
- /** Waits for the Command, its must-finish steps, and no longer than the grace once interrupted. */
8431
- async settle(work) {
8432
- try {
8433
- const value = await Promise.race([work, this.#graceOver]);
8434
- if (this.signal.aborted || value === GRACE_OVER) throw this.signal.reason;
8435
- return value;
8436
- } finally {
8437
- await this.#stepsDone();
8438
- this.close();
8439
- }
8338
+ }
8339
+ async function sweep(folder) {
8340
+ const dead = (await readdir(folder)).filter((name) => {
8341
+ const pid = TEMP_FILE.exec(name)?.[1];
8342
+ return pid !== void 0 && !isAlive(Number(pid));
8343
+ });
8344
+ await Promise.all(dead.map((name) => rm(join(folder, name), { force: true })));
8345
+ }
8346
+ /** The file a symlink at `path` points to, so a replacing write keeps the link. */
8347
+ async function targetOf$1(path) {
8348
+ try {
8349
+ return await realpath(path);
8350
+ } catch (error) {
8351
+ if (isMissingFile(error)) return path;
8352
+ throw error;
8440
8353
  }
8441
- /**
8442
- * The error a run ends with: an interrupt or the run deadline outranks what the Command threw,
8443
- * unless the Command threw its own `INTERRUPTED`, saying what it finished before it stopped.
8444
- * Otherwise each leaf that is not a `CliError` and whose cause chain holds a deadline's abort is
8445
- * reported as that abort, so a declared error keeps its own code.
8446
- */
8447
- errorAfter(error) {
8448
- if (this.signal.aborted) return error instanceof CliError && error.code === "INTERRUPTED" ? error : this.signal.reason;
8449
- const leaves = errorsOf(error).map((leaf) => this.#abortIn(leaf) ?? leaf);
8450
- return leaves.length === 1 ? leaves[0] : new AggregateError(leaves);
8354
+ }
8355
+ async function writeWithMode(path, data, mode) {
8356
+ if (mode === void 0) {
8357
+ await writeFile(path, data);
8358
+ return;
8451
8359
  }
8452
- #abortIn(error) {
8453
- if (error instanceof CliError) return void 0;
8454
- return causesOf(error).find((each) => each instanceof CliError && this.#abortReasons.has(each));
8360
+ await writeFile(path, data, { mode });
8361
+ await chmod(path, mode);
8362
+ }
8363
+ /** What `link` answers on a file system without hard links, such as exFAT. */
8364
+ const NO_HARD_LINKS = /* @__PURE__ */ new Set([
8365
+ "EPERM",
8366
+ "ENOTSUP",
8367
+ "EXDEV"
8368
+ ]);
8369
+ /** A link fails atomically when `path` is taken, so no reader ever sees half a file there. */
8370
+ async function linkNew(temp, path, data, mode) {
8371
+ try {
8372
+ await link(temp, path);
8373
+ } catch (error) {
8374
+ if (!NO_HARD_LINKS.has(errnoOf(error))) throw error;
8375
+ await createNew(path, data, mode);
8455
8376
  }
8456
- #onStop() {
8457
- for (const name of this.#steps.values()) this.#finishing(name);
8458
- this.#startGrace();
8377
+ }
8378
+ /** A file this call created and could not finish is removed, so a rerun does not find half of it. */
8379
+ async function createNew(path, data, mode) {
8380
+ const handle = await open(path, "wx", mode);
8381
+ try {
8382
+ await handle.writeFile(data);
8383
+ if (mode !== void 0) await handle.chmod(mode);
8384
+ } catch (error) {
8385
+ await handle.close();
8386
+ await rm(path, { force: true });
8387
+ throw error;
8459
8388
  }
8460
- #finishing(name) {
8461
- this.#progress(`finishing ${name}`);
8389
+ await handle.close();
8390
+ }
8391
+ const RENAME_ATTEMPTS = 10;
8392
+ const RENAME_BACKOFF_MS = 10;
8393
+ const HELD_BY_ANOTHER_PROGRAM = /* @__PURE__ */ new Set(["EPERM", "EBUSY"]);
8394
+ async function renameRetrying(from, to) {
8395
+ for (let attempt = 1;; attempt++) try {
8396
+ await rename(from, to);
8397
+ return;
8398
+ } catch (error) {
8399
+ if (!(process.platform === "win32" && HELD_BY_ANOTHER_PROGRAM.has(errnoOf(error))) || attempt === RENAME_ATTEMPTS) throw error;
8400
+ await setTimeout$1(attempt * RENAME_BACKOFF_MS);
8462
8401
  }
8463
- #startGrace() {
8464
- if (this.signal.aborted && this.#steps.size === 0) {
8465
- this.#graceCancel.abort();
8466
- this.#graceCancel = new AbortController();
8467
- after(this.#clock, GRACE_MS, this.#graceCancel.signal, () => this.#endGrace(GRACE_OVER));
8402
+ }
8403
+ /** Real files for one run: the first write to each folder sweeps the temp files of dead writers. */
8404
+ function realFiles() {
8405
+ const swept = /* @__PURE__ */ new Set();
8406
+ return {
8407
+ exists: async (path) => {
8408
+ try {
8409
+ await access(path);
8410
+ return true;
8411
+ } catch (error) {
8412
+ if (isNothingThere(error)) return false;
8413
+ throw error;
8414
+ }
8415
+ },
8416
+ write: async (path, data, options = {}) => {
8417
+ await mkdir(dirname(path), { recursive: true });
8418
+ const target = options.exclusive === true ? path : await targetOf$1(path);
8419
+ const folder = dirname(target);
8420
+ if (!swept.has(folder)) {
8421
+ await sweep(folder);
8422
+ swept.add(folder);
8423
+ }
8424
+ const temp = tempPathOf(target);
8425
+ try {
8426
+ await writeWithMode(temp, data, options.mode);
8427
+ if (options.exclusive === true) {
8428
+ await linkNew(temp, target, data, options.mode);
8429
+ await rm(temp);
8430
+ } else await renameRetrying(temp, target);
8431
+ } catch (error) {
8432
+ await rm(temp, { force: true });
8433
+ throw error;
8434
+ }
8435
+ },
8436
+ read: (path) => readFile(path),
8437
+ list: (folder) => readdir(folder),
8438
+ remove: (path) => rm(path, {
8439
+ recursive: true,
8440
+ force: true
8441
+ }),
8442
+ mkdir: async (path, options) => {
8443
+ const mode = options?.mode;
8444
+ if (options?.exclusive !== true) {
8445
+ await mkdir(path, {
8446
+ recursive: true,
8447
+ mode
8448
+ });
8449
+ return;
8450
+ }
8451
+ await mkdir(dirname(path), {
8452
+ recursive: true,
8453
+ mode
8454
+ });
8455
+ await mkdir(path, { mode });
8456
+ },
8457
+ stat: async (path) => {
8458
+ try {
8459
+ const { size, dev, ino } = await stat(path, { bigint: true });
8460
+ return {
8461
+ size: Number(size),
8462
+ id: `${dev}:${ino}`
8463
+ };
8464
+ } catch (error) {
8465
+ if (isNothingThere(error)) return void 0;
8466
+ throw error;
8467
+ }
8468
+ }
8469
+ };
8470
+ }
8471
+ /**
8472
+ * `files` resolving paths against `cwd`, its failures classified by errno; `list` drops the
8473
+ * Runtime's temp files and sorts.
8474
+ */
8475
+ function classifiedFiles(files, { cwd, fileError }) {
8476
+ async function at(access, path, work) {
8477
+ const target = resolve(cwd, path);
8478
+ try {
8479
+ return await work(target);
8480
+ } catch (error) {
8481
+ throw fileError(error, {
8482
+ path: target,
8483
+ access
8484
+ });
8468
8485
  }
8469
8486
  }
8470
- async #stepsDone() {
8471
- while (this.#steps.size > 0) await Promise.all(this.#steps.keys());
8472
- }
8473
- /** Stops the run's timers and its hold on the caller's signal, for a run that never settles. */
8474
- close() {
8475
- this.#graceCancel.abort();
8476
- this.#disposed.abort();
8477
- this.#outer?.removeEventListener("abort", this.#onOuterAbort);
8478
- }
8479
- };
8487
+ return {
8488
+ exists: (path) => at("read", path, (target) => files.exists(target)),
8489
+ write: (path, data, options) => at("write", path, (target) => files.write(target, data, options)),
8490
+ read: (path) => at("read", path, (target) => files.read(target)),
8491
+ list: (folder) => at("read", folder, async (target) => (await files.list(target)).filter((name) => !TEMP_FILE.test(name)).sort()),
8492
+ remove: (path) => at("write", path, (target) => files.remove(target)),
8493
+ mkdir: (path, options) => at("write", path, (target) => files.mkdir(target, options)),
8494
+ stat: (path) => at("read", path, (target) => files.stat(target))
8495
+ };
8496
+ }
8480
8497
  //#endregion
8481
- //#region ../agent-cli/dist/built-ins/install/global-npm.js
8482
- const NOT_GLOBAL_INSTALL = "NOT_GLOBAL_INSTALL";
8483
- /** Where npm puts global packages under its global prefix. */
8484
- function globalModulesDir(prefix) {
8485
- return process.platform === "win32" ? join(prefix, "node_modules") : join(prefix, "lib", "node_modules");
8498
+ //#region ../agent-cli/dist/built-ins/install/release-check.js
8499
+ const CHECK_INTERVAL_MS = 864e5;
8500
+ const checkSchema = object({
8501
+ checkedAt: number(),
8502
+ latest: string().optional(),
8503
+ error: string().optional()
8504
+ });
8505
+ const CHECK_SCRIPT = `
8506
+ const { spawnSync } = require("node:child_process");
8507
+ const { mkdtempSync, renameSync, rmSync, writeFileSync } = require("node:fs");
8508
+ const { tmpdir } = require("node:os");
8509
+ const { join } = require("node:path");
8510
+ const [file, checkedAt, previous, npm, ...args] = process.argv.slice(1);
8511
+ const windows = process.platform === "win32";
8512
+ const cache = mkdtempSync(join(tmpdir(), "release-check-"));
8513
+ const write = (check) => {
8514
+ const temp = file + "." + process.pid + ".tmp";
8515
+ writeFileSync(temp, JSON.stringify({ checkedAt: Number(checkedAt), ...check }));
8516
+ renameSync(temp, file);
8517
+ };
8518
+ const latestOf = (stdout) => {
8519
+ try {
8520
+ const parsed = JSON.parse(stdout);
8521
+ const latest = Array.isArray(parsed) ? parsed.at(-1) : parsed;
8522
+ return typeof latest === "string" && /^\\d+\\.\\d+\\.\\d+/.test(latest) ? latest : undefined;
8523
+ } catch {
8524
+ return undefined;
8525
+ }
8526
+ };
8527
+ try {
8528
+ const answer = spawnSync(windows ? '"' + npm + '"' : npm, args, {
8529
+ encoding: "utf8",
8530
+ shell: windows,
8531
+ windowsHide: true,
8532
+ env: { ...process.env, npm_config_cache: join(cache, "cache"), npm_config_logs_max: "0" },
8533
+ });
8534
+ const latest = answer.status === 0 ? latestOf(answer.stdout) : undefined;
8535
+ if (latest !== undefined) {
8536
+ write({ latest });
8537
+ } else {
8538
+ const said = (answer.stderr || "").trim().split(/\\r?\\n/).slice(0, 5).join("; ");
8539
+ const error = answer.error ? answer.error.message : said || "npm answered " + answer.stdout.trim();
8540
+ write({ latest: previous || undefined, error });
8541
+ }
8542
+ } finally {
8543
+ rmSync(cache, { recursive: true, force: true });
8544
+ }
8545
+ `;
8546
+ function isOff(cliName, env) {
8547
+ return Boolean(env[cliVariable(cliName, "NO_RELEASE_CHECK")] || env.CI);
8548
+ }
8549
+ function cacheFileOf(cliName, env) {
8550
+ const cache = env.XDG_CACHE_HOME || join(env.HOME || env.USERPROFILE || homedir(), ".cache");
8551
+ return join(cache, cliName, "release-check.json");
8552
+ }
8553
+ function parsedCheck(text) {
8554
+ try {
8555
+ return checkSchema.safeParse(JSON.parse(text)).data;
8556
+ } catch {
8557
+ return;
8558
+ }
8486
8559
  }
8487
- function notGlobal(ctx, message) {
8488
- return runtimeError({
8489
- code: NOT_GLOBAL_INSTALL,
8490
- message,
8491
- hint: "install it with npm by running the Next step, so its update Command can replace it",
8492
- next: [wholeLine(installCommandLine(ctx.registry))]
8493
- });
8560
+ async function lastCheck(file) {
8561
+ const text = await orMissing(readFile(file, "utf8"));
8562
+ return text === void 0 ? void 0 : parsedCheck(text);
8563
+ }
8564
+ async function writeCheck(file, check) {
8565
+ await mkdir(dirname(file), { recursive: true });
8566
+ const temp = tempPathOf(file);
8567
+ await writeFile(temp, JSON.stringify(check));
8568
+ await rename(temp, file);
8569
+ }
8570
+ async function startCheck(cli, install, file, last, { node }, ctx) {
8571
+ const checkedAt = ctx.clock.now();
8572
+ const previous = last?.latest ?? "";
8573
+ await writeCheck(file, {
8574
+ checkedAt,
8575
+ latest: last?.latest
8576
+ });
8577
+ const npm = globalNpm(cli, install, ctx, node);
8578
+ const { path, env } = npmOf(npm, process.platform === "win32");
8579
+ await ctx.spawnDetached(process.execPath, [
8580
+ "-e",
8581
+ CHECK_SCRIPT,
8582
+ file,
8583
+ String(checkedAt),
8584
+ previous,
8585
+ path,
8586
+ ...latestViewArgs(npm.registry)
8587
+ ], { env });
8588
+ }
8589
+ function debugLine(cli, ctx, text) {
8590
+ if (isDebug({
8591
+ env: ctx.env,
8592
+ cliName: cli.name
8593
+ })) ctx.progress(`release check failed: ${text}`);
8494
8594
  }
8495
8595
  /**
8496
- * The global npm install this copy runs from, found by asking npm for its global prefix.
8497
- * `bin` is the script that started the CLI; throws `NOT_GLOBAL_INSTALL` when it is not inside that install.
8596
+ * Tells the caller of a newer release that an earlier Release check found, and starts the next
8597
+ * check once the last is a day old.
8498
8598
  */
8499
- async function globalInstallOf(bin, ctx) {
8500
- if (bin === void 0) throw notGlobal(ctx, `${ctx.cliName} runs from source, not from an npm install`);
8501
- const prefix = await globalPrefix(ctx);
8502
- const packageDir = join(globalModulesDir(prefix), ctx.registry.package);
8503
- const installed = await orMissing(realpath(packageDir));
8504
- const here = await orMissing(realpath(bin)) ?? bin;
8505
- if (installed === void 0 || !isInside(installed, here)) throw notGlobal(ctx, `${ctx.cliName} runs from ${here}, not from npm's global install at ${packageDir}`);
8506
- if ((await lstat(packageDir)).isSymbolicLink()) throw notGlobal(ctx, `${ctx.cliName} runs from a link to ${installed} (npm link), not from an npm install`);
8599
+ async function releaseCheck(cli, install, runAs, launch, ctx) {
8600
+ if (launch.builtIn || launch.bin === void 0 || cli.commands === void 0) return;
8601
+ if (isOff(cli.name, ctx.env)) return;
8602
+ try {
8603
+ await checkAndTell(cli, install, runAs, launch, ctx);
8604
+ } catch (error) {
8605
+ if (errnoOf(error) === void 0 && !(error instanceof CliError)) throw error;
8606
+ debugLine(cli, ctx, messageOf(error));
8607
+ }
8608
+ }
8609
+ async function checkAndTell(cli, install, runAs, launch, ctx) {
8610
+ const file = cacheFileOf(cli.name, ctx.env);
8611
+ const last = await lastCheck(file);
8612
+ if (last?.error !== void 0) debugLine(cli, ctx, last.error);
8613
+ if (last === void 0 || ctx.clock.now() - last.checkedAt >= CHECK_INTERVAL_MS) await startCheck(cli, install, file, last, launch, ctx);
8614
+ const latest = last?.latest;
8615
+ if (latest === void 0 || !isVersion(latest) || compareVersions(latest, cli.version) <= 0) return;
8616
+ ctx.notice(`${cli.name} ${latest} is out, this is ${cli.version}; run \`${runAs} update\``);
8617
+ }
8618
+ //#endregion
8619
+ //#region ../agent-cli/dist/built-ins/install/global.js
8620
+ function checkOf(cli, install, { bin, node }) {
8507
8621
  return {
8508
- prefix,
8509
- packageDir
8622
+ install: async (ctx) => {
8623
+ const npm = globalNpm(cli, install, ctx, node);
8624
+ try {
8625
+ return {
8626
+ state: "ok",
8627
+ message: `a global npm install under ${(await globalInstallOf(bin, npm)).prefix}`
8628
+ };
8629
+ } catch (error) {
8630
+ if (error instanceof CliError && error.code === "NOT_GLOBAL_INSTALL") return {
8631
+ state: "problem",
8632
+ message: error.message,
8633
+ fix: setupLineOf(install)
8634
+ };
8635
+ throw error;
8636
+ }
8637
+ },
8638
+ release: async (ctx) => {
8639
+ const fix = `${cli.name} update`;
8640
+ return {
8641
+ npm: globalNpm(cli, install, ctx, node),
8642
+ advise: () => ({ fix })
8643
+ };
8644
+ }
8510
8645
  };
8511
8646
  }
8512
- const NPMJS = "https://registry.npmjs.org/";
8513
- /** The flag that points npm at the CLI's registry; none for npmjs, npm's default. */
8514
- function registryFlags({ package: name, registry }) {
8515
- if (registry === NPMJS) return [];
8516
- const scope = scopeOf(name);
8517
- return [scope === void 0 ? `--registry=${registry}` : `--${scope}:registry=${registry}`];
8647
+ /** The Install of a CLI installed once per machine with npm, which `setup` puts there and `update` replaces. */
8648
+ function globalInstall(cli, install) {
8649
+ const adapter = {
8650
+ runAs: runAsOf(cli),
8651
+ builtIns: (launch) => ({
8652
+ setup: setupCommand(cli, install, launch),
8653
+ update: updateCommand(cli, install, launch)
8654
+ }),
8655
+ exitClasses: ["source"],
8656
+ check: (launch) => checkOf(cli, install, launch),
8657
+ notice: (launch, ctx) => releaseCheck(cli, install, adapter.runAs, launch, ctx)
8658
+ };
8659
+ return adapter;
8518
8660
  }
8519
- function globalRegistry(install) {
8520
- const { package: name, registry } = install;
8661
+ //#endregion
8662
+ //#region ../agent-cli/dist/built-ins/install/install.js
8663
+ const noNotice = async () => {};
8664
+ function noInstall(cli) {
8665
+ const skipped = async () => ({
8666
+ state: "skipped",
8667
+ message: `not checked: ${cli.name} declares no install`
8668
+ });
8521
8669
  return {
8522
- package: name,
8523
- label: registry,
8524
- flags: registryFlags(install),
8525
- unreachableHint: "check the network or VPN",
8526
- loginHint: `log in to ${registry} (\`npm login --registry=${registry}\`) or set its token in ~/.npmrc`,
8527
- missingHint: `check that ${name} is published on ${registry}`
8670
+ runAs: runAsOf(cli),
8671
+ builtIns: () => ({}),
8672
+ exitClasses: [],
8673
+ check: () => ({
8674
+ install: skipped,
8675
+ release: async () => ({ skipped: await skipped() })
8676
+ }),
8677
+ notice: noNotice
8528
8678
  };
8529
8679
  }
8530
- /** npm for a Global install, run in the working directory. */
8531
- function globalNpm(cli, install, ctx, node) {
8532
- return npmContext(cli, globalRegistry(install), ctx, node, ctx.cwd);
8680
+ function installOf(cli) {
8681
+ const { install } = cli;
8682
+ if (install === void 0) return noInstall(cli);
8683
+ return install.mode === "global" ? globalInstall(cli, install) : dependencyInstall(cli, install);
8533
8684
  }
8534
8685
  //#endregion
8535
- //#region ../agent-cli/dist/built-ins/upgrades.js
8536
- /** A skipped release's Upgrade steps, or its whole section when it lists none. */
8537
- const upgradeSchema = union([object({
8538
- version: string(),
8539
- steps: array(string())
8540
- }), object({
8541
- version: string(),
8542
- notes: string()
8543
- })]);
8544
- const RELEASE_HEADING = /^## v?(\S+)/;
8545
- const UPGRADE_HEADING = /^### Upgrade from v\S+$/;
8546
- const HEADING = /^#{1,6} /;
8547
- const ITEM = /^[-*] /;
8548
- const NOTHING_TO_DO = /^nothing to do\.?$/i;
8549
- function releasesIn(changelog) {
8550
- const releases = [];
8551
- let current;
8552
- for (const line of changelog.split(/\r?\n/)) {
8553
- const heading = RELEASE_HEADING.exec(line);
8554
- if (heading !== null) {
8555
- const version = heading[1] ?? "";
8556
- current = isVersion(version) ? {
8557
- version,
8558
- lines: []
8559
- } : void 0;
8560
- if (current !== void 0) releases.push(current);
8561
- } else current?.lines.push(line);
8562
- }
8563
- return releases;
8686
+ //#region ../agent-cli/dist/built-ins/agents-md.js
8687
+ function markersOf(name) {
8688
+ return {
8689
+ begin: `<!-- BEGIN:${name} -->`,
8690
+ end: `<!-- END:${name} -->`
8691
+ };
8564
8692
  }
8565
- function listedSteps(lines) {
8566
- const start = lines.findIndex((line) => UPGRADE_HEADING.test(line.trimEnd()));
8567
- if (start === -1) return [];
8568
- const end = lines.findIndex((line, i) => i > start && HEADING.test(line));
8569
- const body = lines.slice(start + 1, end === -1 ? void 0 : end).filter((line) => line.trim() !== "");
8570
- const starts = body.flatMap((line, i) => ITEM.test(line) ? [i] : []);
8571
- return starts.map((first, i) => body.slice(first, starts[i + 1]).map((line) => line.trim()).join(" ").replace(ITEM, ""));
8693
+ /** `text` with the CLI's Agents section between its markers, appended when they are missing; nothing else changes. */
8694
+ function withSection({ name, text: section }, text) {
8695
+ const { begin, end } = markersOf(name);
8696
+ const whole = `${begin}\n${section}\n${end}`;
8697
+ const start = text.indexOf(begin);
8698
+ const stop = start === -1 ? -1 : text.indexOf(end, start);
8699
+ if (stop !== -1) return `${text.slice(0, start)}${whole}${text.slice(stop + end.length)}`;
8700
+ if (text === "") return `${whole}\n`;
8701
+ return `${text}${text.endsWith("\n") ? "" : "\n"}\n${whole}\n`;
8572
8702
  }
8573
- function upgradeOf({ version, lines }) {
8574
- const listed = listedSteps(lines);
8575
- if (listed.length === 0) return [{
8576
- version,
8577
- notes: lines.join("\n").trim()
8578
- }];
8579
- const todo = listed.filter((step) => !NOTHING_TO_DO.test(step));
8580
- return todo.length === 0 ? [] : [{
8581
- version,
8582
- steps: todo
8583
- }];
8703
+ async function agentsMdAt(agents, root) {
8704
+ const path = join(root, "AGENTS.md");
8705
+ const existing = await orMissing(readFile(path, "utf8"));
8706
+ if (existing === void 0) return {
8707
+ path,
8708
+ agents,
8709
+ change: "create"
8710
+ };
8711
+ if (withSection(agents, existing) === existing) return {
8712
+ path,
8713
+ agents,
8714
+ change: null
8715
+ };
8716
+ return {
8717
+ path,
8718
+ agents,
8719
+ change: existing.includes(markersOf(agents.name).begin) ? "rewrite" : "add"
8720
+ };
8584
8721
  }
8585
- /**
8586
- * What each release after `from` up to `to` asks of the operator, oldest first, from the
8587
- * CHANGELOG.md in `packageDir`; none when the package ships no CHANGELOG.
8588
- */
8589
- async function upgradesOf(packageDir, from, to) {
8590
- let changelog;
8591
- try {
8592
- changelog = await readFile(join(packageDir, "CHANGELOG.md"), "utf8");
8593
- } catch (error) {
8594
- if (isMissingFile(error)) return [];
8595
- throw error;
8596
- }
8597
- return releasesIn(changelog).filter(({ version }) => compareVersions(version, from) > 0 && compareVersions(version, to) <= 0).sort((a, b) => compareVersions(a.version, b.version)).flatMap(upgradeOf);
8722
+ async function handEditsAt(root) {
8723
+ const claudeMd = join(root, "CLAUDE.md");
8724
+ const text = await orMissing(readFile(claudeMd, "utf8"));
8725
+ if (text === void 0 || text.split(/\r?\n/).some((line) => line.trim() === "@AGENTS.md")) return [];
8726
+ return [`add a line \`@AGENTS.md\` to ${claudeMd}, so Claude Code reads AGENTS.md`];
8727
+ }
8728
+ function agentsRow(agents, install) {
8729
+ const { name } = agents;
8730
+ return {
8731
+ name: "agents",
8732
+ advisory: true,
8733
+ run: async (ctx) => {
8734
+ const root = await projectRootOf(ctx.cwd);
8735
+ const { path, change } = await agentsMdAt(agents, root);
8736
+ const fix = `${install.runAs} skill install`;
8737
+ if (change === "rewrite") return {
8738
+ state: "problem",
8739
+ message: `${path} holds a ${name} Agents section other than the one ${fix} writes`,
8740
+ fix
8741
+ };
8742
+ if (change !== null) return {
8743
+ state: "problem",
8744
+ message: `${path} holds no ${name} Agents section, so sessions in this repo do not know ${name}`,
8745
+ fix
8746
+ };
8747
+ const holds = `${path} holds the ${name} Agents section`;
8748
+ const [edit] = await handEditsAt(root);
8749
+ return edit === void 0 ? {
8750
+ state: "ok",
8751
+ message: holds
8752
+ } : {
8753
+ state: "problem",
8754
+ message: `${holds}; ${edit}`
8755
+ };
8756
+ }
8757
+ };
8598
8758
  }
8599
8759
  //#endregion
8600
- //#region ../agent-cli/dist/built-ins/update.js
8601
- const updateSchema = object({
8602
- from: string(),
8603
- latest: string(),
8604
- installed: object({
8605
- version: string(),
8606
- prefix: string()
8760
+ //#region ../agent-cli/dist/built-ins/skill/install-command.js
8761
+ /** The project root and the Skill copy under it, as `skill install` writes it. */
8762
+ async function skillCopyOf(cli, cwd) {
8763
+ const root = await projectRootOf(cwd);
8764
+ return {
8765
+ root,
8766
+ copy: join(root, ".claude", "skills", cli.name)
8767
+ };
8768
+ }
8769
+ const installSchema = object({
8770
+ copy: string(),
8771
+ written: array(string()),
8772
+ removed: array(string()),
8773
+ agentsMd: object({
8774
+ path: string(),
8775
+ changed: boolean()
8607
8776
  }).nullable(),
8608
- upgrades: array(upgradeSchema),
8609
- next: array(string())
8777
+ handEdits: array(string())
8610
8778
  });
8611
- function nothingToInstall({ from, latest }) {
8612
- 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`;
8613
- }
8614
- function renderUpgrade(upgrade) {
8615
- return "steps" in upgrade ? [`\nupgrade steps for ${upgrade.version}:`, ...upgrade.steps.map((step) => `- ${step}`)].join("\n") : `\nchanges in ${upgrade.version}:\n${upgrade.notes}`;
8779
+ async function planOf(cli, folder, agents, cwd, packageRoot) {
8780
+ const { root, copy } = await skillCopyOf(cli, cwd);
8781
+ return {
8782
+ ...await syncOf(join(packageRoot, folder), copy),
8783
+ agentsMd: agents === void 0 ? null : await agentsMdAt(agents, root),
8784
+ handEdits: agents === void 0 ? [] : await handEditsAt(root)
8785
+ };
8616
8786
  }
8617
- function renderUpdate(cli, install, update) {
8618
- const { installed } = update;
8619
- const head = installed === null ? [nothingToInstall(update)] : [`installed ${install.package}@${installed.version} with the npm under ${installed.prefix}, replacing ${update.from}`, ...update.upgrades.map(renderUpgrade)];
8620
- const next = update.next.map((command) => `next: ${command}, so the ${cli.name} skill describes ${update.latest}`);
8621
- return (next.length === 0 ? head : [
8622
- ...head,
8623
- "",
8624
- ...next
8625
- ]).join("\n");
8787
+ const SECTION_VERBS = {
8788
+ create: "create",
8789
+ add: "add the Agents section to",
8790
+ rewrite: "rewrite the Agents section in"
8791
+ };
8792
+ function renderPlan(cli, plan) {
8793
+ const files = [
8794
+ ...plan.write.map((file) => ` write ${file}`),
8795
+ ...plan.replace.map((file) => ` replace ${file}`),
8796
+ ...plan.remove.map((file) => ` remove ${file}`)
8797
+ ];
8798
+ const { agentsMd } = plan;
8799
+ return [
8800
+ files.length === 0 ? `the ${cli.name} skill at ${plan.copy} already matches the package` : `would copy the ${cli.name} skill to ${plan.copy}:`,
8801
+ ...files,
8802
+ ...agentsMd === null ? [] : [agentsMd.change === null ? `${agentsMd.path} already holds the ${cli.name} Agents section` : `would ${SECTION_VERBS[agentsMd.change]} ${agentsMd.path}:\n${agentsMd.agents.text}`],
8803
+ ...plan.handEdits.map((edit) => `by hand: ${edit}`)
8804
+ ].join("\n");
8626
8805
  }
8627
- function interruptedAfterInstall(plan) {
8628
- return new CliError({
8629
- code: INTERRUPTED,
8630
- message: `interrupted, but ${plan.package}@${plan.latest} had already been installed with the npm under ${plan.prefix}, replacing ${plan.from}`,
8631
- hint: "nothing to run again: run the commands under next"
8632
- });
8806
+ function renderInstalled(cli, data) {
8807
+ const { written, removed, agentsMd } = data;
8808
+ return [
8809
+ 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}`,
8810
+ ...agentsMd === null ? [] : [agentsMd.changed ? `wrote the ${cli.name} Agents section in ${agentsMd.path}` : `${agentsMd.path} already holds the ${cli.name} Agents section`],
8811
+ ...data.handEdits.map((edit) => `by hand: ${edit}`)
8812
+ ].join("\n");
8633
8813
  }
8634
- /** The built-in `update`, for a CLI installed globally with npm. */
8635
- function updateCommand(cli, install, skill, { bin, node }) {
8814
+ /** The built-in `skill install`, for a CLI whose skill ships in its package. */
8815
+ function skillInstallCommand(cli, folder, agents) {
8636
8816
  return defineCommand$1({
8637
- description: "install the latest version of this CLI from its registry",
8817
+ description: agents === void 0 ? "copy this CLI's skill into the project" : "copy this CLI's skill into the project and write its Agents section",
8638
8818
  examples: [{
8639
- command: `${cli.name} update`,
8640
- note: "install the latest version and show what changed"
8819
+ command: `${cli.name} skill install`,
8820
+ note: "install the skill this version ships"
8641
8821
  }, {
8642
- command: `${cli.name} update --dry-run`,
8643
- note: "show the version it would install"
8822
+ command: `${cli.name} skill install --dry-run`,
8823
+ note: "show what it would change"
8644
8824
  }],
8645
8825
  effect: "write",
8646
- output: updateSchema,
8647
- plan: async (_input, ctx) => {
8648
- const npm = globalNpm(cli, install, ctx, node);
8649
- const copy = await globalInstallOf(bin, npm);
8650
- return {
8651
- package: install.package,
8652
- from: cli.version,
8653
- latest: await latestVersion(npm),
8654
- prefix: copy.prefix,
8655
- packageDir: copy.packageDir
8656
- };
8657
- },
8826
+ output: installSchema,
8827
+ plan: (_input, ctx) => planOf(cli, folder, agents, ctx.cwd, ctx.packageRoot),
8658
8828
  apply: async (plan, ctx) => {
8659
- const { from, latest, prefix } = plan;
8660
- if (standingOf$1(from, latest) !== "behind") return {
8661
- from,
8662
- latest,
8663
- installed: null,
8664
- upgrades: [],
8665
- next: await skill.nextAt(latest, ctx)
8666
- };
8667
- ctx.signal.throwIfAborted();
8668
- ctx.progress(`installing ${plan.package}@${latest} with the npm under ${prefix}…`);
8669
- await ctx.runToEnd(`installing ${plan.package}@${latest}`, () => installGlobally(latest, globalNpm(cli, install, ctx, node)));
8670
- const update = {
8671
- from,
8672
- latest,
8673
- installed: {
8674
- version: latest,
8675
- prefix
8829
+ await applySync(plan, ctx);
8830
+ const { agentsMd } = plan;
8831
+ if (agentsMd !== null && agentsMd.change !== null) {
8832
+ const existing = await orMissing(readFile(agentsMd.path, "utf8")) ?? "";
8833
+ await ctx.files.write(agentsMd.path, withSection(agentsMd.agents, existing));
8834
+ }
8835
+ return {
8836
+ copy: plan.copy,
8837
+ written: [...plan.write, ...plan.replace].sort(),
8838
+ removed: [...plan.remove],
8839
+ agentsMd: agentsMd === null ? null : {
8840
+ path: agentsMd.path,
8841
+ changed: agentsMd.change !== null
8676
8842
  },
8677
- upgrades: await upgradesOf(plan.packageDir, from, latest),
8678
- next: await skill.nextAt(latest, ctx)
8843
+ handEdits: [...plan.handEdits]
8679
8844
  };
8680
- if (ctx.signal.aborted) {
8681
- ctx.deliver(update);
8682
- throw interruptedAfterInstall(plan);
8683
- }
8684
- return update;
8685
8845
  },
8686
- render: (update) => renderUpdate(cli, install, update),
8687
- 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)
8846
+ render: (data) => renderInstalled(cli, data),
8847
+ renderPlan: (plan) => renderPlan(cli, plan)
8688
8848
  });
8689
8849
  }
8690
8850
  //#endregion
8691
- //#region ../agent-cli/dist/built-ins/install/global.js
8692
- function checkOf(cli, install, { bin, node }) {
8693
- return {
8694
- install: async (ctx) => {
8695
- const npm = globalNpm(cli, install, ctx, node);
8696
- try {
8697
- return {
8698
- state: "ok",
8699
- message: `a global npm install under ${(await globalInstallOf(bin, npm)).prefix}`
8700
- };
8701
- } catch (error) {
8702
- if (error instanceof CliError && error.code === "NOT_GLOBAL_INSTALL") return {
8703
- state: "problem",
8704
- message: error.message,
8705
- fix: installCommandLine(npm.registry)
8706
- };
8707
- throw error;
8708
- }
8709
- },
8710
- release: async (ctx) => {
8711
- const fix = `${cli.name} update`;
8712
- return {
8713
- npm: globalNpm(cli, install, ctx, node),
8714
- advise: () => ({ fix })
8715
- };
8716
- }
8717
- };
8851
+ //#region ../agent-cli/dist/built-ins/skill/project.js
8852
+ async function personalCopyOf(name, copy, ctx) {
8853
+ const home = join(homeOf(ctx.env), ".claude", "skills", name);
8854
+ const real = await orMissing(realpath(home));
8855
+ return real === void 0 || real === await orMissing(realpath(copy)) ? void 0 : home;
8718
8856
  }
8719
- /** The Install of a CLI installed once per machine with npm, which `update` replaces. */
8720
- function globalInstall(cli, install) {
8721
- const adapter = {
8722
- runAs: runAsOf(cli),
8723
- builtIns: (launch) => ({ update: updateCommand(cli, install, skillOf$1(cli, adapter), launch) }),
8724
- exitClasses: ["source"],
8725
- check: (launch) => checkOf(cli, install, launch)
8857
+ /** The project root's copy against the package's, and any under home. */
8858
+ async function findingOf(cli, folder, install, ctx) {
8859
+ const { copy } = await skillCopyOf(cli, ctx.cwd);
8860
+ const [installed, shipped] = await Promise.all([orMissing(readFile(join(copy, "SKILL.md"), "utf8")), readFile(join(ctx.packageRoot, folder, "SKILL.md"), "utf8")]);
8861
+ const stated = installed === void 0 ? void 0 : skillVersionIn(installed);
8862
+ const left = stated !== void 0 && skillVersionIn(shipped) === void 0;
8863
+ const found = left ? `${copy} (${statedVersionNote(stated)})` : copy;
8864
+ const head = installed === void 0 ? `no copy of the ${cli.name} skill at ${copy}` : found;
8865
+ const home = await personalCopyOf(cli.name, copy, ctx);
8866
+ const message = home === void 0 ? head : `${head}; remove ${home}: Claude Code prefers a personal skill over the project's`;
8867
+ if (installed === void 0 || left) return {
8868
+ state: "problem",
8869
+ message,
8870
+ fix: `${install.runAs} skill install`
8871
+ };
8872
+ return home === void 0 ? {
8873
+ state: "ok",
8874
+ message
8875
+ } : {
8876
+ state: "problem",
8877
+ message
8726
8878
  };
8727
- return adapter;
8728
8879
  }
8729
- //#endregion
8730
- //#region ../agent-cli/dist/built-ins/install/install.js
8731
- function noInstall(cli) {
8732
- const skipped = async () => ({
8733
- state: "skipped",
8734
- message: `not checked: ${cli.name} declares no install`
8735
- });
8880
+ /** A skill the package ships in `folder`, which `skill install` copies to the project root. */
8881
+ function projectSkill(cli, { folder, agents: text }, install) {
8882
+ const agents = text === void 0 ? void 0 : {
8883
+ name: cli.name,
8884
+ text
8885
+ };
8736
8886
  return {
8737
- runAs: runAsOf(cli),
8738
- builtIns: () => ({}),
8739
- exitClasses: [],
8740
- check: () => ({
8741
- install: skipped,
8742
- release: async () => ({ skipped: await skipped() })
8743
- })
8887
+ builtIns: { skill: { install: skillInstallCommand(cli, folder, agents) } },
8888
+ rows: [{
8889
+ name: "skill",
8890
+ advisory: true,
8891
+ run: (ctx) => findingOf(cli, folder, install, ctx)
8892
+ }, ...agents === void 0 ? [] : [agentsRow(agents, install)]]
8744
8893
  };
8745
8894
  }
8746
- function installOf(cli) {
8747
- const { install } = cli;
8748
- if (install === void 0) return noInstall(cli);
8749
- return install.mode === "global" ? globalInstall(cli, install) : dependencyInstall(cli, install);
8895
+ //#endregion
8896
+ //#region ../agent-cli/dist/built-ins/skill/skill.js
8897
+ const noSkill = ({ name }) => ({
8898
+ builtIns: {},
8899
+ rows: [{
8900
+ name: "skill",
8901
+ advisory: true,
8902
+ run: async () => ({
8903
+ state: "skipped",
8904
+ message: `not checked: ${name} declares no skill`
8905
+ })
8906
+ }]
8907
+ });
8908
+ /** A Global install's Skill copy goes under home; any other goes under the project. */
8909
+ function skillOf(cli, install) {
8910
+ const { skill } = cli;
8911
+ if (skill === void 0) return noSkill(cli);
8912
+ return cli.install?.mode === "global" ? homeSkill(cli, skill, install) : projectSkill(cli, skill, install);
8750
8913
  }
8751
8914
  //#endregion
8752
8915
  //#region ../agent-cli/dist/built-ins/doctor-rows.js
@@ -8883,7 +9046,7 @@ function doctorChecks(cli, launch, scope) {
8883
9046
  ...secretRows(cli.secrets ?? [], cli.commands),
8884
9047
  installRow(check),
8885
9048
  registryRow(cli, check),
8886
- ...skillOf$1(cli, install).rows
9049
+ ...skillOf(cli, install).rows
8887
9050
  ],
8888
9051
  tools: new Set(tools.map(({ name }) => name))
8889
9052
  };
@@ -8901,8 +9064,7 @@ const reportSchema = object({
8901
9064
  ]),
8902
9065
  message: string(),
8903
9066
  fix: string().optional(),
8904
- code: string().optional(),
8905
- skill: packagedSkillDetailsSchema.optional()
9067
+ code: string().optional()
8906
9068
  })),
8907
9069
  next: array(string())
8908
9070
  });
@@ -9037,15 +9199,15 @@ function doctorCommand(cli, launch) {
9037
9199
  //#endregion
9038
9200
  //#region ../agent-cli/dist/built-ins/commands.js
9039
9201
  /**
9040
- * The Commands the Runtime adds to a multi-Command CLI: `update` only for a Global install, and
9041
- * `skill install` only for a Packaged skill.
9202
+ * The Commands the Runtime adds to a multi-Command CLI: `setup` and `update` only for a Global
9203
+ * install, and `skill install` only for a skill whose copy goes under the project.
9042
9204
  */
9043
9205
  function builtInCommands(cli, launch) {
9044
9206
  const install = installOf(cli);
9045
9207
  return {
9046
9208
  doctor: doctorCommand(cli, launch),
9047
9209
  ...install.builtIns(launch),
9048
- ...skillOf$1(cli, install).builtIns
9210
+ ...skillOf(cli, install).builtIns
9049
9211
  };
9050
9212
  }
9051
9213
  //#endregion
@@ -9182,6 +9344,7 @@ const RUNTIME_EXIT_CLASSES = {
9182
9344
  PROJECT_INVALID: "operator",
9183
9345
  DOCTOR_PROBLEMS: "operator",
9184
9346
  NOT_GLOBAL_INSTALL: "operator",
9347
+ NOT_ON_PATH: "operator",
9185
9348
  REGISTRY_UNREACHABLE: "operator",
9186
9349
  REGISTRY_AUTH: "operator",
9187
9350
  INSTALL_FAILED: "operator",
@@ -9255,20 +9418,22 @@ function checkTree(commands, path, table) {
9255
9418
  else checkTree(node, at, table);
9256
9419
  }
9257
9420
  }
9258
- function checkSkillAdd({ name, skill }) {
9259
- if (skill === void 0 || !("add" in skill)) return;
9260
- const declares = `${name} declares skill.add ${JSON.stringify(skill.add)}`;
9261
- if (skill.add.trim() === "") throw new Error(`${declares}, which is empty`);
9262
- if (/[\r\n]/.test(skill.add)) throw new Error(`${declares}, which holds a line break`);
9421
+ function checkSkill({ name, skill, install }) {
9422
+ if (skill?.agents !== void 0 && install?.mode === "global") throw new Error(`${name} declares an Agents section on a Global install, which has no repo to write it to`);
9423
+ }
9424
+ function checkDocs({ name, docs }) {
9425
+ if (docs === void 0) return;
9426
+ if (!(docs !== "" && !isAbsolute(docs) && !normalize(docs).startsWith(".."))) throw new Error(`${name} declares docs ${JSON.stringify(docs)}: name a folder inside its package, relative to the package root`);
9263
9427
  }
9264
9428
  function checkCli(cli) {
9265
9429
  const table = errorTableOf(cli);
9266
- checkSkillAdd(cli);
9430
+ checkDocs(cli);
9267
9431
  if (cli.commands === void 0) {
9268
9432
  checkCommand(cli.command, commandLabel([], cli.name).join(" "), table);
9269
- if (skillOf$1(cli, installOf(cli)).kind === "packaged") throw new Error(`${cli.name} declares a Packaged skill, which only a CLI with several Commands can install`);
9433
+ if (cli.skill !== void 0) throw new Error(`${cli.name} declares a skill, which only a CLI with several Commands can install`);
9270
9434
  return cli;
9271
9435
  }
9436
+ checkSkill(cli);
9272
9437
  const { commands } = cli;
9273
9438
  checkTree(commands, [], table);
9274
9439
  const taken = Object.keys(builtInCommands(cli, {
@@ -9372,30 +9537,6 @@ function startSideTask(sideTask, runWith, { clock, failed }) {
9372
9537
  };
9373
9538
  }
9374
9539
  //#endregion
9375
- //#region ../agent-cli/dist/built-ins/skill/usage-hint.js
9376
- const SKILL_USAGE_CODES = /* @__PURE__ */ new Set([
9377
- UNKNOWN_OPTION,
9378
- UNKNOWN_COMMAND,
9379
- INVALID_INPUT
9380
- ]);
9381
- /** A usage error whose hint names each Skill copy that differs from this build, since one may have named the input. */
9382
- async function withDifferingSkill(error, cli, place, runAs) {
9383
- if (!(error instanceof UsageError) || !SKILL_USAGE_CODES.has(error.code)) return error;
9384
- let differing;
9385
- try {
9386
- differing = await skillOf$1(cli, installOf(cli)).differing(place);
9387
- } catch (lookup) {
9388
- if (errnoOf(lookup) === void 0) throw lookup;
9389
- return error;
9390
- }
9391
- if (differing.length === 0) return error;
9392
- return new UsageError({
9393
- code: error.code,
9394
- message: error.message,
9395
- hint: `${error.hint}; the ${cli.name} skill may name what this build does not know: ${differing.join("; ")}; run \`${runAs} doctor\` to see how to update it`
9396
- });
9397
- }
9398
- //#endregion
9399
9540
  //#region ../agent-cli/dist/command-line/suggest.js
9400
9541
  const MAX_DISTANCE = 3;
9401
9542
  const MIN_SIMILARITY = .4;
@@ -10147,7 +10288,11 @@ function helpResult(node, root, cli, { style, json }) {
10147
10288
  }
10148
10289
  function versionResult(cli, packageRoot) {
10149
10290
  return {
10150
- stdout: `${(buildStamp() === void 0 ? [versionLine(cli)] : [versionLine(cli), `changelog ${join(packageRoot, "CHANGELOG.md")}`]).join("\n")}\n`,
10291
+ stdout: `${[
10292
+ versionLine(cli),
10293
+ ...buildStamp() === void 0 ? [] : [`changelog ${join(packageRoot, "CHANGELOG.md")}`],
10294
+ ...cli.docs === void 0 ? [] : [`docs ${join(packageRoot, cli.docs)}`]
10295
+ ].join("\n")}\n`,
10151
10296
  stderr: "",
10152
10297
  exitCode: SUCCESS_EXIT_CODE
10153
10298
  };
@@ -10214,6 +10359,77 @@ function checkOperator({ operator, commandLine }, env, rerun) {
10214
10359
  });
10215
10360
  }
10216
10361
  //#endregion
10362
+ //#region ../agent-cli/dist/context/outputs.js
10363
+ const OUTPUT_EXISTS = "OUTPUT_EXISTS";
10364
+ /** Refuses a run that would overwrite existing outputs unless the caller passed `--force`. */
10365
+ async function checkOutputs({ rerun, commandLine, force, files }, outputs) {
10366
+ if (outputs === void 0 || force) return;
10367
+ const found = await Promise.all(outputs.map((path) => files.exists(path)));
10368
+ const existing = outputs.filter((_path, index) => found[index]);
10369
+ if (existing.length === 0) return;
10370
+ const them = existing.length === 1 ? "it" : "them";
10371
+ throw new CliError({
10372
+ code: OUTPUT_EXISTS,
10373
+ message: `\`${commandLine}\` would overwrite ${existing.join(", ")}`,
10374
+ hint: `once the user agrees to replace ${them}, run the Next step`,
10375
+ next: [rerun({ add: [FORCE_FLAG.flag] })]
10376
+ });
10377
+ }
10378
+ /** Guards the writes to declared outputs of `files`, whose paths resolve against `cwd`. */
10379
+ function outputsGuard(files, cwd) {
10380
+ const unwritten = /* @__PURE__ */ new Set();
10381
+ let forceStep = [];
10382
+ const exclusiveWrite = async (path, data, options) => {
10383
+ const target = resolve(cwd, path);
10384
+ unwritten.delete(target);
10385
+ try {
10386
+ await files.write(path, data, {
10387
+ ...options,
10388
+ exclusive: true
10389
+ });
10390
+ } catch (error) {
10391
+ unwritten.add(target);
10392
+ if (!(error instanceof CliError) || error.code !== "ALREADY_EXISTS") throw error;
10393
+ throw new CliError({
10394
+ code: ALREADY_EXISTS,
10395
+ message: error.message,
10396
+ hint: "once the user agrees to replace it, run the Next step",
10397
+ next: [forceStep]
10398
+ }, { cause: error });
10399
+ }
10400
+ };
10401
+ return {
10402
+ files: {
10403
+ ...files,
10404
+ write: (path, data, options) => unwritten.has(resolve(cwd, path)) ? exclusiveWrite(path, data, options) : files.write(path, data, options)
10405
+ },
10406
+ guard: (outputs, step) => {
10407
+ forceStep = step;
10408
+ for (const path of outputs) unwritten.add(resolve(cwd, path));
10409
+ }
10410
+ };
10411
+ }
10412
+ /**
10413
+ * Creates each output folder and a temp file in it, so a sandbox denial fails before `apply`
10414
+ * spends anything. The temp file's name is the Runtime's, so a crashed run's leftover is swept.
10415
+ */
10416
+ async function probeOutputs({ files, cwd, fileError }, outputs) {
10417
+ const folders = new Set(outputs.map((path) => dirname(resolve(cwd, path))));
10418
+ await Promise.all([...folders].map(async (folder) => {
10419
+ const probe = tempPathOf(join(folder, "outputs-probe"));
10420
+ try {
10421
+ await files.mkdir(folder);
10422
+ await files.write(probe, "");
10423
+ await files.remove(probe);
10424
+ } catch (error) {
10425
+ throw fileError(error, {
10426
+ path: folder,
10427
+ access: "write"
10428
+ });
10429
+ }
10430
+ }));
10431
+ }
10432
+ //#endregion
10217
10433
  //#region ../agent-cli/dist/context/spill.js
10218
10434
  const STDOUT_BUDGET_CHARS = 3e4;
10219
10435
  const SPILL_RETENTION_MS = 6048e5;
@@ -11106,11 +11322,7 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11106
11322
  });
11107
11323
  });
11108
11324
  } catch (error) {
11109
- return failed(await withDifferingSkill(error, cli, {
11110
- cwd,
11111
- env,
11112
- packageRoot
11113
- }, invocation.runAs), noSideTask);
11325
+ return failed(error, noSideTask);
11114
11326
  }
11115
11327
  const { input, flags } = parsed;
11116
11328
  resumed = command.deadline?.resume(input, rerunOf);
@@ -11178,9 +11390,10 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11178
11390
  let served = false;
11179
11391
  let settled;
11180
11392
  try {
11181
- const outcome = await control.settle(skillOf$1(cli, installOf(cli)).notice({
11393
+ const outcome = await control.settle(noticesOf(cli, {
11182
11394
  builtIn,
11183
- bin: options.bin
11395
+ bin: options.bin,
11396
+ node: options.node
11184
11397
  }, ctx).then(() => execute(command, input, ctx, {
11185
11398
  dryRun: flags.dryRun,
11186
11399
  confirm: flags.confirm,
@@ -11240,6 +11453,9 @@ function once(build) {
11240
11453
  throw outcome.error;
11241
11454
  };
11242
11455
  }
11456
+ async function noticesOf(cli, launch, ctx) {
11457
+ await installOf(cli).notice(launch, ctx);
11458
+ }
11243
11459
  //#endregion
11244
11460
  //#region ../agent-cli/dist/style.js
11245
11461
  function colourEnabled(env, stdoutIsTTY) {
@@ -11322,16 +11538,10 @@ async function routeAndRun(cli, argv, options, write, stopNotice) {
11322
11538
  ...format,
11323
11539
  json: routed.json
11324
11540
  }));
11325
- case "usage error": {
11326
- const place = {
11327
- ...options,
11328
- packageRoot: packageRootOf(options.bin, options.cwd)
11329
- };
11330
- return ranAlone(errorResult(await withDifferingSkill(routed.error, cli, place, runAs), {
11331
- ...misused(routed.node),
11332
- json: routed.json
11333
- }));
11334
- }
11541
+ case "usage error": return ranAlone(errorResult(routed.error, {
11542
+ ...misused(routed.node),
11543
+ json: routed.json
11544
+ }));
11335
11545
  case "command": return whileStopping(options.signal, stopNotice === void 0 ? void 0 : () => write.stderr(stopLine(stopNotice, routed.json)), () => runCommand(cli, routed, options, write, misused(routed.spec)));
11336
11546
  }
11337
11547
  }
@@ -11394,7 +11604,7 @@ async function runProcess(cli) {
11394
11604
  //#region package.json
11395
11605
  var package_default = {
11396
11606
  name: "@duyquangnvx/create-agent-cli",
11397
- version: "0.22.0",
11607
+ version: "0.24.0",
11398
11608
  description: "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
11399
11609
  type: "module",
11400
11610
  engines: { "node": ">=22.12.0" },
@@ -11443,80 +11653,35 @@ function fill(text, values, quoted) {
11443
11653
  //#region src/install-mode.ts
11444
11654
  const PARTIALS_DIR = fileURLToPath(new URL("../partials/", import.meta.url));
11445
11655
  const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11446
- const COMMANDS_ANCHOR = " commands: {";
11447
- const GLOBAL_INSTALL = ` install: "global",\n`;
11448
- const NO_SKILL_HUB = "<!-- Set `skill.source` in `defineCli` and `skillHub: { git }` in `scripts/release.ts` to the git URL of the Skill Hub each release commits the skill to, or set `skill.add` to the whole line that installs the skill and `skillHub: { api }` to the API Skill Hub each release publishes the skill to. Then put here what `node dist/bin.js doctor` gives in its `skill` row: the line of its fix, after `! `. -->";
11449
- function skillHubOf({ git, api, add }) {
11450
- if (git !== void 0 && api !== void 0) throw new UsageError({
11451
- code: "SKILL_HOMES_CONFLICT",
11452
- message: "--skill-hub-git and --skill-hub-api each name the Skill Hub the skill goes to",
11453
- hint: "pass one of them"
11454
- });
11455
- if (api !== void 0 && add === void 0) throw new UsageError({
11456
- code: "SKILL_ADD_MISSING",
11457
- message: "--skill-hub-api needs --skill-add, the whole line that installs the skill from the hub",
11458
- hint: "add --skill-add with the hub's install line for the skill"
11459
- });
11460
- if (add !== void 0 && api === void 0) throw new UsageError({
11461
- code: "SKILL_HUB_API_MISSING",
11462
- message: "--skill-add is the install line of an API Skill Hub, which --skill-hub-api names",
11463
- hint: "add --skill-hub-api with the hub's URL"
11464
- });
11465
- if (add !== void 0 && (add.trim() === "" || /[\r\n]/.test(add))) throw new UsageError({
11466
- code: "SKILL_ADD_INVALID",
11467
- message: `--skill-add ${JSON.stringify(add)} is not one command line`,
11468
- hint: "pass the hub's install line for the skill, on one line"
11469
- });
11470
- if (git !== void 0) return {
11471
- kind: "git",
11472
- url: git
11473
- };
11474
- return api === void 0 || add === void 0 ? void 0 : {
11475
- kind: "api",
11476
- url: api,
11477
- add
11478
- };
11479
- }
11480
- function installModeOf(flags, dependency, binName) {
11481
- const skillHub = skillHubOf(flags);
11482
- if (skillHub !== void 0 && dependency) throw new UsageError({
11483
- code: "SKILL_HOMES_CONFLICT",
11484
- message: `--skill-hub-${skillHub.kind} and --dependency each name where the skill comes from: a Dependency install ships it inside the package`,
11485
- hint: `pass --dependency for a CLI each project pins, or --skill-hub-${skillHub.kind} for a global one whose skill each release puts on a Skill Hub`
11486
- });
11487
- if ((skillHub !== void 0 || dependency) && !SKILL_NAME.test(binName)) throw new UsageError({
11656
+ const TEMPLATE_REGISTRY = "https://registry.npmjs.org/";
11657
+ function installModeOf(dependency, binName) {
11658
+ if (!SKILL_NAME.test(binName)) throw new UsageError({
11488
11659
  code: "SKILL_NAME_INVALID",
11489
11660
  message: `${binName} cannot name a skill, whose names are lowercase letters, digits and single hyphens`,
11490
- hint: `choose a name such as my-cli, or leave out ${dependency ? "--dependency" : `--skill-hub-${skillHub?.kind}`}`
11661
+ hint: "choose a name such as my-cli"
11491
11662
  });
11492
- return dependency ? { mode: "dependency" } : {
11493
- mode: "global",
11494
- skillHub
11495
- };
11496
- }
11497
- function skillLineOf(skillHub, binName) {
11498
- if (skillHub === void 0) return NO_SKILL_HUB;
11499
- return skillHub.kind === "git" ? `\`! npx -y skills add ${skillHub.url} --skill ${binName} -g\`` : `\`! ${skillHub.add}\``;
11663
+ return dependency ? "dependency" : "global";
11500
11664
  }
11501
11665
  async function partialsOf(mode, values) {
11502
11666
  const partial = async (name) => fill(await readFile(join(PARTIALS_DIR, mode, `${name}.md`), "utf8"), values, false).trimEnd();
11503
- const [installLines, skillCheck, installSteps, updateSteps, sandbox] = await Promise.all([
11667
+ const [installLines, skillCheck, installSteps, updateSteps, sandbox, whenMissing] = await Promise.all([
11504
11668
  partial("installLines"),
11505
11669
  partial("skillCheck"),
11506
11670
  partial("installSteps"),
11507
11671
  partial("updateSteps"),
11508
- partial("sandbox")
11672
+ partial("sandbox"),
11673
+ partial("whenMissing")
11509
11674
  ]);
11510
11675
  return {
11511
11676
  installLines,
11512
11677
  skillCheck,
11513
11678
  installSteps,
11514
11679
  updateSteps,
11515
- sandbox
11680
+ sandbox,
11681
+ whenMissing
11516
11682
  };
11517
11683
  }
11518
- async function globalSetup(skillHub, packageName, binName) {
11519
- const skillLine = skillLineOf(skillHub, binName);
11684
+ async function globalSetup(packageName, binName) {
11520
11685
  const runAs = runAsOf({ name: binName });
11521
11686
  return {
11522
11687
  values: {
@@ -11526,24 +11691,16 @@ async function globalSetup(skillHub, packageName, binName) {
11526
11691
  packageName,
11527
11692
  binName,
11528
11693
  runAs,
11529
- skillLine
11694
+ setupLine: setupLineOf({
11695
+ mode: "global",
11696
+ package: packageName,
11697
+ registry: TEMPLATE_REGISTRY
11698
+ })
11530
11699
  })
11531
11700
  },
11532
- edits: skillHub === void 0 ? [] : [{
11533
- file: "src/cli.ts",
11534
- find: COMMANDS_ANCHOR,
11535
- replace: ` skill: ${skillOf(skillHub)},\n${COMMANDS_ANCHOR}`
11536
- }, {
11537
- file: "scripts/release.ts",
11538
- find: " smoke: {",
11539
- replace: ` skillHub: { ${skillHub.kind}: ${JSON.stringify(skillHub.url)} },\n smoke: {`
11540
- }],
11541
- files: []
11701
+ edits: []
11542
11702
  };
11543
11703
  }
11544
- function skillOf(skillHub) {
11545
- return skillHub.kind === "git" ? `{ source: ${JSON.stringify(skillHub.url)} }` : `{ add: ${JSON.stringify(skillHub.add)} }`;
11546
- }
11547
11704
  async function dependencySetup(packageName, binName) {
11548
11705
  const runAs = runAsOf({
11549
11706
  name: binName,
@@ -11564,18 +11721,13 @@ async function dependencySetup(packageName, binName) {
11564
11721
  },
11565
11722
  edits: [{
11566
11723
  file: "src/cli.ts",
11567
- find: GLOBAL_INSTALL,
11724
+ find: ` install: "global",\n`,
11568
11725
  replace: ` install: "dependency",\n`
11569
- }, {
11570
- file: "src/cli.ts",
11571
- find: COMMANDS_ANCHOR,
11572
- replace: ` skill: { packaged: "skills/${binName}" },\n${COMMANDS_ANCHOR}`
11573
- }],
11574
- files: ["skills"]
11726
+ }]
11575
11727
  };
11576
11728
  }
11577
11729
  async function installSetupOf(install, packageName, binName) {
11578
- return install.mode === "global" ? globalSetup(install.skillHub, packageName, binName) : dependencySetup(packageName, binName);
11730
+ return install === "global" ? globalSetup(packageName, binName) : dependencySetup(packageName, binName);
11579
11731
  }
11580
11732
  //#endregion
11581
11733
  //#region src/steps.ts
@@ -11728,9 +11880,6 @@ async function editManifest(root, edit) {
11728
11880
  async function setRepository(root, repository) {
11729
11881
  await editManifest(root, (entries) => entries.flatMap((entry) => entry[0] === "description" ? [entry, ["repository", repository]] : [entry]));
11730
11882
  }
11731
- async function addFiles(root, files) {
11732
- await editManifest(root, (entries) => entries.map(([key, value]) => key === "files" && Array.isArray(value) ? [key, [...value, ...files]] : [key, value]));
11733
- }
11734
11883
  async function ensureFree(path) {
11735
11884
  if (!await isFree(path)) throw new UsageError({
11736
11885
  code: "TARGET_NOT_EMPTY",
@@ -11756,18 +11905,11 @@ await runProcess(defineCli({
11756
11905
  install: boolean().default(true).describe("run pnpm install"),
11757
11906
  git: boolean().default(true).describe("create a git repository with an initial commit"),
11758
11907
  repository: url({ protocol: /^https$/ }).optional().describe("the https URL of the CLI's git repository"),
11759
- skillHubGit: 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 a git Skill Hub each release commits the CLI's skill to"),
11760
- skillHubApi: url({ protocol: /^https$/ }).optional().describe("the https URL of an API Skill Hub each release publishes the CLI's skill to"),
11761
- skillAdd: string().optional().describe("the whole line that installs the CLI's skill from the API Skill Hub"),
11762
- dependency: boolean().default(false).describe("install the CLI per project, as a devDependency, with its skill shipped inside the package")
11908
+ dependency: boolean().default(false).describe("install the CLI per project, as a devDependency, with its skill copied into the project")
11763
11909
  }),
11764
- plan: async ({ name, description, install, git, repository, skillHubGit, skillHubApi, skillAdd, dependency }, ctx) => {
11910
+ plan: async ({ name, description, install, git, repository, dependency }, ctx) => {
11765
11911
  const binName = PACKAGE_NAME.exec(name)?.[1] ?? name;
11766
- const mode = installModeOf({
11767
- git: skillHubGit,
11768
- api: skillHubApi,
11769
- add: skillAdd
11770
- }, dependency, binName);
11912
+ const mode = installModeOf(dependency, binName);
11771
11913
  const path = resolve(ctx.cwd, binName);
11772
11914
  await ensureFree(path);
11773
11915
  const setup = await installSetupOf(mode, name, binName);
@@ -11790,8 +11932,7 @@ await runProcess(defineCli({
11790
11932
  }),
11791
11933
  install,
11792
11934
  repository,
11793
- edits: setup.edits,
11794
- manifestFiles: setup.files
11935
+ edits: setup.edits
11795
11936
  };
11796
11937
  },
11797
11938
  apply: async (plan, ctx) => {
@@ -11799,7 +11940,6 @@ await runProcess(defineCli({
11799
11940
  await writeTemplate(plan.path, plan.files, plan.values);
11800
11941
  if (plan.repository !== void 0) await setRepository(plan.path, plan.repository);
11801
11942
  for (const edit of plan.edits) await applyEdit(plan.path, edit);
11802
- if (plan.manifestFiles.length > 0) await addFiles(plan.path, plan.manifestFiles);
11803
11943
  await runSteps(plan.steps, plan.path, ctx);
11804
11944
  return {
11805
11945
  path: plan.path,