@duyquangnvx/create-agent-cli 0.23.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
@@ -7085,9 +7085,9 @@ function secretRows(secrets, commands) {
7085
7085
  /** The bundler's stamp, absent when the CLI runs unbundled. Read per call, so a test can define it. */
7086
7086
  function buildStamp() {
7087
7087
  return {
7088
- "sha": "71583a7",
7088
+ "sha": "45caa4e",
7089
7089
  "dirty": false,
7090
- "builtAt": "2026-11-03T06:40:47.000Z"
7090
+ "builtAt": "2026-11-05T07:43:22.000Z"
7091
7091
  };
7092
7092
  }
7093
7093
  /** The first line of `--version`: the version, and the commit and time of a stamped build. */
@@ -7237,6 +7237,16 @@ function installCommandLine(registry, version = "latest") {
7237
7237
  ...registry.flags
7238
7238
  ].join(" ");
7239
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
+ }
7240
7250
  function npmOf(ctx, windows) {
7241
7251
  const beside = ctx.node === void 0 ? void 0 : join(dirname(ctx.node), windows ? "npm.cmd" : "npm");
7242
7252
  if (beside === void 0 || !existsSync(beside)) return {
@@ -7280,13 +7290,13 @@ function failureOf(call, answer, ctx) {
7280
7290
  const code = NPM_CODE.exec(answer.stderr)?.[1];
7281
7291
  const output = tail$1(answer.stderr);
7282
7292
  const next = [ctx.rerun];
7283
- if (code !== void 0 && NETWORK_CODES.has(code)) return new CliError({
7293
+ if (code !== void 0 && NETWORK_CODES.has(code)) return runtimeError({
7284
7294
  code: REGISTRY_UNREACHABLE,
7285
7295
  message: `npm could not reach ${registry} (${code})${output}`,
7286
7296
  hint: `${unreachableHint}, then ${RUN_AGAIN}`,
7287
7297
  next
7288
7298
  });
7289
- if (code !== void 0 && AUTH_CODES.has(code)) return new CliError({
7299
+ if (code !== void 0 && AUTH_CODES.has(code)) return runtimeError({
7290
7300
  code: REGISTRY_AUTH,
7291
7301
  message: `${registry} refused npm's credentials (${code})${output}`,
7292
7302
  hint: authHint(code, ctx),
@@ -7299,7 +7309,7 @@ function failureOf(call, answer, ctx) {
7299
7309
  hint: `fix what npm reports, then ${RUN_AGAIN} with the first Next step, or install it yourself with the second`,
7300
7310
  next: [...next, wholeLine(installCommandLine(ctx.registry))]
7301
7311
  });
7302
- return new CliError({
7312
+ return runtimeError({
7303
7313
  code: NPM_FAILED,
7304
7314
  message: `npm ${call} failed with exit code ${answer.exitCode}${output}`,
7305
7315
  hint: `fix what npm reports, then ${RUN_AGAIN}`,
@@ -7623,183 +7633,6 @@ function dependencyInstall(cli, install) {
7623
7633
  };
7624
7634
  }
7625
7635
  //#endregion
7626
- //#region ../agent-cli/dist/built-ins/skill/hub.js
7627
- const SKILL_DIRS = [[".claude", "skills"], [".agents", "skills"]];
7628
- const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
7629
- const METADATA_VERSION = /^metadata:[ \t]*\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+version:[ \t]*["']?([^"'\s]+)["']?[ \t]*$/m;
7630
- /** The version a SKILL.md's frontmatter `metadata.version` states. */
7631
- function skillVersionIn(skill) {
7632
- const frontmatter = FRONTMATTER.exec(skill)?.[1];
7633
- return frontmatter === void 0 ? void 0 : METADATA_VERSION.exec(`${frontmatter}\n`)?.[1];
7634
- }
7635
- function homeOf(env) {
7636
- return env.HOME ?? env.USERPROFILE ?? homedir();
7637
- }
7638
- async function copyAt(path, version) {
7639
- let skill;
7640
- try {
7641
- skill = await readFile(join(path, "SKILL.md"), "utf8");
7642
- } catch (error) {
7643
- if (isMissingFile(error)) return;
7644
- return {
7645
- path,
7646
- version: void 0,
7647
- state: "unreadable"
7648
- };
7649
- }
7650
- const stated = skillVersionIn(skill);
7651
- if (stated === void 0 || !isVersion(stated) || !isVersion(version)) return {
7652
- path,
7653
- version: stated,
7654
- state: "unversioned"
7655
- };
7656
- return {
7657
- path,
7658
- version: stated,
7659
- state: standingOf$1(stated, version)
7660
- };
7661
- }
7662
- async function skillCopies(cliName, version, { cwd, env }) {
7663
- const paths = [cwd, homeOf(env)].flatMap((root) => SKILL_DIRS.map((dir) => join(root, ...dir, cliName)));
7664
- const seen = /* @__PURE__ */ new Set();
7665
- const copies = [];
7666
- for (const path of paths) {
7667
- const copy = await copyAt(path, version);
7668
- const real = copy === void 0 ? void 0 : await realpath(path);
7669
- if (copy !== void 0 && real !== void 0 && !seen.has(real)) {
7670
- seen.add(real);
7671
- copies.push(copy);
7672
- }
7673
- }
7674
- return copies;
7675
- }
7676
- const has = (copies, wanted) => copies.some(({ state }) => state === wanted);
7677
- function addLineOf(cliName, from) {
7678
- return "add" in from ? from.add : `npx -y skills add ${from.source} --skill ${cliName} -g`;
7679
- }
7680
- function fixFor(cliName, copies, from) {
7681
- const update = `npx -y skills update ${cliName}`;
7682
- const add = from === void 0 ? void 0 : addLineOf(cliName, from);
7683
- if (copies.length === 0) return add;
7684
- if (has(copies, "unversioned") && add !== void 0) return has(copies, "behind") ? `${update} && ${add}` : add;
7685
- return has(copies, "behind") || has(copies, "unversioned") ? update : void 0;
7686
- }
7687
- const SKILL_STATE_NOTES = {
7688
- current: "the same as this build",
7689
- behind: "older than this build",
7690
- ahead: "newer than this build",
7691
- unversioned: "states no metadata.version",
7692
- unreadable: "could not be read"
7693
- };
7694
- function describeCopy({ path, version, state }) {
7695
- return `${path}${version === void 0 ? "" : ` ${version}`} (${SKILL_STATE_NOTES[state]})`;
7696
- }
7697
- /** A skill on a Skill Hub, which `from` says how to install; without it, no line adds it. */
7698
- function hubSkill(cli, from) {
7699
- const findingOf = (copies) => {
7700
- const fix = fixFor(cli.name, copies, from);
7701
- const message = copies.length === 0 ? `no copy of the ${cli.name} skill is installed` : copies.map(describeCopy).join("; ");
7702
- return fix === void 0 ? {
7703
- state: "ok",
7704
- message
7705
- } : {
7706
- state: "ok",
7707
- message,
7708
- fix
7709
- };
7710
- };
7711
- return {
7712
- kind: "hub",
7713
- builtIns: {},
7714
- rows: [{
7715
- name: "skill",
7716
- advisory: true,
7717
- run: async (ctx) => findingOf(await skillCopies(cli.name, cli.version, ctx))
7718
- }],
7719
- notice: async () => {},
7720
- differing: async (place) => (await skillCopies(cli.name, cli.version, place)).filter(({ state }) => state !== "current" && state !== "unreadable").map(describeCopy),
7721
- nextAt: async (version, ctx) => {
7722
- const copies = await skillCopies(cli.name, version, ctx);
7723
- const fix = fixFor(cli.name, copies, from);
7724
- return fix === void 0 ? [] : [fix];
7725
- }
7726
- };
7727
- }
7728
- //#endregion
7729
- //#region ../agent-cli/dist/built-ins/agents-md.js
7730
- function markersOf(name) {
7731
- return {
7732
- begin: `<!-- BEGIN:${name} -->`,
7733
- end: `<!-- END:${name} -->`
7734
- };
7735
- }
7736
- /** `text` with the CLI's Agents section between its markers, appended when they are missing; nothing else changes. */
7737
- function withSection({ name, text: section }, text) {
7738
- const { begin, end } = markersOf(name);
7739
- const whole = `${begin}\n${section}\n${end}`;
7740
- const start = text.indexOf(begin);
7741
- const stop = start === -1 ? -1 : text.indexOf(end, start);
7742
- if (stop !== -1) return `${text.slice(0, start)}${whole}${text.slice(stop + end.length)}`;
7743
- if (text === "") return `${whole}\n`;
7744
- return `${text}${text.endsWith("\n") ? "" : "\n"}\n${whole}\n`;
7745
- }
7746
- async function agentsMdAt(agents, root) {
7747
- const path = join(root, "AGENTS.md");
7748
- const existing = await orMissing(readFile(path, "utf8"));
7749
- if (existing === void 0) return {
7750
- path,
7751
- agents,
7752
- change: "create"
7753
- };
7754
- if (withSection(agents, existing) === existing) return {
7755
- path,
7756
- agents,
7757
- change: null
7758
- };
7759
- return {
7760
- path,
7761
- agents,
7762
- change: existing.includes(markersOf(agents.name).begin) ? "rewrite" : "add"
7763
- };
7764
- }
7765
- async function handEditsAt(root) {
7766
- const claudeMd = join(root, "CLAUDE.md");
7767
- const text = await orMissing(readFile(claudeMd, "utf8"));
7768
- if (text === void 0 || text.split(/\r?\n/).some((line) => line.trim() === "@AGENTS.md")) return [];
7769
- return [`add a line \`@AGENTS.md\` to ${claudeMd}, so Claude Code reads AGENTS.md`];
7770
- }
7771
- function agentsRow(agents, install) {
7772
- const { name } = agents;
7773
- return {
7774
- name: "agents",
7775
- advisory: true,
7776
- run: async (ctx) => {
7777
- const root = await projectRootOf(ctx.cwd);
7778
- const { path, change } = await agentsMdAt(agents, root);
7779
- const fix = `${install.runAs} skill install`;
7780
- if (change === "rewrite") return {
7781
- state: "problem",
7782
- message: `${path} holds a ${name} Agents section other than the one ${fix} writes`,
7783
- fix
7784
- };
7785
- if (change !== null) return {
7786
- state: "problem",
7787
- message: `${path} holds no ${name} Agents section, so sessions in this repo do not know ${name}`,
7788
- fix
7789
- };
7790
- const holds = `${path} holds the ${name} Agents section`;
7791
- const [edit] = await handEditsAt(root);
7792
- return edit === void 0 ? {
7793
- state: "ok",
7794
- message: holds
7795
- } : {
7796
- state: "problem",
7797
- message: `${holds}; ${edit}`
7798
- };
7799
- }
7800
- };
7801
- }
7802
- //#endregion
7803
7636
  //#region ../agent-cli/dist/context/errno.js
7804
7637
  const PERMISSION_DENIED = "PERMISSION_DENIED";
7805
7638
  const DISK_FULL = "DISK_FULL";
@@ -7862,565 +7695,84 @@ function classifyErrno(error, context) {
7862
7695
  }
7863
7696
  }
7864
7697
  //#endregion
7865
- //#region ../agent-cli/dist/context/files.js
7866
- const TEMP_FILE = /^\..+\.agent-cli-(\d+)\.tmp$/;
7867
- /** Where the Runtime writes `path` before moving it into place; a dead writer's is swept. */
7868
- function tempPathOf(path) {
7869
- 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
+ });
7870
7711
  }
7871
- function isAlive(pid) {
7872
- try {
7873
- process.kill(pid, 0);
7874
- return true;
7875
- } catch (error) {
7876
- return errnoOf(error) === "EPERM";
7877
- }
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";
7878
7718
  }
7879
- async function sweep(folder) {
7880
- const dead = (await readdir(folder)).filter((name) => {
7881
- const pid = TEMP_FILE.exec(name)?.[1];
7882
- 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
7883
7724
  });
7884
- await Promise.all(dead.map((name) => rm(join(folder, name), { force: true })));
7885
7725
  }
7886
- /** The file a symlink at `path` points to, so a replacing write keeps the link. */
7887
- async function targetOf$1(path) {
7888
- try {
7889
- return await realpath(path);
7890
- } catch (error) {
7891
- if (isMissingFile(error)) return path;
7892
- throw error;
7893
- }
7894
- }
7895
- async function writeWithMode(path, data, mode) {
7896
- if (mode === void 0) {
7897
- await writeFile(path, data);
7898
- 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
+ }
7899
7760
  }
7900
- await writeFile(path, data, { mode });
7901
- await chmod(path, mode);
7902
- }
7903
- /** What `link` answers on a file system without hard links, such as exFAT. */
7904
- const NO_HARD_LINKS = /* @__PURE__ */ new Set([
7905
- "EPERM",
7906
- "ENOTSUP",
7907
- "EXDEV"
7908
- ]);
7909
- /** A link fails atomically when `path` is taken, so no reader ever sees half a file there. */
7910
- async function linkNew(temp, path, data, mode) {
7911
- try {
7912
- await link(temp, path);
7913
- } catch (error) {
7914
- if (!NO_HARD_LINKS.has(errnoOf(error))) throw error;
7915
- 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")]);
7916
7763
  }
7917
- }
7918
- /** A file this call created and could not finish is removed, so a rerun does not find half of it. */
7919
- async function createNew(path, data, mode) {
7920
- const handle = await open(path, "wx", mode);
7921
- try {
7922
- await handle.writeFile(data);
7923
- if (mode !== void 0) await handle.chmod(mode);
7924
- } catch (error) {
7925
- await handle.close();
7926
- await rm(path, { force: true });
7927
- throw error;
7928
- }
7929
- await handle.close();
7930
- }
7931
- const RENAME_ATTEMPTS = 10;
7932
- const RENAME_BACKOFF_MS = 10;
7933
- const HELD_BY_ANOTHER_PROGRAM = /* @__PURE__ */ new Set(["EPERM", "EBUSY"]);
7934
- async function renameRetrying(from, to) {
7935
- for (let attempt = 1;; attempt++) try {
7936
- await rename(from, to);
7937
- return;
7938
- } catch (error) {
7939
- if (!(process.platform === "win32" && HELD_BY_ANOTHER_PROGRAM.has(errnoOf(error))) || attempt === RENAME_ATTEMPTS) throw error;
7940
- await setTimeout$1(attempt * RENAME_BACKOFF_MS);
7941
- }
7942
- }
7943
- /** Real files for one run: the first write to each folder sweeps the temp files of dead writers. */
7944
- function realFiles() {
7945
- const swept = /* @__PURE__ */ new Set();
7946
- return {
7947
- exists: async (path) => {
7948
- try {
7949
- await access(path);
7950
- return true;
7951
- } catch (error) {
7952
- if (isNothingThere(error)) return false;
7953
- throw error;
7954
- }
7955
- },
7956
- write: async (path, data, options = {}) => {
7957
- await mkdir(dirname(path), { recursive: true });
7958
- const target = options.exclusive === true ? path : await targetOf$1(path);
7959
- const folder = dirname(target);
7960
- if (!swept.has(folder)) {
7961
- await sweep(folder);
7962
- swept.add(folder);
7963
- }
7964
- const temp = tempPathOf(target);
7965
- try {
7966
- await writeWithMode(temp, data, options.mode);
7967
- if (options.exclusive === true) {
7968
- await linkNew(temp, target, data, options.mode);
7969
- await rm(temp);
7970
- } else await renameRetrying(temp, target);
7971
- } catch (error) {
7972
- await rm(temp, { force: true });
7973
- throw error;
7974
- }
7975
- },
7976
- read: (path) => readFile(path),
7977
- list: (folder) => readdir(folder),
7978
- remove: (path) => rm(path, {
7979
- recursive: true,
7980
- force: true
7981
- }),
7982
- mkdir: async (path, options) => {
7983
- const mode = options?.mode;
7984
- if (options?.exclusive !== true) {
7985
- await mkdir(path, {
7986
- recursive: true,
7987
- mode
7988
- });
7989
- return;
7990
- }
7991
- await mkdir(dirname(path), {
7992
- recursive: true,
7993
- mode
7994
- });
7995
- await mkdir(path, { mode });
7996
- },
7997
- stat: async (path) => {
7998
- try {
7999
- const { size, dev, ino } = await stat(path, { bigint: true });
8000
- return {
8001
- size: Number(size),
8002
- id: `${dev}:${ino}`
8003
- };
8004
- } catch (error) {
8005
- if (isNothingThere(error)) return void 0;
8006
- throw error;
8007
- }
8008
- }
8009
- };
8010
- }
8011
- /**
8012
- * `files` resolving paths against `cwd`, its failures classified by errno; `list` drops the
8013
- * Runtime's temp files and sorts.
8014
- */
8015
- function classifiedFiles(files, { cwd, fileError }) {
8016
- async function at(access, path, work) {
8017
- const target = resolve(cwd, path);
8018
- try {
8019
- return await work(target);
8020
- } catch (error) {
8021
- throw fileError(error, {
8022
- path: target,
8023
- access
8024
- });
8025
- }
8026
- }
8027
- return {
8028
- exists: (path) => at("read", path, (target) => files.exists(target)),
8029
- write: (path, data, options) => at("write", path, (target) => files.write(target, data, options)),
8030
- read: (path) => at("read", path, (target) => files.read(target)),
8031
- list: (folder) => at("read", folder, async (target) => (await files.list(target)).filter((name) => !TEMP_FILE.test(name)).sort()),
8032
- remove: (path) => at("write", path, (target) => files.remove(target)),
8033
- mkdir: (path, options) => at("write", path, (target) => files.mkdir(target, options)),
8034
- stat: (path) => at("read", path, (target) => files.stat(target))
8035
- };
8036
- }
8037
- //#endregion
8038
- //#region ../agent-cli/dist/context/outputs.js
8039
- const OUTPUT_EXISTS = "OUTPUT_EXISTS";
8040
- /** Refuses a run that would overwrite existing outputs unless the caller passed `--force`. */
8041
- async function checkOutputs({ rerun, commandLine, force, files }, outputs) {
8042
- if (outputs === void 0 || force) return;
8043
- const found = await Promise.all(outputs.map((path) => files.exists(path)));
8044
- const existing = outputs.filter((_path, index) => found[index]);
8045
- if (existing.length === 0) return;
8046
- const them = existing.length === 1 ? "it" : "them";
8047
- throw new CliError({
8048
- code: OUTPUT_EXISTS,
8049
- message: `\`${commandLine}\` would overwrite ${existing.join(", ")}`,
8050
- hint: `once the user agrees to replace ${them}, run the Next step`,
8051
- next: [rerun({ add: [FORCE_FLAG.flag] })]
8052
- });
8053
- }
8054
- /** Guards the writes to declared outputs of `files`, whose paths resolve against `cwd`. */
8055
- function outputsGuard(files, cwd) {
8056
- const unwritten = /* @__PURE__ */ new Set();
8057
- let forceStep = [];
8058
- const exclusiveWrite = async (path, data, options) => {
8059
- const target = resolve(cwd, path);
8060
- unwritten.delete(target);
8061
- try {
8062
- await files.write(path, data, {
8063
- ...options,
8064
- exclusive: true
8065
- });
8066
- } catch (error) {
8067
- unwritten.add(target);
8068
- if (!(error instanceof CliError) || error.code !== "ALREADY_EXISTS") throw error;
8069
- throw new CliError({
8070
- code: ALREADY_EXISTS,
8071
- message: error.message,
8072
- hint: "once the user agrees to replace it, run the Next step",
8073
- next: [forceStep]
8074
- }, { cause: error });
8075
- }
8076
- };
8077
- return {
8078
- files: {
8079
- ...files,
8080
- write: (path, data, options) => unwritten.has(resolve(cwd, path)) ? exclusiveWrite(path, data, options) : files.write(path, data, options)
8081
- },
8082
- guard: (outputs, step) => {
8083
- forceStep = step;
8084
- for (const path of outputs) unwritten.add(resolve(cwd, path));
8085
- }
8086
- };
8087
- }
8088
- /**
8089
- * Creates each output folder and a temp file in it, so a sandbox denial fails before `apply`
8090
- * spends anything. The temp file's name is the Runtime's, so a crashed run's leftover is swept.
8091
- */
8092
- async function probeOutputs({ files, cwd, fileError }, outputs) {
8093
- const folders = new Set(outputs.map((path) => dirname(resolve(cwd, path))));
8094
- await Promise.all([...folders].map(async (folder) => {
8095
- const probe = tempPathOf(join(folder, "outputs-probe"));
8096
- try {
8097
- await files.mkdir(folder);
8098
- await files.write(probe, "");
8099
- await files.remove(probe);
8100
- } catch (error) {
8101
- throw fileError(error, {
8102
- path: folder,
8103
- access: "write"
8104
- });
8105
- }
8106
- }));
8107
- }
8108
- //#endregion
8109
- //#region ../agent-cli/dist/built-ins/skill/install-command.js
8110
- /** The project root and the Skill copy under it, as `skill install` writes it. */
8111
- async function skillCopyOf(cli, cwd) {
8112
- const root = await projectRootOf(cwd);
8113
- return {
8114
- root,
8115
- copy: join(root, ".claude", "skills", cli.name)
8116
- };
8117
- }
8118
- /** Every file under `dir`, relative to it and sorted; none when `dir` is missing. */
8119
- async function filesIn(dir) {
8120
- return (await orMissing(readdir(dir, {
8121
- recursive: true,
8122
- withFileTypes: true
8123
- })) ?? []).filter((entry) => entry.isFile()).map((entry) => relative(dir, join(entry.parentPath, entry.name))).sort();
8124
- }
8125
- /** The versions of the package's skill in `source` and of its copy in `copy`. */
8126
- async function skillVersionsOf(source, copy) {
8127
- const [shipped, installed] = await Promise.all([readFile(join(source, "SKILL.md"), "utf8"), orMissing(readFile(join(copy, "SKILL.md"), "utf8"))]);
8128
- const packageVersion = skillVersionIn(shipped) ?? null;
8129
- const copyVersion = installed === void 0 ? void 0 : skillVersionIn(installed) ?? null;
8130
- return {
8131
- packageVersion,
8132
- copyVersion,
8133
- same: copyVersion === packageVersion
8134
- };
8135
- }
8136
- const installSchema = object({
8137
- copy: string(),
8138
- written: array(string()),
8139
- removed: array(string()),
8140
- agentsMd: object({
8141
- path: string(),
8142
- changed: boolean()
8143
- }).nullable(),
8144
- handEdits: array(string())
8145
- });
8146
- async function sameContent(left, right) {
8147
- const [a, b] = await Promise.all([readFile(left), readFile(right)]);
8148
- return a.equals(b);
8149
- }
8150
- async function planOf(cli, folder, agents, cwd, packageRoot) {
8151
- const source = join(packageRoot, folder);
8152
- const { root, copy } = await skillCopyOf(cli, cwd);
8153
- const [shipped, present] = await Promise.all([filesIn(source), filesIn(copy)]);
8154
- const differs = await Promise.all(shipped.map(async (file) => present.includes(file) && !await sameContent(join(source, file), join(copy, file))));
8155
- return {
8156
- source,
8157
- copy,
8158
- write: shipped.filter((file) => !present.includes(file)),
8159
- replace: shipped.filter((_file, index) => differs[index]),
8160
- remove: present.filter((file) => !shipped.includes(file)),
8161
- sameVersion: (await skillVersionsOf(source, copy)).same,
8162
- agentsMd: agents === void 0 ? null : await agentsMdAt(agents, root),
8163
- handEdits: agents === void 0 ? [] : await handEditsAt(root)
8164
- };
8165
- }
8166
- const SECTION_VERBS = {
8167
- create: "create",
8168
- add: "add the Agents section to",
8169
- rewrite: "rewrite the Agents section in"
8170
- };
8171
- function renderPlan(cli, plan) {
8172
- const files = [
8173
- ...plan.write.map((file) => ` write ${file}`),
8174
- ...plan.replace.map((file) => ` replace ${file}`),
8175
- ...plan.remove.map((file) => ` remove ${file}`)
8176
- ];
8177
- const { agentsMd } = plan;
8178
- return [
8179
- files.length === 0 ? `the ${cli.name} skill at ${plan.copy} already matches the package` : `would copy the ${cli.name} skill to ${plan.copy}:`,
8180
- ...files,
8181
- ...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}`],
8182
- ...plan.handEdits.map((edit) => `by hand: ${edit}`)
8183
- ].join("\n");
8184
- }
8185
- function renderInstalled(cli, data) {
8186
- const { written, removed, agentsMd } = data;
8187
- return [
8188
- 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}`,
8189
- ...agentsMd === null ? [] : [agentsMd.changed ? `wrote the ${cli.name} Agents section in ${agentsMd.path}` : `${agentsMd.path} already holds the ${cli.name} Agents section`],
8190
- ...data.handEdits.map((edit) => `by hand: ${edit}`)
8191
- ].join("\n");
8192
- }
8193
- function editedRemovals(plan, ctx) {
8194
- const them = plan.remove.length === 1 ? "it" : "them";
8195
- return new CliError({
8196
- code: OUTPUT_EXISTS,
8197
- message: `skill install would remove ${plan.remove.map((file) => join(plan.copy, file)).join(", ")}`,
8198
- hint: `once the user agrees to remove ${them}, run the Next step`,
8199
- next: [ctx.rerun({ add: [FORCE_FLAG.flag] })]
8200
- });
8201
- }
8202
- /** The built-in `skill install`, for a CLI whose skill ships in its package. */
8203
- function skillInstallCommand(cli, folder, agents) {
8204
- return defineCommand$1({
8205
- 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",
8206
- examples: [{
8207
- command: `${cli.name} skill install`,
8208
- note: "install the skill this version ships"
8209
- }, {
8210
- command: `${cli.name} skill install --dry-run`,
8211
- note: "show what it would change"
8212
- }],
8213
- effect: "write",
8214
- output: installSchema,
8215
- force: "replace or remove hand-edited files of a skill copy at the package's version",
8216
- plan: (_input, ctx) => planOf(cli, folder, agents, ctx.cwd, ctx.packageRoot),
8217
- outputs: (plan) => plan.sameVersion ? [...plan.write, ...plan.replace].map((file) => join(plan.copy, file)) : [],
8218
- apply: async (plan, ctx) => {
8219
- if (plan.sameVersion && plan.remove.length > 0 && !ctx.force) throw editedRemovals(plan, ctx);
8220
- for (const file of [...plan.write, ...plan.replace]) await ctx.files.write(join(plan.copy, file), await readFile(join(plan.source, file)));
8221
- for (const file of plan.remove) await ctx.files.remove(join(plan.copy, file));
8222
- const { agentsMd } = plan;
8223
- if (agentsMd !== null && agentsMd.change !== null) {
8224
- const existing = await orMissing(readFile(agentsMd.path, "utf8")) ?? "";
8225
- await ctx.files.write(agentsMd.path, withSection(agentsMd.agents, existing));
8226
- }
8227
- return {
8228
- copy: plan.copy,
8229
- written: [...plan.write, ...plan.replace].sort(),
8230
- removed: [...plan.remove],
8231
- agentsMd: agentsMd === null ? null : {
8232
- path: agentsMd.path,
8233
- changed: agentsMd.change !== null
8234
- },
8235
- handEdits: [...plan.handEdits]
8236
- };
8237
- },
8238
- render: (data) => renderInstalled(cli, data),
8239
- renderPlan: (plan) => renderPlan(cli, plan)
8240
- });
8241
- }
8242
- //#endregion
8243
- //#region ../agent-cli/dist/built-ins/skill/packaged.js
8244
- async function personalCopyOf(name, copy, ctx) {
8245
- const home = join(homeOf(ctx.env), ".claude", "skills", name);
8246
- const real = await orMissing(realpath(home));
8247
- return real === void 0 || real === await orMissing(realpath(copy)) ? void 0 : home;
8248
- }
8249
- /** What the `skill` row of a Packaged skill carries under `--json` beside its message. */
8250
- const packagedSkillDetailsSchema = object({
8251
- copy: string(),
8252
- copyVersion: string().nullable(),
8253
- packageVersion: string().nullable(),
8254
- /** A personal copy, which Claude Code prefers over the project's. */
8255
- home: string().optional()
8256
- });
8257
- function versionLabel(version) {
8258
- return version ?? "(no metadata.version)";
8259
- }
8260
- function compared(name, { copy, copyVersion, packageVersion }, same) {
8261
- const stated = `${copy} ${versionLabel(copyVersion)}`;
8262
- return same ? `${stated}, the version ${name} ships` : `${stated} differs from the ${versionLabel(packageVersion)} that ${name} ships`;
8263
- }
8264
- /** The project root's copy against the package's, and any under home. */
8265
- async function findingOf(cli, folder, install, ctx) {
8266
- const { copy } = await skillCopyOf(cli, ctx.cwd);
8267
- const versions = await skillVersionsOf(join(ctx.packageRoot, folder), copy);
8268
- const { packageVersion } = versions;
8269
- const copyVersion = versions.copyVersion ?? null;
8270
- const home = await personalCopyOf(cli.name, copy, ctx);
8271
- const skill = {
8272
- copy,
8273
- copyVersion,
8274
- packageVersion,
8275
- ...home === void 0 ? {} : { home }
8276
- };
8277
- const head = versions.copyVersion === void 0 ? `no copy of the ${cli.name} skill at ${copy}` : compared(cli.name, skill, versions.same);
8278
- const message = home === void 0 ? head : `${head}; remove ${home}: Claude Code prefers a personal skill over the project's`;
8279
- if (!versions.same) return {
8280
- state: "problem",
8281
- message,
8282
- fix: `${install.runAs} skill install`,
8283
- skill
8284
- };
8285
- return home === void 0 ? {
8286
- state: "ok",
8287
- message,
8288
- skill
8289
- } : {
8290
- state: "problem",
8291
- message,
8292
- skill
8293
- };
8294
- }
8295
- async function notice(cli, folder, install, { builtIn, bin }, ctx) {
8296
- if (builtIn || bin === void 0 || cli.commands === void 0) return;
8297
- let finding;
8298
- try {
8299
- finding = await findingOf(cli, folder, install, ctx);
8300
- } catch (error) {
8301
- if (errnoOf(error) === void 0) throw error;
8302
- ctx.notice(`could not check the ${cli.name} skill: ${messageOf(error)}`);
8303
- return;
8304
- }
8305
- if (finding.state !== "problem") return;
8306
- ctx.notice(finding.fix === void 0 ? finding.message : `${finding.message}; run \`${finding.fix}\``);
8307
- }
8308
- /** A skill shipped in the package at `packaged`, which `skill install` copies to the project root. */
8309
- function packagedSkill(cli, { packaged: folder, agents: text }, install) {
8310
- const agents = text === void 0 ? void 0 : {
8311
- name: cli.name,
8312
- text
8313
- };
8314
- return {
8315
- kind: "packaged",
8316
- builtIns: { skill: { install: skillInstallCommand(cli, folder, agents) } },
8317
- rows: [{
8318
- name: "skill",
8319
- advisory: true,
8320
- run: (ctx) => findingOf(cli, folder, install, ctx)
8321
- }, ...agents === void 0 ? [] : [agentsRow(agents, install)]],
8322
- notice: (launch, ctx) => notice(cli, folder, install, launch, ctx),
8323
- differing: async ({ cwd, packageRoot }) => {
8324
- const { copy } = await skillCopyOf(cli, cwd);
8325
- const { copyVersion, packageVersion, same } = await skillVersionsOf(join(packageRoot, folder), copy);
8326
- return copyVersion === void 0 || same ? [] : [compared(cli.name, {
8327
- copy,
8328
- copyVersion,
8329
- packageVersion
8330
- }, false)];
8331
- },
8332
- nextAt: async (_version, ctx) => {
8333
- const { fix } = await findingOf(cli, folder, install, ctx);
8334
- return fix === void 0 ? [] : [fix];
8335
- }
8336
- };
8337
- }
8338
- //#endregion
8339
- //#region ../agent-cli/dist/built-ins/skill/skill.js
8340
- /** A Packaged skill, or else a Skill Hub's, also for a CLI that declares no `skill`. */
8341
- function skillOf$1(cli, install) {
8342
- const { skill } = cli;
8343
- return skill !== void 0 && "packaged" in skill ? packagedSkill(cli, skill, install) : hubSkill(cli, skill);
8344
- }
8345
- //#endregion
8346
- //#region ../agent-cli/dist/context/interrupts.js
8347
- const INTERRUPTED = "INTERRUPTED";
8348
- const DEADLINE_EXCEEDED = "DEADLINE_EXCEEDED";
8349
- /** How long an interrupted Command may take to settle before the run ends without it. */
8350
- const GRACE_MS = 3e3;
8351
- const GRACE_OVER = Symbol("grace over");
8352
- function interruptedError(rerun) {
8353
- return new CliError({
8354
- code: INTERRUPTED,
8355
- message: "interrupted before it finished",
8356
- hint: "to finish it, run the Next step, unless it was stopped on purpose; if a tool timeout stopped it, give it longer",
8357
- next: [rerun]
8358
- });
8359
- }
8360
- const BOUND_WORDS = {
8361
- deadline: "deadline",
8362
- ignoreStop: "timeout"
8363
- };
8364
- function waitHint(bound) {
8365
- 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";
8366
- }
8367
- function deadlineError(commandLine, ms, advice, bound = "deadline") {
8368
- return new CliError({
8369
- code: DEADLINE_EXCEEDED,
8370
- message: `${commandLine} ran past its ${ms} ms ${BOUND_WORDS[bound]}`,
8371
- ...advice
8372
- });
8373
- }
8374
- /** One Command run's cancellation: its signal, its deadlines and the steps it must finish. */
8375
- var RunControl = class {
8376
- signal;
8377
- #controller = new AbortController();
8378
- #commandLine;
8379
- #rerun;
8380
- #outer;
8381
- #onOuterAbort = () => this.#controller.abort(interruptedError(this.#rerun));
8382
- /** Each running must-finish step, tracked until it settles, with its name. */
8383
- #steps = /* @__PURE__ */ new Map();
8384
- #progress;
8385
- #clock;
8386
- #disposed = new AbortController();
8387
- /** The reasons this run's deadlines aborted with, to tell them in a Command's error. */
8388
- #abortReasons = /* @__PURE__ */ new WeakSet();
8389
- #graceCancel = new AbortController();
8390
- #endGrace = () => {};
8391
- #graceOver = new Promise((resolve) => {
8392
- this.#endGrace = resolve;
8393
- });
8394
- constructor({ commandLine, rerun, outer, clock, progress, runDeadline }) {
8395
- this.#commandLine = commandLine;
8396
- this.#rerun = rerun;
8397
- this.#outer = outer;
8398
- this.#clock = clock;
8399
- this.#progress = progress;
8400
- this.signal = this.#controller.signal;
8401
- this.signal.addEventListener("abort", () => this.#onStop(), { once: true });
8402
- if (outer?.aborted === true) this.#onOuterAbort();
8403
- else outer?.addEventListener("abort", this.#onOuterAbort, { once: true });
8404
- if (runDeadline !== void 0) {
8405
- const { ms, advice } = runDeadline;
8406
- after(clock, ms, this.#disposed.signal, () => this.#controller.abort(deadlineError(commandLine, ms, advice())));
8407
- }
8408
- }
8409
- deadline(ms, options = {}) {
8410
- return options.ignoreStop === true ? this.#abortsAfter(ms, "ignoreStop") : AbortSignal.any([this.signal, this.#abortsAfter(ms, "deadline")]);
8411
- }
8412
- #abortsAfter(ms, bound) {
8413
- const controller = new AbortController();
8414
- after(this.#clock, ms, this.#disposed.signal, () => {
8415
- const advice = {
8416
- hint: waitHint(bound),
8417
- next: [this.#rerun]
8418
- };
8419
- const reason = deadlineError(this.#commandLine, ms, advice, bound);
8420
- this.#abortReasons.add(reason);
8421
- controller.abort(reason);
8422
- });
8423
- return controller.signal;
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;
8424
7776
  }
8425
7777
  async runToEnd(name, work) {
8426
7778
  this.#graceCancel.abort();
@@ -8492,52 +7844,185 @@ const NOT_GLOBAL_INSTALL = "NOT_GLOBAL_INSTALL";
8492
7844
  function globalModulesDir(prefix) {
8493
7845
  return process.platform === "win32" ? join(prefix, "node_modules") : join(prefix, "lib", "node_modules");
8494
7846
  }
8495
- function notGlobal(ctx, message) {
8496
- return runtimeError({
8497
- code: NOT_GLOBAL_INSTALL,
8498
- message,
8499
- hint: "install it with npm by running the Next step, so its update Command can replace it",
8500
- next: [wholeLine(installCommandLine(ctx.registry))]
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))]
7853
+ });
7854
+ }
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`);
7867
+ return {
7868
+ prefix,
7869
+ packageDir
7870
+ };
7871
+ }
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}`];
7878
+ }
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;
7885
+ return {
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}`
7892
+ };
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
7900
+ /** Every file under `dir`, relative to it and sorted; none when `dir` is missing. */
7901
+ async function filesIn(dir) {
7902
+ return (await orMissing(readdir(dir, {
7903
+ recursive: true,
7904
+ withFileTypes: true
7905
+ })) ?? []).filter((entry) => entry.isFile()).map((entry) => relative(dir, join(entry.parentPath, entry.name))).sort();
7906
+ }
7907
+ async function sameContent(left, right) {
7908
+ const [a, b] = await Promise.all([readFile(left), readFile(right)]);
7909
+ return a.equals(b);
7910
+ }
7911
+ async function syncOf(source, copy) {
7912
+ const [shipped, present] = await Promise.all([filesIn(source), filesIn(copy)]);
7913
+ const differs = await Promise.all(shipped.map(async (file) => present.includes(file) && !await sameContent(join(source, file), join(copy, file))));
7914
+ return {
7915
+ source,
7916
+ copy,
7917
+ write: shipped.filter((file) => !present.includes(file)),
7918
+ replace: shipped.filter((_file, index) => differs[index]),
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;
8501
7950
  });
8502
7951
  }
7952
+ const homeSyncSchema = object({
7953
+ copies: array(string()),
7954
+ written: array(string()),
7955
+ removed: array(string())
7956
+ });
8503
7957
  /**
8504
- * The global npm install this copy runs from, found by asking npm for its global prefix.
8505
- * `bin` is the script that started the CLI; throws `NOT_GLOBAL_INSTALL` when it is not inside that install.
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.
8506
7960
  */
8507
- async function globalInstallOf(bin, ctx) {
8508
- if (bin === void 0) throw notGlobal(ctx, `${ctx.cliName} runs from source, not from an npm install`);
8509
- const prefix = await globalPrefix(ctx);
8510
- const packageDir = join(globalModulesDir(prefix), ctx.registry.package);
8511
- const installed = await orMissing(realpath(packageDir));
8512
- const here = await orMissing(realpath(bin)) ?? bin;
8513
- if (installed === void 0 || !isInside(installed, here)) throw notGlobal(ctx, `${ctx.cliName} runs from ${here}, not from npm's global install at ${packageDir}`);
8514
- if ((await lstat(packageDir)).isSymbolicLink()) throw notGlobal(ctx, `${ctx.cliName} runs from a link to ${installed} (npm link), not from an npm install`);
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);
8515
7968
  return {
8516
- prefix,
8517
- packageDir
7969
+ copies,
7970
+ written: syncs.flatMap((sync) => [...sync.write, ...sync.replace]),
7971
+ removed: syncs.flatMap((sync) => sync.remove)
8518
7972
  };
8519
7973
  }
8520
- const NPMJS = "https://registry.npmjs.org/";
8521
- /** The flag that points npm at the CLI's registry; none for npmjs, npm's default. */
8522
- function registryFlags({ package: name, registry }) {
8523
- if (registry === NPMJS) return [];
8524
- const scope = scopeOf(name);
8525
- return [scope === void 0 ? `--registry=${registry}` : `--${scope}:registry=${registry}`];
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`];
8526
7977
  }
8527
- function globalRegistry(install) {
8528
- const { package: name, registry } = install;
8529
- return {
8530
- package: name,
8531
- label: registry,
8532
- flags: registryFlags(install),
8533
- unreachableHint: "check the network or VPN",
8534
- loginHint: `log in to ${registry} (\`npm login --registry=${registry}\`) or set its token in ~/.npmrc`,
8535
- missingHint: `check that ${name} is published on ${registry}`
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`
8536
8014
  };
8537
8015
  }
8538
- /** npm for a Global install, run in the working directory. */
8539
- function globalNpm(cli, install, ctx, node) {
8540
- return npmContext(cli, globalRegistry(install), ctx, node, ctx.cwd);
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
+ };
8541
8026
  }
8542
8027
  //#endregion
8543
8028
  //#region ../agent-cli/dist/built-ins/upgrades.js
@@ -8604,6 +8089,147 @@ async function upgradesOf(packageDir, from, to) {
8604
8089
  }
8605
8090
  return releasesIn(changelog).filter(({ version }) => compareVersions(version, from) > 0 && compareVersions(version, to) <= 0).sort((a, b) => compareVersions(a.version, b.version)).flatMap(upgradeOf);
8606
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))
8122
+ };
8123
+ }
8124
+ function needsInstall({ from, latest }) {
8125
+ return from === null || standingOf$1(from, latest) === "behind";
8126
+ }
8127
+ function binDirOf(prefix) {
8128
+ return process.platform === "win32" ? prefix : join(prefix, "bin");
8129
+ }
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: []
8143
+ });
8144
+ }
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 }) {
8170
+ return defineCommand$1({
8171
+ description: "install this CLI and its skill on this machine",
8172
+ examples: [{
8173
+ command: `${cli.name} setup`,
8174
+ note: "install the latest version and the skill"
8175
+ }, {
8176
+ command: `${cli.name} setup --dry-run`,
8177
+ note: "show what it would change"
8178
+ }],
8179
+ effect: "write",
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
+ });
8191
+ return {
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)
8198
+ };
8199
+ },
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")
8231
+ });
8232
+ }
8607
8233
  //#endregion
8608
8234
  //#region ../agent-cli/dist/built-ins/update.js
8609
8235
  const updateSchema = object({
@@ -8613,34 +8239,30 @@ const updateSchema = object({
8613
8239
  version: string(),
8614
8240
  prefix: string()
8615
8241
  }).nullable(),
8616
- upgrades: array(upgradeSchema),
8617
- next: array(string())
8242
+ skill: homeSyncSchema.nullable(),
8243
+ upgrades: array(upgradeSchema)
8618
8244
  });
8619
8245
  function nothingToInstall({ from, latest }) {
8620
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`;
8621
8247
  }
8622
- function renderUpgrade(upgrade) {
8623
- return "steps" in upgrade ? [`\nupgrade steps for ${upgrade.version}:`, ...upgrade.steps.map((step) => `- ${step}`)].join("\n") : `\nchanges in ${upgrade.version}:\n${upgrade.notes}`;
8624
- }
8625
8248
  function renderUpdate(cli, install, update) {
8626
8249
  const { installed } = update;
8627
- const head = installed === null ? [nothingToInstall(update)] : [`installed ${install.package}@${installed.version} with the npm under ${installed.prefix}, replacing ${update.from}`, ...update.upgrades.map(renderUpgrade)];
8628
- const next = update.next.map((command) => `next: ${command}, so the ${cli.name} skill describes ${update.latest}`);
8629
- return (next.length === 0 ? head : [
8630
- ...head,
8631
- "",
8632
- ...next
8633
- ]).join("\n");
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");
8634
8256
  }
8635
8257
  function interruptedAfterInstall(plan) {
8636
8258
  return new CliError({
8637
8259
  code: INTERRUPTED,
8638
8260
  message: `interrupted, but ${plan.package}@${plan.latest} had already been installed with the npm under ${plan.prefix}, replacing ${plan.from}`,
8639
- hint: "nothing to run again: run the commands under next"
8261
+ hint: "nothing to run again"
8640
8262
  });
8641
8263
  }
8642
8264
  /** The built-in `update`, for a CLI installed globally with npm. */
8643
- function updateCommand(cli, install, skill, { bin, node }) {
8265
+ function updateCommand(cli, install, { bin, node }) {
8644
8266
  return defineCommand$1({
8645
8267
  description: "install the latest version of this CLI from its registry",
8646
8268
  examples: [{
@@ -8660,40 +8282,217 @@ function updateCommand(cli, install, skill, { bin, node }) {
8660
8282
  from: cli.version,
8661
8283
  latest: await latestVersion(npm),
8662
8284
  prefix: copy.prefix,
8663
- packageDir: copy.packageDir
8285
+ packageDir: copy.packageDir,
8286
+ copies: cli.skill === void 0 ? [] : await homeCopiesOf(cli.name, ctx.env)
8287
+ };
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)
8664
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";
8337
+ }
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;
8353
+ }
8354
+ }
8355
+ async function writeWithMode(path, data, mode) {
8356
+ if (mode === void 0) {
8357
+ await writeFile(path, data);
8358
+ return;
8359
+ }
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);
8376
+ }
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;
8388
+ }
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);
8401
+ }
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
+ }
8665
8435
  },
8666
- apply: async (plan, ctx) => {
8667
- const { from, latest, prefix } = plan;
8668
- if (standingOf$1(from, latest) !== "behind") return {
8669
- from,
8670
- latest,
8671
- installed: null,
8672
- upgrades: [],
8673
- next: await skill.nextAt(latest, ctx)
8674
- };
8675
- ctx.signal.throwIfAborted();
8676
- ctx.progress(`installing ${plan.package}@${latest} with the npm under ${prefix}…`);
8677
- await ctx.runToEnd(`installing ${plan.package}@${latest}`, () => installGlobally(latest, globalNpm(cli, install, ctx, node)));
8678
- const update = {
8679
- from,
8680
- latest,
8681
- installed: {
8682
- version: latest,
8683
- prefix
8684
- },
8685
- upgrades: await upgradesOf(plan.packageDir, from, latest),
8686
- next: await skill.nextAt(latest, ctx)
8687
- };
8688
- if (ctx.signal.aborted) {
8689
- ctx.deliver(update);
8690
- throw interruptedAfterInstall(plan);
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;
8691
8450
  }
8692
- return update;
8451
+ await mkdir(dirname(path), {
8452
+ recursive: true,
8453
+ mode
8454
+ });
8455
+ await mkdir(path, { mode });
8693
8456
  },
8694
- render: (update) => renderUpdate(cli, install, update),
8695
- 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)
8696
- });
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
+ });
8485
+ }
8486
+ }
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
+ };
8697
8496
  }
8698
8497
  //#endregion
8699
8498
  //#region ../agent-cli/dist/built-ins/install/release-check.js
@@ -8807,78 +8606,310 @@ async function releaseCheck(cli, install, runAs, launch, ctx) {
8807
8606
  debugLine(cli, ctx, messageOf(error));
8808
8607
  }
8809
8608
  }
8810
- async function checkAndTell(cli, install, runAs, launch, ctx) {
8811
- const file = cacheFileOf(cli.name, ctx.env);
8812
- const last = await lastCheck(file);
8813
- if (last?.error !== void 0) debugLine(cli, ctx, last.error);
8814
- if (last === void 0 || ctx.clock.now() - last.checkedAt >= CHECK_INTERVAL_MS) await startCheck(cli, install, file, last, launch, ctx);
8815
- const latest = last?.latest;
8816
- if (latest === void 0 || !isVersion(latest) || compareVersions(latest, cli.version) <= 0) return;
8817
- ctx.notice(`${cli.name} ${latest} is out, this is ${cli.version}; run \`${runAs} update\``);
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 }) {
8621
+ return {
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
+ }
8645
+ };
8646
+ }
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;
8660
+ }
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
+ });
8669
+ return {
8670
+ runAs: runAsOf(cli),
8671
+ builtIns: () => ({}),
8672
+ exitClasses: [],
8673
+ check: () => ({
8674
+ install: skipped,
8675
+ release: async () => ({ skipped: await skipped() })
8676
+ }),
8677
+ notice: noNotice
8678
+ };
8679
+ }
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);
8684
+ }
8685
+ //#endregion
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
+ };
8692
+ }
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`;
8702
+ }
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
+ };
8721
+ }
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
+ };
8758
+ }
8759
+ //#endregion
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()
8776
+ }).nullable(),
8777
+ handEdits: array(string())
8778
+ });
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
+ };
8786
+ }
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");
8818
8805
  }
8819
- //#endregion
8820
- //#region ../agent-cli/dist/built-ins/install/global.js
8821
- function checkOf(cli, install, { bin, node }) {
8822
- return {
8823
- install: async (ctx) => {
8824
- const npm = globalNpm(cli, install, ctx, node);
8825
- try {
8826
- return {
8827
- state: "ok",
8828
- message: `a global npm install under ${(await globalInstallOf(bin, npm)).prefix}`
8829
- };
8830
- } catch (error) {
8831
- if (error instanceof CliError && error.code === "NOT_GLOBAL_INSTALL") return {
8832
- state: "problem",
8833
- message: error.message,
8834
- fix: installCommandLine(npm.registry)
8835
- };
8836
- throw error;
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");
8813
+ }
8814
+ /** The built-in `skill install`, for a CLI whose skill ships in its package. */
8815
+ function skillInstallCommand(cli, folder, agents) {
8816
+ return defineCommand$1({
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",
8818
+ examples: [{
8819
+ command: `${cli.name} skill install`,
8820
+ note: "install the skill this version ships"
8821
+ }, {
8822
+ command: `${cli.name} skill install --dry-run`,
8823
+ note: "show what it would change"
8824
+ }],
8825
+ effect: "write",
8826
+ output: installSchema,
8827
+ plan: (_input, ctx) => planOf(cli, folder, agents, ctx.cwd, ctx.packageRoot),
8828
+ apply: async (plan, ctx) => {
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));
8837
8834
  }
8838
- },
8839
- release: async (ctx) => {
8840
- const fix = `${cli.name} update`;
8841
8835
  return {
8842
- npm: globalNpm(cli, install, ctx, node),
8843
- advise: () => ({ fix })
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
8842
+ },
8843
+ handEdits: [...plan.handEdits]
8844
8844
  };
8845
- }
8846
- };
8845
+ },
8846
+ render: (data) => renderInstalled(cli, data),
8847
+ renderPlan: (plan) => renderPlan(cli, plan)
8848
+ });
8847
8849
  }
8848
- /** The Install of a CLI installed once per machine with npm, which `update` replaces. */
8849
- function globalInstall(cli, install) {
8850
- const adapter = {
8851
- runAs: runAsOf(cli),
8852
- builtIns: (launch) => ({ update: updateCommand(cli, install, skillOf$1(cli, adapter), launch) }),
8853
- exitClasses: ["source"],
8854
- check: (launch) => checkOf(cli, install, launch),
8855
- notice: (launch, ctx) => releaseCheck(cli, install, adapter.runAs, launch, ctx)
8850
+ //#endregion
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;
8856
+ }
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
8856
8878
  };
8857
- return adapter;
8858
8879
  }
8859
- //#endregion
8860
- //#region ../agent-cli/dist/built-ins/install/install.js
8861
- const noNotice = async () => {};
8862
- function noInstall(cli) {
8863
- const skipped = async () => ({
8864
- state: "skipped",
8865
- message: `not checked: ${cli.name} declares no install`
8866
- });
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
+ };
8867
8886
  return {
8868
- runAs: runAsOf(cli),
8869
- builtIns: () => ({}),
8870
- exitClasses: [],
8871
- check: () => ({
8872
- install: skipped,
8873
- release: async () => ({ skipped: await skipped() })
8874
- }),
8875
- notice: noNotice
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)]]
8876
8893
  };
8877
8894
  }
8878
- function installOf(cli) {
8879
- const { install } = cli;
8880
- if (install === void 0) return noInstall(cli);
8881
- 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);
8882
8913
  }
8883
8914
  //#endregion
8884
8915
  //#region ../agent-cli/dist/built-ins/doctor-rows.js
@@ -9015,7 +9046,7 @@ function doctorChecks(cli, launch, scope) {
9015
9046
  ...secretRows(cli.secrets ?? [], cli.commands),
9016
9047
  installRow(check),
9017
9048
  registryRow(cli, check),
9018
- ...skillOf$1(cli, install).rows
9049
+ ...skillOf(cli, install).rows
9019
9050
  ],
9020
9051
  tools: new Set(tools.map(({ name }) => name))
9021
9052
  };
@@ -9033,8 +9064,7 @@ const reportSchema = object({
9033
9064
  ]),
9034
9065
  message: string(),
9035
9066
  fix: string().optional(),
9036
- code: string().optional(),
9037
- skill: packagedSkillDetailsSchema.optional()
9067
+ code: string().optional()
9038
9068
  })),
9039
9069
  next: array(string())
9040
9070
  });
@@ -9169,15 +9199,15 @@ function doctorCommand(cli, launch) {
9169
9199
  //#endregion
9170
9200
  //#region ../agent-cli/dist/built-ins/commands.js
9171
9201
  /**
9172
- * The Commands the Runtime adds to a multi-Command CLI: `update` only for a Global install, and
9173
- * `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.
9174
9204
  */
9175
9205
  function builtInCommands(cli, launch) {
9176
9206
  const install = installOf(cli);
9177
9207
  return {
9178
9208
  doctor: doctorCommand(cli, launch),
9179
9209
  ...install.builtIns(launch),
9180
- ...skillOf$1(cli, install).builtIns
9210
+ ...skillOf(cli, install).builtIns
9181
9211
  };
9182
9212
  }
9183
9213
  //#endregion
@@ -9314,6 +9344,7 @@ const RUNTIME_EXIT_CLASSES = {
9314
9344
  PROJECT_INVALID: "operator",
9315
9345
  DOCTOR_PROBLEMS: "operator",
9316
9346
  NOT_GLOBAL_INSTALL: "operator",
9347
+ NOT_ON_PATH: "operator",
9317
9348
  REGISTRY_UNREACHABLE: "operator",
9318
9349
  REGISTRY_AUTH: "operator",
9319
9350
  INSTALL_FAILED: "operator",
@@ -9387,20 +9418,22 @@ function checkTree(commands, path, table) {
9387
9418
  else checkTree(node, at, table);
9388
9419
  }
9389
9420
  }
9390
- function checkSkillAdd({ name, skill }) {
9391
- if (skill === void 0 || !("add" in skill)) return;
9392
- const declares = `${name} declares skill.add ${JSON.stringify(skill.add)}`;
9393
- if (skill.add.trim() === "") throw new Error(`${declares}, which is empty`);
9394
- 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`);
9395
9427
  }
9396
9428
  function checkCli(cli) {
9397
9429
  const table = errorTableOf(cli);
9398
- checkSkillAdd(cli);
9430
+ checkDocs(cli);
9399
9431
  if (cli.commands === void 0) {
9400
9432
  checkCommand(cli.command, commandLabel([], cli.name).join(" "), table);
9401
- 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`);
9402
9434
  return cli;
9403
9435
  }
9436
+ checkSkill(cli);
9404
9437
  const { commands } = cli;
9405
9438
  checkTree(commands, [], table);
9406
9439
  const taken = Object.keys(builtInCommands(cli, {
@@ -9504,30 +9537,6 @@ function startSideTask(sideTask, runWith, { clock, failed }) {
9504
9537
  };
9505
9538
  }
9506
9539
  //#endregion
9507
- //#region ../agent-cli/dist/built-ins/skill/usage-hint.js
9508
- const SKILL_USAGE_CODES = /* @__PURE__ */ new Set([
9509
- UNKNOWN_OPTION,
9510
- UNKNOWN_COMMAND,
9511
- INVALID_INPUT
9512
- ]);
9513
- /** A usage error whose hint names each Skill copy that differs from this build, since one may have named the input. */
9514
- async function withDifferingSkill(error, cli, place, runAs) {
9515
- if (!(error instanceof UsageError) || !SKILL_USAGE_CODES.has(error.code)) return error;
9516
- let differing;
9517
- try {
9518
- differing = await skillOf$1(cli, installOf(cli)).differing(place);
9519
- } catch (lookup) {
9520
- if (errnoOf(lookup) === void 0) throw lookup;
9521
- return error;
9522
- }
9523
- if (differing.length === 0) return error;
9524
- return new UsageError({
9525
- code: error.code,
9526
- message: error.message,
9527
- 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`
9528
- });
9529
- }
9530
- //#endregion
9531
9540
  //#region ../agent-cli/dist/command-line/suggest.js
9532
9541
  const MAX_DISTANCE = 3;
9533
9542
  const MIN_SIMILARITY = .4;
@@ -10279,7 +10288,11 @@ function helpResult(node, root, cli, { style, json }) {
10279
10288
  }
10280
10289
  function versionResult(cli, packageRoot) {
10281
10290
  return {
10282
- 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`,
10283
10296
  stderr: "",
10284
10297
  exitCode: SUCCESS_EXIT_CODE
10285
10298
  };
@@ -10346,6 +10359,77 @@ function checkOperator({ operator, commandLine }, env, rerun) {
10346
10359
  });
10347
10360
  }
10348
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
10349
10433
  //#region ../agent-cli/dist/context/spill.js
10350
10434
  const STDOUT_BUDGET_CHARS = 3e4;
10351
10435
  const SPILL_RETENTION_MS = 6048e5;
@@ -11238,11 +11322,7 @@ async function runCommand(cli, routed, options, write, baseFormat) {
11238
11322
  });
11239
11323
  });
11240
11324
  } catch (error) {
11241
- return failed(await withDifferingSkill(error, cli, {
11242
- cwd,
11243
- env,
11244
- packageRoot
11245
- }, invocation.runAs), noSideTask);
11325
+ return failed(error, noSideTask);
11246
11326
  }
11247
11327
  const { input, flags } = parsed;
11248
11328
  resumed = command.deadline?.resume(input, rerunOf);
@@ -11374,9 +11454,7 @@ function once(build) {
11374
11454
  };
11375
11455
  }
11376
11456
  async function noticesOf(cli, launch, ctx) {
11377
- const install = installOf(cli);
11378
- await skillOf$1(cli, install).notice(launch, ctx);
11379
- await install.notice(launch, ctx);
11457
+ await installOf(cli).notice(launch, ctx);
11380
11458
  }
11381
11459
  //#endregion
11382
11460
  //#region ../agent-cli/dist/style.js
@@ -11460,16 +11538,10 @@ async function routeAndRun(cli, argv, options, write, stopNotice) {
11460
11538
  ...format,
11461
11539
  json: routed.json
11462
11540
  }));
11463
- case "usage error": {
11464
- const place = {
11465
- ...options,
11466
- packageRoot: packageRootOf(options.bin, options.cwd)
11467
- };
11468
- return ranAlone(errorResult(await withDifferingSkill(routed.error, cli, place, runAs), {
11469
- ...misused(routed.node),
11470
- json: routed.json
11471
- }));
11472
- }
11541
+ case "usage error": return ranAlone(errorResult(routed.error, {
11542
+ ...misused(routed.node),
11543
+ json: routed.json
11544
+ }));
11473
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)));
11474
11546
  }
11475
11547
  }
@@ -11532,7 +11604,7 @@ async function runProcess(cli) {
11532
11604
  //#region package.json
11533
11605
  var package_default = {
11534
11606
  name: "@duyquangnvx/create-agent-cli",
11535
- version: "0.23.0",
11607
+ version: "0.24.0",
11536
11608
  description: "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
11537
11609
  type: "module",
11538
11610
  engines: { "node": ">=22.12.0" },
@@ -11581,80 +11653,35 @@ function fill(text, values, quoted) {
11581
11653
  //#region src/install-mode.ts
11582
11654
  const PARTIALS_DIR = fileURLToPath(new URL("../partials/", import.meta.url));
11583
11655
  const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11584
- const COMMANDS_ANCHOR = " commands: {";
11585
- const GLOBAL_INSTALL = ` install: "global",\n`;
11586
- 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 `! `. -->";
11587
- function skillHubOf({ git, api, add }) {
11588
- if (git !== void 0 && api !== void 0) throw new UsageError({
11589
- code: "SKILL_HOMES_CONFLICT",
11590
- message: "--skill-hub-git and --skill-hub-api each name the Skill Hub the skill goes to",
11591
- hint: "pass one of them"
11592
- });
11593
- if (api !== void 0 && add === void 0) throw new UsageError({
11594
- code: "SKILL_ADD_MISSING",
11595
- message: "--skill-hub-api needs --skill-add, the whole line that installs the skill from the hub",
11596
- hint: "add --skill-add with the hub's install line for the skill"
11597
- });
11598
- if (add !== void 0 && api === void 0) throw new UsageError({
11599
- code: "SKILL_HUB_API_MISSING",
11600
- message: "--skill-add is the install line of an API Skill Hub, which --skill-hub-api names",
11601
- hint: "add --skill-hub-api with the hub's URL"
11602
- });
11603
- if (add !== void 0 && (add.trim() === "" || /[\r\n]/.test(add))) throw new UsageError({
11604
- code: "SKILL_ADD_INVALID",
11605
- message: `--skill-add ${JSON.stringify(add)} is not one command line`,
11606
- hint: "pass the hub's install line for the skill, on one line"
11607
- });
11608
- if (git !== void 0) return {
11609
- kind: "git",
11610
- url: git
11611
- };
11612
- return api === void 0 || add === void 0 ? void 0 : {
11613
- kind: "api",
11614
- url: api,
11615
- add
11616
- };
11617
- }
11618
- function installModeOf(flags, dependency, binName) {
11619
- const skillHub = skillHubOf(flags);
11620
- if (skillHub !== void 0 && dependency) throw new UsageError({
11621
- code: "SKILL_HOMES_CONFLICT",
11622
- message: `--skill-hub-${skillHub.kind} and --dependency each name where the skill comes from: a Dependency install ships it inside the package`,
11623
- 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`
11624
- });
11625
- 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({
11626
11659
  code: "SKILL_NAME_INVALID",
11627
11660
  message: `${binName} cannot name a skill, whose names are lowercase letters, digits and single hyphens`,
11628
- 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"
11629
11662
  });
11630
- return dependency ? { mode: "dependency" } : {
11631
- mode: "global",
11632
- skillHub
11633
- };
11634
- }
11635
- function skillLineOf(skillHub, binName) {
11636
- if (skillHub === void 0) return NO_SKILL_HUB;
11637
- return skillHub.kind === "git" ? `\`! npx -y skills add ${skillHub.url} --skill ${binName} -g\`` : `\`! ${skillHub.add}\``;
11663
+ return dependency ? "dependency" : "global";
11638
11664
  }
11639
11665
  async function partialsOf(mode, values) {
11640
11666
  const partial = async (name) => fill(await readFile(join(PARTIALS_DIR, mode, `${name}.md`), "utf8"), values, false).trimEnd();
11641
- const [installLines, skillCheck, installSteps, updateSteps, sandbox] = await Promise.all([
11667
+ const [installLines, skillCheck, installSteps, updateSteps, sandbox, whenMissing] = await Promise.all([
11642
11668
  partial("installLines"),
11643
11669
  partial("skillCheck"),
11644
11670
  partial("installSteps"),
11645
11671
  partial("updateSteps"),
11646
- partial("sandbox")
11672
+ partial("sandbox"),
11673
+ partial("whenMissing")
11647
11674
  ]);
11648
11675
  return {
11649
11676
  installLines,
11650
11677
  skillCheck,
11651
11678
  installSteps,
11652
11679
  updateSteps,
11653
- sandbox
11680
+ sandbox,
11681
+ whenMissing
11654
11682
  };
11655
11683
  }
11656
- async function globalSetup(skillHub, packageName, binName) {
11657
- const skillLine = skillLineOf(skillHub, binName);
11684
+ async function globalSetup(packageName, binName) {
11658
11685
  const runAs = runAsOf({ name: binName });
11659
11686
  return {
11660
11687
  values: {
@@ -11664,24 +11691,16 @@ async function globalSetup(skillHub, packageName, binName) {
11664
11691
  packageName,
11665
11692
  binName,
11666
11693
  runAs,
11667
- skillLine
11694
+ setupLine: setupLineOf({
11695
+ mode: "global",
11696
+ package: packageName,
11697
+ registry: TEMPLATE_REGISTRY
11698
+ })
11668
11699
  })
11669
11700
  },
11670
- edits: skillHub === void 0 ? [] : [{
11671
- file: "src/cli.ts",
11672
- find: COMMANDS_ANCHOR,
11673
- replace: ` skill: ${skillOf(skillHub)},\n${COMMANDS_ANCHOR}`
11674
- }, {
11675
- file: "scripts/release.ts",
11676
- find: " smoke: {",
11677
- replace: ` skillHub: { ${skillHub.kind}: ${JSON.stringify(skillHub.url)} },\n smoke: {`
11678
- }],
11679
- files: []
11701
+ edits: []
11680
11702
  };
11681
11703
  }
11682
- function skillOf(skillHub) {
11683
- return skillHub.kind === "git" ? `{ source: ${JSON.stringify(skillHub.url)} }` : `{ add: ${JSON.stringify(skillHub.add)} }`;
11684
- }
11685
11704
  async function dependencySetup(packageName, binName) {
11686
11705
  const runAs = runAsOf({
11687
11706
  name: binName,
@@ -11702,18 +11721,13 @@ async function dependencySetup(packageName, binName) {
11702
11721
  },
11703
11722
  edits: [{
11704
11723
  file: "src/cli.ts",
11705
- find: GLOBAL_INSTALL,
11724
+ find: ` install: "global",\n`,
11706
11725
  replace: ` install: "dependency",\n`
11707
- }, {
11708
- file: "src/cli.ts",
11709
- find: COMMANDS_ANCHOR,
11710
- replace: ` skill: { packaged: "skills/${binName}" },\n${COMMANDS_ANCHOR}`
11711
- }],
11712
- files: ["skills"]
11726
+ }]
11713
11727
  };
11714
11728
  }
11715
11729
  async function installSetupOf(install, packageName, binName) {
11716
- return install.mode === "global" ? globalSetup(install.skillHub, packageName, binName) : dependencySetup(packageName, binName);
11730
+ return install === "global" ? globalSetup(packageName, binName) : dependencySetup(packageName, binName);
11717
11731
  }
11718
11732
  //#endregion
11719
11733
  //#region src/steps.ts
@@ -11866,9 +11880,6 @@ async function editManifest(root, edit) {
11866
11880
  async function setRepository(root, repository) {
11867
11881
  await editManifest(root, (entries) => entries.flatMap((entry) => entry[0] === "description" ? [entry, ["repository", repository]] : [entry]));
11868
11882
  }
11869
- async function addFiles(root, files) {
11870
- await editManifest(root, (entries) => entries.map(([key, value]) => key === "files" && Array.isArray(value) ? [key, [...value, ...files]] : [key, value]));
11871
- }
11872
11883
  async function ensureFree(path) {
11873
11884
  if (!await isFree(path)) throw new UsageError({
11874
11885
  code: "TARGET_NOT_EMPTY",
@@ -11894,18 +11905,11 @@ await runProcess(defineCli({
11894
11905
  install: boolean().default(true).describe("run pnpm install"),
11895
11906
  git: boolean().default(true).describe("create a git repository with an initial commit"),
11896
11907
  repository: url({ protocol: /^https$/ }).optional().describe("the https URL of the CLI's git repository"),
11897
- 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"),
11898
- skillHubApi: url({ protocol: /^https$/ }).optional().describe("the https URL of an API Skill Hub each release publishes the CLI's skill to"),
11899
- skillAdd: string().optional().describe("the whole line that installs the CLI's skill from the API Skill Hub"),
11900
- 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")
11901
11909
  }),
11902
- plan: async ({ name, description, install, git, repository, skillHubGit, skillHubApi, skillAdd, dependency }, ctx) => {
11910
+ plan: async ({ name, description, install, git, repository, dependency }, ctx) => {
11903
11911
  const binName = PACKAGE_NAME.exec(name)?.[1] ?? name;
11904
- const mode = installModeOf({
11905
- git: skillHubGit,
11906
- api: skillHubApi,
11907
- add: skillAdd
11908
- }, dependency, binName);
11912
+ const mode = installModeOf(dependency, binName);
11909
11913
  const path = resolve(ctx.cwd, binName);
11910
11914
  await ensureFree(path);
11911
11915
  const setup = await installSetupOf(mode, name, binName);
@@ -11928,8 +11932,7 @@ await runProcess(defineCli({
11928
11932
  }),
11929
11933
  install,
11930
11934
  repository,
11931
- edits: setup.edits,
11932
- manifestFiles: setup.files
11935
+ edits: setup.edits
11933
11936
  };
11934
11937
  },
11935
11938
  apply: async (plan, ctx) => {
@@ -11937,7 +11940,6 @@ await runProcess(defineCli({
11937
11940
  await writeTemplate(plan.path, plan.files, plan.values);
11938
11941
  if (plan.repository !== void 0) await setRepository(plan.path, plan.repository);
11939
11942
  for (const edit of plan.edits) await applyEdit(plan.path, edit);
11940
- if (plan.manifestFiles.length > 0) await addFiles(plan.path, plan.manifestFiles);
11941
11943
  await runSteps(plan.steps, plan.path, ctx);
11942
11944
  return {
11943
11945
  path: plan.path,