@patchgg/cli 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +34 -6
  2. package/dist/cli.js +367 -102
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,10 +5,14 @@ The command-line tool for [Patch](https://patch.gg): make, validate and publish
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- npm i -g @patchgg/cli
8
+ npm i -g @patchgg/cli # with Node 20 or newer
9
+ curl -fsSL https://patch.gg/install.sh | sh # macOS and Linux, no Node needed
10
+ irm https://patch.gg/install.ps1 | iex # Windows PowerShell, no Node needed
9
11
  ```
10
12
 
11
- Every way of installing pch, and what each one takes, is on [docs.patch.gg/cli](https://docs.patch.gg/cli#install). They all get the same build of a version.
13
+ Every way of installing pch, and what each one takes, is on [docs.patch.gg/cli](https://docs.patch.gg/cli#install). They all get the same build of a version. The one-line installers put pch's executable in `~/.pch/bin` (`%USERPROFILE%\.pch\bin` on Windows) and add that to your PATH.
14
+
15
+ `pch update` brings the installers' pch up to the newest version. Installed with npm or pnpm, it says the command that updates it instead, and Patch Desktop's own pch moves with Patch Desktop. Once a day at most, pch says when a newer version is out, after a command in a terminal; never with `--json`, in CI, or when what it prints goes to another program.
12
16
 
13
17
  ## Use
14
18
 
@@ -468,6 +472,7 @@ This is on the `repos` branch, and Patch has it behind a flag, until it launches
468
472
  | | `PCH_API_URL` | Point at another server, e.g. `http://localhost:3000` |
469
473
  | | `PCH_GIT_URL` | Where that server's repos live, e.g. `http://localhost:8787` |
470
474
  | | `PCH_CONFIG` | Config file (default `~/.pch/config.json`, mode 600) |
475
+ | | `PCH_CDN_URL` | Where pch looks for its own releases, for `pch update` |
471
476
 
472
477
  ## Your agent
473
478
 
@@ -508,12 +513,35 @@ PCH_BIN=dist/cli.js pnpm test # the tests that run pch as a command, against t
508
513
 
509
514
  CI runs both, the first on Windows, macOS and Linux.
510
515
 
516
+ ### What the installers put in place
517
+
518
+ The one-line installers download an executable: pch's `dist/` and Node 26 in one file, through Node's single executable support, so it needs nothing installed. `scripts/build-binary.mjs` makes it from a `dist/` folder, on Node 26, for the machine it runs on, with the licence texts and the notices (Node's own licence added) inside it. On macOS it signs it ad hoc, which Apple Silicon needs.
519
+
520
+ ```sh
521
+ node scripts/build-binary.mjs dist out # on Node 26: out/pch-<os>-<arch>[.exe]
522
+ node scripts/smoke.mjs --executable out/pch-... # the smoke test, and pch update, against it
523
+ ```
524
+
525
+ CI runs both on Windows, macOS and Linux. pch knows it's the executable through `node:sea` (`src/install.ts`): it reads its licence texts from inside itself, and git's sign-in helper and the MCP server line `pch agent install` writes run it by its own path.
526
+
511
527
  ### Releasing
512
528
 
513
- `package.json` stays `"private": true`, so npm and pnpm refuse to publish pch from a checkout, and a test keeps it that way. [`publish.yml`](.github/workflows/publish.yml) is the only way out. It runs on a `v<version>` tag on `main`, and stops unless production has repos and this repo has an `NPM_TOKEN` secret. One build per version: its `pack` job runs every check, builds once, takes `private` out of its own copy, packs, tries the tarball installed, and keeps it as the run's artifact `npm-package`. Its `publish` job sends that same tarball to npm, with provenance once this repo is public. Every other way of installing pch is made from that tarball's `dist/`, never rebuilt from source.
514
- 1. `pnpm sync <a patch checkout at main>`, then `pnpm generate`, so what's released checks what the API checks.
515
- 2. Move the version on in `package.json`, the only place it lives (the build writes it into the bundle), and merge that to `main`.
516
- 3. `git tag v<version> <that commit>`, then `git push origin v<version>`.
529
+ **Release with the button:** [Actions, Release pch](https://github.com/patch-gg/pch/actions/workflows/release.yml), **Run workflow** on `main`, then pick `patch` (fixes), `minor` (new things) or `major` (breaking). It tags `main` with the next version and releases it to npm and the one-line installers together. Patch-gg/patch's `product/design/cli.md`, Releasing pch, says what each step does and what to do when one fails.
530
+
531
+ `package.json` stays `"private": true`, so npm and pnpm refuse to publish pch from a checkout, and a test keeps it that way. [`publish.yml`](.github/workflows/publish.yml) is the only way out. It runs on a `v<version>` tag on `main`, which is the version, and stops unless production has repos. It publishes with npm's trusted publishing, so no npm key is kept anywhere: npm takes `@patchgg/cli` only from this repo's `publish.yml`, as the package's settings on npmjs.com say. One build per version: its `pack` job runs every check, builds once, takes `private` out of its own copy, packs, tries the tarball installed, and keeps it as the run's artifact `npm-package`. Every other way of installing pch is made from that tarball's `dist/`, never rebuilt from source. Then, in order:
532
+ - `executable` makes the six executables, each on its own runner (Linux, macOS and Windows, x64 and arm64), and runs the smoke test against each, and on Linux the tests too.
533
+ - `sign-windows` signs both Windows executables with Patch's Artifact Signing certificate profile and checks each signature, in the `production` environment. It signs in to Azure with GitHub's OIDC token, which the app registration `patch-pch-signing-production` trusts for that environment alone, so there's no secret. A Windows executable is never shipped unsigned: missing settings stop the release.
534
+ - `staging` gzips them, writes `SHA256SUMS`, and puts the version on cdn-staging.patch.gg under `cli/<version>/`, then moves `cli/latest.json` to it ([`scripts/put-release.sh`](scripts/put-release.sh)).
535
+ - `try-installers` installs it from staging.patch.gg on a clean Debian container, Linux on Arm, both kinds of Mac and both kinds of Windows, and checks it runs, says its version, and on Windows, its signature.
536
+ - `publish` sends the tarball to npm, with provenance once this repo is public.
537
+ - `production` puts the same files on cdn.patch.gg and moves production's `latest.json`.
538
+
539
+ Nothing reaches npm or production unless every platform got through. A version's files never change: a version that's on the CDN already with other files stops the release. `latest.json` only moves forward, and it's the one version check: the installers read it, and so do `pch update` and the notice. The `staging` and `production` environments take `v*` tags only, and hold R2 keys for their own public bucket alone (`infra/scripts/create-pch-release-keys.mjs` in patch-gg/patch makes them).
540
+ To release what's on `main`:
541
+ 1. `pnpm sync <a patch checkout at main>`, then `pnpm generate`, so what's released checks what the API checks, and merge that to `main` if it changed anything.
542
+ 2. Press the button: Actions, **Release pch**, Run workflow on `main`, and pick how big a change it is: `patch` for fixes, `minor` for new things, `major` for a breaking change. Or `gh workflow run release.yml -f bump=minor`.
543
+
544
+ The button ([`release.yml`](.github/workflows/release.yml)) takes the newest `v<version>` tag, moves it on by that much, tags `main`'s newest commit with it and starts `publish.yml` on the tag. It refuses a commit that's released already. A person can still push a tag by hand, which runs the same release. Nobody edits a version: the tag is the version, `publish.yml` writes it into its own copy of `package.json`, and the build writes that into the bundle. `version` in `package.json` on `main` is only what a copy run from source says.
517
545
 
518
546
  ### End to end
519
547
 
package/dist/cli.js CHANGED
@@ -49,14 +49,14 @@ var require_picocolors = __commonJS({
49
49
  var argv = p.argv || [];
50
50
  var env = p.env || {};
51
51
  var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
52
- var formatter = (open3, close, replace = open3) => (input2) => {
52
+ var formatter = (open3, close, replace2 = open3) => (input2) => {
53
53
  let string4 = "" + input2, index = string4.indexOf(close, open3.length);
54
- return ~index ? open3 + replaceClose(string4, close, replace, index) + close : open3 + string4 + close;
54
+ return ~index ? open3 + replaceClose(string4, close, replace2, index) + close : open3 + string4 + close;
55
55
  };
56
- var replaceClose = (string4, close, replace, index) => {
56
+ var replaceClose = (string4, close, replace2, index) => {
57
57
  let result = "", cursor = 0;
58
58
  do {
59
- result += string4.substring(cursor, index) + replace;
59
+ result += string4.substring(cursor, index) + replace2;
60
60
  cursor = index + close.length;
61
61
  index = string4.indexOf(close, cursor);
62
62
  } while (~index);
@@ -172,7 +172,7 @@ var VERSION;
172
172
  var init_version = __esm({
173
173
  "src/version.ts"() {
174
174
  "use strict";
175
- VERSION = true ? "0.1.0" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
175
+ VERSION = true ? "0.1.1" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
176
176
  }
177
177
  });
178
178
 
@@ -39734,6 +39734,62 @@ init_errors();
39734
39734
  import { spawn } from "node:child_process";
39735
39735
  import { resolve } from "node:path";
39736
39736
  init_errors();
39737
+
39738
+ // src/install.ts
39739
+ import { existsSync, realpathSync } from "node:fs";
39740
+ import { createRequire } from "node:module";
39741
+ import { dirname as dirname2, join as join2, sep } from "node:path";
39742
+ var require2 = createRequire(import.meta.url);
39743
+ function seaApi() {
39744
+ try {
39745
+ return require2("node:sea");
39746
+ } catch {
39747
+ return null;
39748
+ }
39749
+ }
39750
+ var sea = seaApi();
39751
+ var isExecutable = sea?.isSea() ?? false;
39752
+ function embeddedText(key) {
39753
+ if (!sea || !isExecutable) throw new Error(`pch isn't an executable, so ${key} isn't in it.`);
39754
+ return sea.getAsset(key, "utf8");
39755
+ }
39756
+ function installKind(script = process.argv[1], versions = process.versions, executable = isExecutable) {
39757
+ if (executable) return "executable";
39758
+ if (versions.electron) return "desktop";
39759
+ let path2 = script ?? "";
39760
+ try {
39761
+ path2 = realpathSync(path2);
39762
+ } catch {
39763
+ }
39764
+ const parts = path2.split(/[\\/]/);
39765
+ const at = parts.lastIndexOf("node_modules");
39766
+ if (at < 0 || parts[at + 1] !== "@patchgg" || parts[at + 2] !== "cli") return "other";
39767
+ if (/[\\/]pnpm[\\/]global[\\/]/.test(path2)) return "pnpm";
39768
+ const owner = parts.slice(0, at).join(sep);
39769
+ return existsSync(join2(owner || sep, "package.json")) ? "other" : "npm";
39770
+ }
39771
+ function updateCommand(kind) {
39772
+ if (kind === "executable") return "pch update";
39773
+ if (kind === "npm") return "npm install -g @patchgg/cli";
39774
+ if (kind === "pnpm") return "pnpm add -g @patchgg/cli";
39775
+ return null;
39776
+ }
39777
+ function executableName(platform = process.platform, arch = process.arch) {
39778
+ const os = { linux: "linux", darwin: "macos", win32: "windows" }[platform];
39779
+ if (!os || arch !== "x64" && arch !== "arm64") return null;
39780
+ return `pch-${os}-${arch}${os === "windows" ? ".exe" : ""}`;
39781
+ }
39782
+ var CDNS = {
39783
+ production: "https://cdn.patch.gg",
39784
+ staging: "https://cdn-staging.patch.gg",
39785
+ local: null
39786
+ };
39787
+ function releasesUrl(env, vars = process.env) {
39788
+ const base = vars.PCH_CDN_URL ?? CDNS[env];
39789
+ return base ? `${base.replace(/\/+$/, "")}/cli` : null;
39790
+ }
39791
+
39792
+ // src/git.ts
39737
39793
  function runGit(args2, options = {}) {
39738
39794
  return new Promise((done, fail) => {
39739
39795
  const child = spawn("git", args2, {
@@ -39813,7 +39869,7 @@ function gitFailed(args2, result, advice = "") {
39813
39869
  );
39814
39870
  }
39815
39871
  var helperKey = (url2) => `credential.${url2}.helper`;
39816
- var PCH_GONE = "pch isn't where it was, so git can't sign in to Patch. Install it again with npm i -g @patchgg/cli, then run pch login.";
39872
+ var PCH_GONE = `pch isn't where it was, so git can't sign in to Patch. Install it again with ${isExecutable ? "the installer at https://docs.patch.gg/cli" : "npm i -g @patchgg/cli"}, then run pch login.`;
39817
39873
  function helperCommand({ config: config2, target }) {
39818
39874
  const usesToken = Boolean(process.env.PCH_TOKEN);
39819
39875
  const vars = {
@@ -39827,10 +39883,11 @@ function helperCommand({ config: config2, target }) {
39827
39883
  ELECTRON_RUN_AS_NODE: process.env.ELECTRON_RUN_AS_NODE
39828
39884
  };
39829
39885
  const settings = Object.entries(vars).filter(([, value]) => value).map(([name, value]) => `export ${name}=${shellQuote(value)}; `).join("");
39830
- const node2 = shellQuote(process.execPath);
39831
- const script = shellQuote(process.argv[1]);
39832
- const here = [node2, ...nodeFlags().map(shellQuote), script].join(" ");
39833
- return `!f() { ${settings}if [ -x ${node2} ] && [ -e ${script} ]; then ${here} git-credential "$1"; elif command -v pch >/dev/null; then pch git-credential "$1"; else echo "${PCH_GONE}" >&2; fi; }; f`;
39886
+ const program3 = shellQuote(process.execPath);
39887
+ const script = isExecutable ? null : shellQuote(process.argv[1]);
39888
+ const here = script ? [program3, ...nodeFlags().map(shellQuote), script].join(" ") : program3;
39889
+ const there = script ? `[ -x ${program3} ] && [ -e ${script} ]` : `[ -x ${program3} ]`;
39890
+ return `!f() { ${settings}if ${there}; then ${here} git-credential "$1"; elif command -v pch >/dev/null; then pch git-credential "$1"; else echo "${PCH_GONE}" >&2; fi; }; f`;
39834
39891
  }
39835
39892
  function nodeFlags() {
39836
39893
  return process.execArgv.map(
@@ -40308,7 +40365,7 @@ async function tokenRevoke(ctx, id) {
40308
40365
  var import_picocolors5 = __toESM(require_picocolors(), 1);
40309
40366
  import { constants } from "node:fs";
40310
40367
  import { access as access2, copyFile, lstat, mkdir as mkdir2, readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
40311
- import { basename, dirname as dirname2, extname, join as join4, resolve as resolve4 } from "node:path";
40368
+ import { basename, dirname as dirname3, extname, join as join5, resolve as resolve4 } from "node:path";
40312
40369
  import { createInterface } from "node:readline";
40313
40370
 
40314
40371
  // src/project-files.ts
@@ -40316,7 +40373,7 @@ var import_semver = __toESM(require_semver2(), 1);
40316
40373
  import { createHash } from "node:crypto";
40317
40374
  import { createReadStream } from "node:fs";
40318
40375
  import { readdir, readFile as readFile2, stat } from "node:fs/promises";
40319
- import { join as join2, relative, resolve as resolve2, sep } from "node:path";
40376
+ import { join as join3, relative, resolve as resolve2, sep as sep2 } from "node:path";
40320
40377
 
40321
40378
  // node_modules/.pnpm/zod@4.6.7/node_modules/zod/index.js
40322
40379
  init_external();
@@ -42085,7 +42142,7 @@ var projectFileSchema = external_exports.object({
42085
42142
  var usage = (code, message, details) => new CliError(EXIT.usage, code, message, details);
42086
42143
  async function loadProject(dir) {
42087
42144
  const root = resolve2(dir);
42088
- const path2 = join2(root, "patch.json");
42145
+ const path2 = join3(root, "patch.json");
42089
42146
  let text;
42090
42147
  try {
42091
42148
  text = await readFile2(path2, "utf8");
@@ -42129,11 +42186,11 @@ async function loadProject(dir) {
42129
42186
  );
42130
42187
  }
42131
42188
  const paths = (await listFiles(filesRoot)).filter(
42132
- (name) => join2(filesRoot, name) !== path2 && !(filesRoot === root && (name === BRIEF_FILE || name.startsWith(`${BRIEF_FOLDER}/`)))
42189
+ (name) => join3(filesRoot, name) !== path2 && !(filesRoot === root && (name === BRIEF_FILE || name.startsWith(`${BRIEF_FOLDER}/`)))
42133
42190
  );
42134
42191
  const files = [];
42135
42192
  for (const name of paths) {
42136
- const absolute = join2(filesRoot, ...name.split("/"));
42193
+ const absolute = join3(filesRoot, ...name.split("/"));
42137
42194
  files.push({ path: name, absolute, ...await hashFile(absolute) });
42138
42195
  }
42139
42196
  return {
@@ -42179,11 +42236,11 @@ async function listFiles(root) {
42179
42236
  const found = [];
42180
42237
  async function walk(dir) {
42181
42238
  for (const entry of await readdir(dir, { withFileTypes: true })) {
42182
- const absolute = join2(dir, entry.name);
42239
+ const absolute = join3(dir, entry.name);
42183
42240
  if (entry.isDirectory()) {
42184
42241
  if (!SKIPPED_DIRS.has(entry.name)) await walk(absolute);
42185
42242
  } else if (entry.isFile() && !SKIPPED_FILES.has(entry.name)) {
42186
- found.push(relative(root, absolute).split(sep).join("/"));
42243
+ found.push(relative(root, absolute).split(sep2).join("/"));
42187
42244
  }
42188
42245
  }
42189
42246
  }
@@ -42204,7 +42261,7 @@ function hashFile(path2) {
42204
42261
  // src/commands/agent-guide.ts
42205
42262
  var import_picocolors4 = __toESM(require_picocolors(), 1);
42206
42263
  import { access, writeFile as writeFile2 } from "node:fs/promises";
42207
- import { join as join3, resolve as resolve3 } from "node:path";
42264
+ import { join as join4, resolve as resolve3 } from "node:path";
42208
42265
 
42209
42266
  // src/starter.ts
42210
42267
  var AGENT_GUIDE = `# Working on this project
@@ -42332,11 +42389,11 @@ Thumbs.db
42332
42389
 
42333
42390
  // src/commands/agent-guide.ts
42334
42391
  var AGENT_GUIDE_FILE = "AGENTS.md";
42335
- var GUIDES = [AGENT_GUIDE_FILE, "CLAUDE.md", join3(".claude", "CLAUDE.md")];
42392
+ var GUIDES = [AGENT_GUIDE_FILE, "CLAUDE.md", join4(".claude", "CLAUDE.md")];
42336
42393
  async function agentGuidesIn(root) {
42337
42394
  const found = [];
42338
42395
  for (const name of GUIDES) {
42339
- const there = await access(join3(root, name)).then(
42396
+ const there = await access(join4(root, name)).then(
42340
42397
  () => true,
42341
42398
  () => false
42342
42399
  );
@@ -42349,18 +42406,18 @@ async function agentGuideIn(root) {
42349
42406
  }
42350
42407
  async function writeAgentGuide(root) {
42351
42408
  const own2 = await agentGuideIn(root);
42352
- if (!own2) await writeFile2(join3(root, AGENT_GUIDE_FILE), AGENT_GUIDE);
42409
+ if (!own2) await writeFile2(join4(root, AGENT_GUIDE_FILE), AGENT_GUIDE);
42353
42410
  return { name: own2 ?? AGENT_GUIDE_FILE, written: !own2 };
42354
42411
  }
42355
42412
  async function agentGuide(ctx, dir) {
42356
42413
  const root = resolve3(dir);
42357
42414
  const { name, written } = await writeAgentGuide(root);
42358
- ctx.out.result({ path: join3(root, name), written }, () => {
42415
+ ctx.out.result({ path: join4(root, name), written }, () => {
42359
42416
  if (written) {
42360
- ctx.out.line(`Wrote ${import_picocolors4.default.bold(join3(root, name))}, for coding agents working in this folder.`);
42417
+ ctx.out.line(`Wrote ${import_picocolors4.default.bold(join4(root, name))}, for coding agents working in this folder.`);
42361
42418
  ctx.out.line(import_picocolors4.default.dim("Commit it: in a clone, pch publish refuses files that aren't."));
42362
42419
  } else {
42363
- ctx.out.line(`${join3(root, name)} is this folder's guide for agents, so it's left as it is.`);
42420
+ ctx.out.line(`${join4(root, name)} is this folder's guide for agents, so it's left as it is.`);
42364
42421
  ctx.out.line(
42365
42422
  import_picocolors4.default.dim("pch guide has how to run, check and publish, to add to it if it helps.")
42366
42423
  );
@@ -42413,7 +42470,7 @@ ${shown.join("\n")}
42413
42470
  ${said2.join("\n")}` : null;
42414
42471
  }
42415
42472
  async function gatherPictures(root, named, say) {
42416
- const folder = join4(root, BRIEF_FOLDER);
42473
+ const folder = join5(root, BRIEF_FOLDER);
42417
42474
  for (const path2 of named) {
42418
42475
  const from = resolve4(path2.trim().replace(/^["']|["']$/g, ""));
42419
42476
  const found = await lstat(from).catch(() => null);
@@ -42421,7 +42478,7 @@ async function gatherPictures(root, named, say) {
42421
42478
  say(`Left out ${path2}: pch takes a PNG, JPEG, GIF or WebP file it can read.`);
42422
42479
  continue;
42423
42480
  }
42424
- if (dirname2(from) === folder) continue;
42481
+ if (dirname3(from) === folder) continue;
42425
42482
  await mkdir2(folder, { recursive: true });
42426
42483
  await copyAside(from, folder);
42427
42484
  }
@@ -42433,7 +42490,7 @@ async function copyAside(from, folder) {
42433
42490
  const stem = basename(from, extension);
42434
42491
  for (let number4 = 1; ; number4++) {
42435
42492
  const name = number4 === 1 ? `${stem}${extension}` : `${stem}-${number4}${extension}`;
42436
- const copied = await copyFile(from, join4(folder, name), constants.COPYFILE_EXCL).then(
42493
+ const copied = await copyFile(from, join5(folder, name), constants.COPYFILE_EXCL).then(
42437
42494
  () => true,
42438
42495
  (error62) => {
42439
42496
  if (error62.code === "EEXIST") return false;
@@ -42457,7 +42514,7 @@ async function fromTerminal(ctx) {
42457
42514
  }
42458
42515
  async function agentBrief(ctx, dir) {
42459
42516
  const root = resolve4(dir);
42460
- const path2 = join4(root, BRIEF_FILE);
42517
+ const path2 = join5(root, BRIEF_FILE);
42461
42518
  const there = await access2(path2).then(
42462
42519
  () => true,
42463
42520
  () => false
@@ -42507,7 +42564,7 @@ async function agentBrief(ctx, dir) {
42507
42564
  async function guidesNotNaming(root) {
42508
42565
  const silent = [];
42509
42566
  for (const name of await agentGuidesIn(root)) {
42510
- const text = await readFile3(join4(root, name), "utf8").catch(() => "");
42567
+ const text = await readFile3(join5(root, name), "utf8").catch(() => "");
42511
42568
  if (!text.includes(BRIEF_FILE)) silent.push(name);
42512
42569
  }
42513
42570
  return silent;
@@ -42517,10 +42574,10 @@ async function guidesNotNaming(root) {
42517
42574
  var import_picocolors6 = __toESM(require_picocolors(), 1);
42518
42575
  init_errors();
42519
42576
  import { spawnSync } from "node:child_process";
42520
- import { existsSync } from "node:fs";
42577
+ import { existsSync as existsSync2 } from "node:fs";
42521
42578
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
42522
42579
  import { homedir as homedir2 } from "node:os";
42523
- import { delimiter, dirname as dirname3, join as join5, resolve as resolve5 } from "node:path";
42580
+ import { delimiter, dirname as dirname4, join as join6, resolve as resolve5 } from "node:path";
42524
42581
  var AGENT_IDS = ["claude", "codex", "cursor"];
42525
42582
  var AGENT_NAMES = {
42526
42583
  claude: "Claude Code",
@@ -42617,7 +42674,7 @@ async function installCursor(server) {
42617
42674
  ...settings.mcpServers,
42618
42675
  [server.name]: { command: server.command, args: server.args }
42619
42676
  };
42620
- await mkdir3(dirname3(path2), { recursive: true });
42677
+ await mkdir3(dirname4(path2), { recursive: true });
42621
42678
  await writeFile4(path2, `${JSON.stringify(settings, null, 2)}
42622
42679
  `);
42623
42680
  return { agent: "cursor", state: had ? "updated" : "added", where: path2 };
@@ -42636,7 +42693,7 @@ function mcpServer({ target }, options = {}, found = {}) {
42636
42693
  return {
42637
42694
  name,
42638
42695
  command: process.execPath,
42639
- args: [...process.execArgv, resolve5(process.argv[1]), ...flags]
42696
+ args: isExecutable ? flags : [...process.execArgv, resolve5(process.argv[1]), ...flags]
42640
42697
  };
42641
42698
  }
42642
42699
  function commandLine(server) {
@@ -42657,7 +42714,7 @@ function pchLogin({ target }) {
42657
42714
  }
42658
42715
  function isHere(agent2) {
42659
42716
  if (onPath(agent2)) return true;
42660
- if (agent2 === "cursor") return existsSync(join5(homedir2(), ".cursor"));
42717
+ if (agent2 === "cursor") return existsSync2(join6(homedir2(), ".cursor"));
42661
42718
  return false;
42662
42719
  }
42663
42720
  function codexHas(server) {
@@ -42666,7 +42723,7 @@ function codexHas(server) {
42666
42723
  }
42667
42724
  function onPath(bin) {
42668
42725
  const names = process.platform === "win32" ? [`${bin}.cmd`, `${bin}.exe`, `${bin}.bat`, bin] : [bin];
42669
- return (process.env.PATH ?? "").split(delimiter).some((folder) => folder && names.some((name) => existsSync(join5(folder, name))));
42726
+ return (process.env.PATH ?? "").split(delimiter).some((folder) => folder && names.some((name) => existsSync2(join6(folder, name))));
42670
42727
  }
42671
42728
  function run(bin, args2) {
42672
42729
  const shell = process.platform === "win32";
@@ -42678,7 +42735,7 @@ function run(bin, args2) {
42678
42735
  var import_picocolors7 = __toESM(require_picocolors(), 1);
42679
42736
  import { createHash as createHash2 } from "node:crypto";
42680
42737
  import { mkdir as mkdir4, readFile as readFile5, stat as stat2, writeFile as writeFile5 } from "node:fs/promises";
42681
- import { basename as basename2, dirname as dirname4, extname as extname2, isAbsolute, join as join6, relative as relative2, resolve as resolve6, sep as sep2 } from "node:path";
42738
+ import { basename as basename2, dirname as dirname5, extname as extname2, isAbsolute, join as join7, relative as relative2, resolve as resolve6, sep as sep3 } from "node:path";
42682
42739
  import { setTimeout as sleep2 } from "node:timers/promises";
42683
42740
  init_errors();
42684
42741
  var DEFAULT_WAIT_SECONDS = 300;
@@ -42784,7 +42841,7 @@ async function assetsAdd(ctx, ids, options) {
42784
42841
  saved.push(path2);
42785
42842
  }
42786
42843
  if (!saved.length) continue;
42787
- const holds = saved.length === 1 ? saved[0] : dirname4(saved[0]);
42844
+ const holds = saved.length === 1 ? saved[0] : dirname5(saved[0]);
42788
42845
  credits.push({
42789
42846
  files: creditedFiles(holds, saved.length > 1, project2?.filesRoot ?? process.cwd()),
42790
42847
  license: item.license.spdx,
@@ -42871,7 +42928,7 @@ function placeInFolder(item, file2, taken) {
42871
42928
  taken.add(path2);
42872
42929
  return path2;
42873
42930
  }
42874
- var forwardSlashes = (path2) => path2.split(sep2).join("/");
42931
+ var forwardSlashes = (path2) => path2.split(sep3).join("/");
42875
42932
  var fromHere = (path2) => forwardSlashes(relative2(process.cwd(), path2));
42876
42933
  function creditedFiles(path2, folder, filesRoot) {
42877
42934
  const inRelease = relative2(filesRoot, path2);
@@ -42881,15 +42938,15 @@ function creditedFiles(path2, folder, filesRoot) {
42881
42938
  }
42882
42939
  async function projectFileFor(root) {
42883
42940
  const here = process.cwd();
42884
- for (let dir = root; ; dir = dirname4(dir)) {
42885
- const path2 = join6(dir, "patch.json");
42941
+ for (let dir = root; ; dir = dirname5(dir)) {
42942
+ const path2 = join7(dir, "patch.json");
42886
42943
  if (await stat2(path2).catch(() => null)) {
42887
42944
  const contents = await readJson(path2);
42888
42945
  if (!contents) return null;
42889
42946
  const files = typeof contents.files === "string" ? contents.files : ".";
42890
42947
  return { path: path2, filesRoot: resolve6(dir, files), contents };
42891
42948
  }
42892
- if (dir === here || relative2(here, dir).startsWith("..") || dir === dirname4(dir)) return null;
42949
+ if (dir === here || relative2(here, dir).startsWith("..") || dir === dirname5(dir)) return null;
42893
42950
  }
42894
42951
  }
42895
42952
  async function readJson(path2) {
@@ -42909,7 +42966,7 @@ async function addToCreditsFile(path2, lines) {
42909
42966
  async function addToRecipe(project2, credits) {
42910
42967
  const { recipe } = project2.contents;
42911
42968
  const inFile = typeof recipe === "string";
42912
- const path2 = inFile ? resolve6(dirname4(project2.path), recipe) : project2.path;
42969
+ const path2 = inFile ? resolve6(dirname5(project2.path), recipe) : project2.path;
42913
42970
  const held = inFile ? await readJson(path2) : recipe ?? {};
42914
42971
  if (!held || held.credits !== void 0 && !Array.isArray(held.credits)) return null;
42915
42972
  const before = held.credits ?? [];
@@ -42922,14 +42979,14 @@ async function addToRecipe(project2, credits) {
42922
42979
  }
42923
42980
  function inside(root, relative3) {
42924
42981
  const path2 = resolve6(root, relative3);
42925
- if (!path2.startsWith(root + sep2)) {
42982
+ if (!path2.startsWith(root + sep3)) {
42926
42983
  throw new CliError(EXIT.error, "asset_path_invalid", `${relative3} isn't inside ${root}.`);
42927
42984
  }
42928
42985
  return path2;
42929
42986
  }
42930
42987
  async function writeInside(root, relative3, contents) {
42931
42988
  const path2 = inside(root, relative3);
42932
- await mkdir4(dirname4(path2), { recursive: true });
42989
+ await mkdir4(dirname5(path2), { recursive: true });
42933
42990
  await writeFile5(path2, contents);
42934
42991
  }
42935
42992
  async function download(url2) {
@@ -42951,7 +43008,7 @@ async function saveChecked(url2, path2, sha256) {
42951
43008
  `${url2} didn't download as Patch says it should: try again.`
42952
43009
  );
42953
43010
  }
42954
- await mkdir4(dirname4(path2), { recursive: true });
43011
+ await mkdir4(dirname5(path2), { recursive: true });
42955
43012
  await writeFile5(path2, bytes);
42956
43013
  }
42957
43014
 
@@ -43454,8 +43511,8 @@ async function diffOf(ctx, project2, request) {
43454
43511
  }
43455
43512
  async function standing(ctx, project2, request) {
43456
43513
  const { diff, upToDate } = await diffOf(ctx, project2, request);
43457
- const update = upToDate === false ? await updateSteps(ctx, project2, request).catch(() => null) : null;
43458
- return { diff, fields: { up_to_date: upToDate, update } };
43514
+ const update2 = upToDate === false ? await updateSteps(ctx, project2, request).catch(() => null) : null;
43515
+ return { diff, fields: { up_to_date: upToDate, update: update2 } };
43459
43516
  }
43460
43517
  async function updateSteps(ctx, project2, request) {
43461
43518
  const repo = await unwrap(
@@ -43803,17 +43860,17 @@ async function changeRequestWithdraw(ctx, ref) {
43803
43860
 
43804
43861
  // src/commands/check.ts
43805
43862
  var import_picocolors10 = __toESM(require_picocolors(), 1);
43806
- import { existsSync as existsSync3 } from "node:fs";
43863
+ import { existsSync as existsSync4 } from "node:fs";
43807
43864
  import { mkdir as mkdir6, mkdtemp as mkdtemp2 } from "node:fs/promises";
43808
43865
  import { tmpdir as tmpdir2 } from "node:os";
43809
- import { basename as basename3, join as join11, resolve as resolve8 } from "node:path";
43866
+ import { basename as basename3, join as join12, resolve as resolve8 } from "node:path";
43810
43867
 
43811
43868
  // src/check/browser.ts
43812
43869
  import { spawn as spawn3 } from "node:child_process";
43813
- import { existsSync as existsSync2 } from "node:fs";
43870
+ import { existsSync as existsSync3 } from "node:fs";
43814
43871
  import { chmod as chmod2, mkdir as mkdir5, mkdtemp, rename, rm, writeFile as writeFile6 } from "node:fs/promises";
43815
43872
  import { tmpdir, userInfo } from "node:os";
43816
- import { delimiter as delimiter2, dirname as dirname5, join as join7, resolve as resolve7, sep as sep3 } from "node:path";
43873
+ import { delimiter as delimiter2, dirname as dirname6, join as join8, resolve as resolve7, sep as sep4 } from "node:path";
43817
43874
  import { inflateRawSync } from "node:zlib";
43818
43875
  init_errors();
43819
43876
  var VERSIONS = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json";
@@ -43822,9 +43879,9 @@ function installedBrowsers(vars) {
43822
43879
  if (process.platform === "win32") {
43823
43880
  const places = [vars.ProgramFiles, vars["ProgramFiles(x86)"], vars.LOCALAPPDATA];
43824
43881
  return places.filter((place) => Boolean(place)).flatMap((place) => [
43825
- join7(place, "Google", "Chrome", "Application", "chrome.exe"),
43826
- join7(place, "Microsoft", "Edge", "Application", "msedge.exe"),
43827
- join7(place, "Chromium", "Application", "chrome.exe")
43882
+ join8(place, "Google", "Chrome", "Application", "chrome.exe"),
43883
+ join8(place, "Microsoft", "Edge", "Application", "msedge.exe"),
43884
+ join8(place, "Chromium", "Application", "chrome.exe")
43828
43885
  ]);
43829
43886
  }
43830
43887
  if (process.platform === "darwin") {
@@ -43835,7 +43892,7 @@ function installedBrowsers(vars) {
43835
43892
  ];
43836
43893
  }
43837
43894
  const names = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "msedge"];
43838
- return (vars.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((place) => names.map((name) => join7(place, name)));
43895
+ return (vars.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((place) => names.map((name) => join8(place, name)));
43839
43896
  }
43840
43897
  function downloadPlatform() {
43841
43898
  const { platform, arch } = process;
@@ -43844,8 +43901,8 @@ function downloadPlatform() {
43844
43901
  if (platform === "linux") return arch === "x64" ? "linux64" : null;
43845
43902
  return null;
43846
43903
  }
43847
- var browsersFolder = () => join7(dirname5(configPath()), "browser");
43848
- var shellFile = (platform) => join7(
43904
+ var browsersFolder = () => join8(dirname6(configPath()), "browser");
43905
+ var shellFile = (platform) => join8(
43849
43906
  browsersFolder(),
43850
43907
  platform,
43851
43908
  `chrome-headless-shell-${platform}`,
@@ -43853,16 +43910,16 @@ var shellFile = (platform) => join7(
43853
43910
  );
43854
43911
  function findBrowser(named, vars = process.env) {
43855
43912
  if (named) {
43856
- if (!existsSync2(named)) {
43913
+ if (!existsSync3(named)) {
43857
43914
  throw new CliError(EXIT.usage, "browser_missing", `There's no browser at ${named}.`);
43858
43915
  }
43859
43916
  return { path: resolve7(named), headlessShell: /headless[-_]shell/i.test(named), from: "named" };
43860
43917
  }
43861
- const installed = installedBrowsers(vars).find((path2) => existsSync2(path2));
43918
+ const installed = installedBrowsers(vars).find((path2) => existsSync3(path2));
43862
43919
  if (installed) return { path: installed, headlessShell: false, from: "this PC" };
43863
43920
  const platform = downloadPlatform();
43864
43921
  const shell = platform && shellFile(platform);
43865
- return shell && existsSync2(shell) ? { path: shell, headlessShell: true, from: "downloaded" } : null;
43922
+ return shell && existsSync3(shell) ? { path: shell, headlessShell: true, from: "downloaded" } : null;
43866
43923
  }
43867
43924
  async function downloadBrowser(out) {
43868
43925
  const platform = downloadPlatform();
@@ -43873,7 +43930,7 @@ async function downloadBrowser(out) {
43873
43930
  `There's no browser on this PC, and none to download for ${process.platform} ${process.arch}. Install Chrome or Chromium, or name one with --browser.`
43874
43931
  );
43875
43932
  }
43876
- const folder = join7(browsersFolder(), platform);
43933
+ const folder = join8(browsersFolder(), platform);
43877
43934
  const failed3 = (why) => new CliError(
43878
43935
  EXIT.error,
43879
43936
  "browser_download_failed",
@@ -43921,7 +43978,7 @@ async function downloadBrowser(out) {
43921
43978
  await rm(folder, { recursive: true, force: true });
43922
43979
  await rename(unpacking, folder);
43923
43980
  const shell = shellFile(platform);
43924
- if (!existsSync2(shell)) throw failed3("the download didn't hold what it should");
43981
+ if (!existsSync3(shell)) throw failed3("the download didn't hold what it should");
43925
43982
  return { path: shell, headlessShell: true, from: "downloaded" };
43926
43983
  }
43927
43984
  async function unzip(zip, into) {
@@ -43943,14 +44000,14 @@ async function unzip(zip, into) {
43943
44000
  const name = zip.toString("utf8", at + 46, at + 46 + nameLength);
43944
44001
  at += 46 + nameLength + extraLength + commentLength;
43945
44002
  const target = resolve7(root, name);
43946
- if (!target.startsWith(root + sep3)) throw new Error(`a path leaves the zip: ${name}`);
44003
+ if (!target.startsWith(root + sep4)) throw new Error(`a path leaves the zip: ${name}`);
43947
44004
  if (name.endsWith("/")) {
43948
44005
  await mkdir5(target, { recursive: true });
43949
44006
  continue;
43950
44007
  }
43951
44008
  const start = local + 30 + zip.readUInt16LE(local + 26) + zip.readUInt16LE(local + 28);
43952
44009
  const body = zip.subarray(start, start + packed);
43953
- await mkdir5(dirname5(target), { recursive: true });
44010
+ await mkdir5(dirname6(target), { recursive: true });
43954
44011
  await writeFile6(target, method === 0 ? body : inflateRawSync(body));
43955
44012
  if (mode & 73) await chmod2(target, 493);
43956
44013
  }
@@ -44003,7 +44060,7 @@ var Browser = class _Browser {
44003
44060
  );
44004
44061
  }
44005
44062
  static async launch(found, sandboxOff) {
44006
- const profile = await mkdtemp(join7(tmpdir(), "pch-check-profile-"));
44063
+ const profile = await mkdtemp(join8(tmpdir(), "pch-check-profile-"));
44007
44064
  const child = spawn3(
44008
44065
  found.path,
44009
44066
  [
@@ -44196,7 +44253,7 @@ async function checkModel(file2) {
44196
44253
 
44197
44254
  // src/check/play.ts
44198
44255
  import { writeFile as writeFile7 } from "node:fs/promises";
44199
- import { join as join9 } from "node:path";
44256
+ import { join as join10 } from "node:path";
44200
44257
  import { setTimeout as wait } from "node:timers/promises";
44201
44258
 
44202
44259
  // src/check/pixels.ts
@@ -44284,7 +44341,7 @@ function changed(a, b) {
44284
44341
  import { createReadStream as createReadStream2 } from "node:fs";
44285
44342
  import { stat as stat3 } from "node:fs/promises";
44286
44343
  import { createServer } from "node:http";
44287
- import { join as join8 } from "node:path";
44344
+ import { join as join9 } from "node:path";
44288
44345
  var GAME_HOST_HEADERS = {
44289
44346
  "Content-Security-Policy": [
44290
44347
  "default-src 'self'",
@@ -44393,7 +44450,7 @@ async function serveGame(root, files) {
44393
44450
  notFound();
44394
44451
  return;
44395
44452
  }
44396
- const absolute = join8(root, ...path2.split("/"));
44453
+ const absolute = join9(root, ...path2.split("/"));
44397
44454
  const { type, encoding } = typeOf(path2);
44398
44455
  const html = type.startsWith("text/html");
44399
44456
  const headers = {
@@ -44700,7 +44757,7 @@ async function play(browser, options) {
44700
44757
  if (!shot) return null;
44701
44758
  const bytes = Buffer.from(shot.data, "base64");
44702
44759
  if (keep) {
44703
- const file2 = join9(options.out, name);
44760
+ const file2 = join10(options.out, name);
44704
44761
  await writeFile7(file2, bytes);
44705
44762
  observed.screenshots.push(file2);
44706
44763
  }
@@ -44824,7 +44881,7 @@ async function readRendering(browser, origin) {
44824
44881
 
44825
44882
  // src/check/report.ts
44826
44883
  import { readFile as readFile8 } from "node:fs/promises";
44827
- import { join as join10 } from "node:path";
44884
+ import { join as join11 } from "node:path";
44828
44885
  var NEEDS_GPU = /web ?gpu|web ?gl|graphics adapter|gpu adapter|no adapter/i;
44829
44886
  var seconds = (ms) => ms < 1e3 ? `${ms} ms` : `${(ms / 1e3).toFixed(ms < 1e4 ? 1 : 0)} s`;
44830
44887
  var limit = (ms) => `${ms / 1e3} s`;
@@ -44836,7 +44893,7 @@ async function pathsInText(root, files) {
44836
44893
  const byLowerCase = new Map(files.map((file2) => [file2.toLowerCase(), file2]));
44837
44894
  const found = [];
44838
44895
  for (const file2 of files.filter((name) => TEXT_FILES.test(name))) {
44839
- const text = await readFile8(join10(root, ...file2.split("/")), "utf8").catch(() => "");
44896
+ const text = await readFile8(join11(root, ...file2.split("/")), "utf8").catch(() => "");
44840
44897
  if (!text || text.length > TEXT_LIMIT) continue;
44841
44898
  const folder = file2.includes("/") ? file2.slice(0, file2.lastIndexOf("/") + 1) : "";
44842
44899
  for (const [, written] of text.matchAll(PATH_IN_TEXT)) {
@@ -45027,7 +45084,7 @@ var LABELS = {
45027
45084
  var ADVICE = "This is advice: nothing here stops you publishing, and it doesn't judge what kind of game this is. Patch's own test runs in Patch Desktop: pch test start.";
45028
45085
  async function findGame(dir) {
45029
45086
  const root = resolve8(dir);
45030
- if (existsSync3(join11(root, "patch.json"))) {
45087
+ if (existsSync4(join12(root, "patch.json"))) {
45031
45088
  const project2 = await loadProject(root);
45032
45089
  if (project2.recipe.mode !== "browser") {
45033
45090
  throw usage2(
@@ -45044,8 +45101,8 @@ async function findGame(dir) {
45044
45101
  };
45045
45102
  }
45046
45103
  for (const folder of ["", "dist", "build", "public", "out"]) {
45047
- if (existsSync3(join11(root, folder, "index.html"))) {
45048
- const here = join11(root, folder);
45104
+ if (existsSync4(join12(root, folder, "index.html"))) {
45105
+ const here = join12(root, folder);
45049
45106
  return { root: here, files: await listFiles(here), entry: "index.html", selfChecks: [] };
45050
45107
  }
45051
45108
  }
@@ -45072,9 +45129,9 @@ async function check2(ctx, dir, options) {
45072
45129
  );
45073
45130
  }
45074
45131
  const models = options.models === true ? await Promise.all(
45075
- game.files.filter((file2) => file2.toLowerCase().endsWith(".glb")).map((file2) => modelAt(join11(game.root, ...file2.split("/"))))
45132
+ game.files.filter((file2) => file2.toLowerCase().endsWith(".glb")).map((file2) => modelAt(join12(game.root, ...file2.split("/"))))
45076
45133
  ) : [];
45077
- const folder = options.out ? resolve8(options.out) : await mkdtemp2(join11(tmpdir2(), `pch-check-${basename3(resolve8(dir))}-`));
45134
+ const folder = options.out ? resolve8(options.out) : await mkdtemp2(join12(tmpdir2(), `pch-check-${basename3(resolve8(dir))}-`));
45078
45135
  await mkdir6(folder, { recursive: true });
45079
45136
  const found = findBrowser(options.browser) ?? await downloadBrowser(out);
45080
45137
  out.step(`Opening ${game.entry} from ${game.root}, muted, for ${seconds2} s or so`);
@@ -45217,7 +45274,7 @@ function printModels(ctx, models) {
45217
45274
  // src/commands/connect.ts
45218
45275
  var import_picocolors14 = __toESM(require_picocolors(), 1);
45219
45276
  import { readFile as readFile12, writeFile as writeFile10 } from "node:fs/promises";
45220
- import { join as join14, resolve as resolve11 } from "node:path";
45277
+ import { join as join15, resolve as resolve11 } from "node:path";
45221
45278
  import { setTimeout as sleep5 } from "node:timers/promises";
45222
45279
  init_errors();
45223
45280
 
@@ -45265,7 +45322,7 @@ function sameName(a, b) {
45265
45322
  var import_picocolors13 = __toESM(require_picocolors(), 1);
45266
45323
  import { randomUUID as randomUUID2 } from "node:crypto";
45267
45324
  import { mkdir as mkdir7, readdir as readdir3, readFile as readFile11, rm as rm2, writeFile as writeFile9 } from "node:fs/promises";
45268
- import { join as join13, resolve as resolve10 } from "node:path";
45325
+ import { join as join14, resolve as resolve10 } from "node:path";
45269
45326
  import { setTimeout as sleep4 } from "node:timers/promises";
45270
45327
  init_errors();
45271
45328
  init_guide_words();
@@ -45307,7 +45364,7 @@ function unknownLicence(name) {
45307
45364
  );
45308
45365
  }
45309
45366
  async function licenceText(id, fill, texts = LICENCE_TEXTS) {
45310
- const text = await readFile9(new URL(`${id}.txt`, texts), "utf8");
45367
+ const text = isExecutable && texts === LICENCE_TEXTS ? embeddedText(`licences/${id}.txt`) : await readFile9(new URL(`${id}.txt`, texts), "utf8");
45311
45368
  return text.replace(
45312
45369
  /^Copyright \(c\) <year> <(?:copyright holders|owner)>/m,
45313
45370
  `Copyright (c) ${fill.year} ${fill.holder}`
@@ -45442,7 +45499,7 @@ async function versionTaken(ctx, ref, failure2) {
45442
45499
  // src/commands/init.ts
45443
45500
  var import_picocolors11 = __toESM(require_picocolors(), 1);
45444
45501
  import { readFile as readFile10, stat as stat4, writeFile as writeFile8 } from "node:fs/promises";
45445
- import { basename as basename4, join as join12, resolve as resolve9 } from "node:path";
45502
+ import { basename as basename4, join as join13, resolve as resolve9 } from "node:path";
45446
45503
  init_errors();
45447
45504
  init_guide_words();
45448
45505
  var FIRST_VERSION = "0.1.0";
@@ -45452,7 +45509,7 @@ function startingProjectFile(project2, version2 = FIRST_VERSION) {
45452
45509
  }
45453
45510
  async function init(ctx, dir, options) {
45454
45511
  const root = resolve9(dir);
45455
- const path2 = join12(root, "patch.json");
45512
+ const path2 = join13(root, "patch.json");
45456
45513
  if (!options.force && await readFile10(path2, "utf8").catch(() => null) !== null) {
45457
45514
  throw new CliError(
45458
45515
  EXIT.conflict,
@@ -45462,7 +45519,7 @@ async function init(ctx, dir, options) {
45462
45519
  }
45463
45520
  const name = await projectFor(ctx, root, options.project);
45464
45521
  const files = options.files ?? await builtFolder(root);
45465
- const hasRecipe = await stat4(join12(root, "recipe.json")).then(
45522
+ const hasRecipe = await stat4(join13(root, "recipe.json")).then(
45466
45523
  () => true,
45467
45524
  () => false
45468
45525
  );
@@ -45485,7 +45542,7 @@ async function init(ctx, dir, options) {
45485
45542
  "pch validate",
45486
45543
  "pch publish"
45487
45544
  ];
45488
- const agentGuide2 = { path: join12(root, guide2.name), written: guide2.written };
45545
+ const agentGuide2 = { path: join13(root, guide2.name), written: guide2.written };
45489
45546
  const result = { path: path2, ...contents, on_patch: onPatch2, agent_guide: agentGuide2 };
45490
45547
  ctx.out.result({ ...result, next, look_first: LOOK_FIRST }, () => {
45491
45548
  ctx.out.line(`Wrote ${import_picocolors11.default.bold(path2)} for ${import_picocolors11.default.bold(name)}.`);
@@ -45527,9 +45584,9 @@ async function builtFolder(root) {
45527
45584
  (found) => found.isFile(),
45528
45585
  () => false
45529
45586
  );
45530
- if (await isFile(join12(root, "index.html"))) return void 0;
45587
+ if (await isFile(join13(root, "index.html"))) return void 0;
45531
45588
  for (const folder of ["dist", "build", "public", "out"]) {
45532
- if (await isFile(join12(root, folder, "index.html"))) return folder;
45589
+ if (await isFile(join13(root, folder, "index.html"))) return folder;
45533
45590
  }
45534
45591
  return void 0;
45535
45592
  }
@@ -45775,7 +45832,7 @@ async function newRepo(ctx, ref, dir, options) {
45775
45832
  lfs,
45776
45833
  url: url2,
45777
45834
  look_first: LOOK_FIRST,
45778
- agent_guide: { path: join13(folder, AGENT_GUIDE_FILE), written: true },
45835
+ agent_guide: { path: join14(folder, AGENT_GUIDE_FILE), written: true },
45779
45836
  commits_as: commitsAs,
45780
45837
  ...options.visibility === "private" ? { who_can_see: PRIVATE_REPO } : {}
45781
45838
  },
@@ -45815,14 +45872,14 @@ async function clearFolder(folder, keepIt) {
45815
45872
  if (!keepIt) return rm2(folder, { recursive: true, force: true });
45816
45873
  const entries = await readdir3(folder).catch(() => []);
45817
45874
  await Promise.all(
45818
- entries.map((entry) => rm2(join13(folder, entry), { recursive: true, force: true }))
45875
+ entries.map((entry) => rm2(join14(folder, entry), { recursive: true, force: true }))
45819
45876
  );
45820
45877
  }
45821
45878
  async function firstCommit(ctx, folder, name, files) {
45822
45879
  await mkdir7(folder, { recursive: true });
45823
45880
  await mustGit(["init", "-q", "-b", FIRST_BRANCH], { cwd: folder });
45824
45881
  const commitsAs = await commitAsYourHandle(ctx, folder);
45825
- for (const [path2, text] of Object.entries(files)) await writeFile9(join13(folder, path2), text);
45882
+ for (const [path2, text] of Object.entries(files)) await writeFile9(join14(folder, path2), text);
45826
45883
  const lfs = await hasLfs();
45827
45884
  if (!lfs) ctx.out.step(NO_LFS);
45828
45885
  else if (!await lfsSetUp()) await setUpLfsHere(ctx, folder);
@@ -46200,7 +46257,7 @@ async function originOnGithub(ctx, dir) {
46200
46257
  );
46201
46258
  }
46202
46259
  async function readManifest(dir) {
46203
- const path2 = join14(dir, "patch.json");
46260
+ const path2 = join15(dir, "patch.json");
46204
46261
  let text;
46205
46262
  try {
46206
46263
  text = await readFile12(path2, "utf8");
@@ -46596,7 +46653,7 @@ async function nameInManifest(manifest, project2) {
46596
46653
  // src/commands/covers.ts
46597
46654
  var import_picocolors15 = __toESM(require_picocolors(), 1);
46598
46655
  import { mkdir as mkdir8, readFile as readFile13, writeFile as writeFile11 } from "node:fs/promises";
46599
- import { basename as basename5, extname as extname3, join as join15, resolve as resolve12 } from "node:path";
46656
+ import { basename as basename5, extname as extname3, join as join16, resolve as resolve12 } from "node:path";
46600
46657
  import { setTimeout as sleep7 } from "node:timers/promises";
46601
46658
  init_errors();
46602
46659
 
@@ -46817,13 +46874,13 @@ async function follow2(ctx, project2, job, wait2) {
46817
46874
  async function save(ctx, job, folder) {
46818
46875
  const ready = job.takes.filter((take) => take.state === "ready" && take.url);
46819
46876
  if (!ready.length || folder === void 0) return [];
46820
- const dir = resolve12(folder || join15("covers", job.id));
46877
+ const dir = resolve12(folder || join16("covers", job.id));
46821
46878
  await mkdir8(dir, { recursive: true });
46822
46879
  ctx.out.step(`Saving ${ready.length} take${ready.length === 1 ? "" : "s"} to ${dir}`);
46823
46880
  const saved = [];
46824
46881
  for (const take of ready) {
46825
- const file2 = join15(dir, `${take.shape}-${take.id}.jpg`);
46826
- const preview = join15(dir, `${take.shape}-${take.id}-card.jpg`);
46882
+ const file2 = join16(dir, `${take.shape}-${take.id}.jpg`);
46883
+ const preview = join16(dir, `${take.shape}-${take.id}-card.jpg`);
46827
46884
  for (const [url2, path2] of [
46828
46885
  [take.url, file2],
46829
46886
  [take.preview_url, preview]
@@ -46941,7 +46998,7 @@ function outFolder(out, byDefault) {
46941
46998
  // src/commands/git.ts
46942
46999
  var import_picocolors16 = __toESM(require_picocolors(), 1);
46943
47000
  import { stat as stat5 } from "node:fs/promises";
46944
- import { basename as basename6, join as join16, resolve as resolve13 } from "node:path";
47001
+ import { basename as basename6, join as join17, resolve as resolve13 } from "node:path";
46945
47002
 
46946
47003
  // src/direct-push.ts
46947
47004
  init_errors();
@@ -47706,7 +47763,7 @@ async function clone2(ctx, ref, dir) {
47706
47763
  if (lfsForThisOne) await setUpLfsHere(ctx, into);
47707
47764
  const commitsAs = target.token ? await commitAsYourHandle(ctx, into) : null;
47708
47765
  const guide2 = await agentGuideIn(into);
47709
- const brief = await stat5(join16(into, BRIEF_FILE)).then(
47766
+ const brief = await stat5(join17(into, BRIEF_FILE)).then(
47710
47767
  (found) => found.isFile(),
47711
47768
  () => false
47712
47769
  );
@@ -48286,7 +48343,7 @@ var import_picocolors19 = __toESM(require_picocolors(), 1);
48286
48343
  import { createHash as createHash3 } from "node:crypto";
48287
48344
  import { mkdir as mkdir9, mkdtemp as mkdtemp3, writeFile as writeFile12 } from "node:fs/promises";
48288
48345
  import { tmpdir as tmpdir3 } from "node:os";
48289
- import { join as join17, resolve as resolve15 } from "node:path";
48346
+ import { join as join18, resolve as resolve15 } from "node:path";
48290
48347
  import { setTimeout as sleep9 } from "node:timers/promises";
48291
48348
  init_errors();
48292
48349
  var HOLD_SECONDS = 50;
@@ -48467,7 +48524,7 @@ async function testFootage(ctx, ref, options) {
48467
48524
  );
48468
48525
  const sheets = data.length ? await saveSheets(
48469
48526
  data,
48470
- options.out ? resolve15(options.out) : await mkdtemp3(join17(tmpdir3(), "pch-footage-"))
48527
+ options.out ? resolve15(options.out) : await mkdtemp3(join18(tmpdir3(), "pch-footage-"))
48471
48528
  ) : [];
48472
48529
  const next = data.length ? reviewCommand(name) : nextStep(name, "testing", test2).command;
48473
48530
  out.result({ test: test2.id, state: test2.state, data: sheets, next }, () => {
@@ -48500,7 +48557,7 @@ async function saveSheets(sheets, folder) {
48500
48557
  `Run ${sheet.run}'s sheet ${sheet.index} didn't download intact. Try again.`
48501
48558
  );
48502
48559
  }
48503
- const path2 = join17(folder, `run-${sheet.run}-sheet-${sheet.index}.jpg`);
48560
+ const path2 = join18(folder, `run-${sheet.run}-sheet-${sheet.index}.jpg`);
48504
48561
  await writeFile12(path2, bytes);
48505
48562
  const { url: _url2, ...kept } = sheet;
48506
48563
  saved.push({ ...kept, path: path2 });
@@ -48652,8 +48709,204 @@ async function categories(ctx, options) {
48652
48709
  });
48653
48710
  }
48654
48711
 
48655
- // src/commands/validate.ts
48712
+ // src/commands/update.ts
48656
48713
  var import_picocolors21 = __toESM(require_picocolors(), 1);
48714
+ var import_semver3 = __toESM(require_semver2(), 1);
48715
+ import { spawnSync as spawnSync2 } from "node:child_process";
48716
+ import { createHash as createHash4 } from "node:crypto";
48717
+ import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
48718
+ import { chmod as chmod3, rename as rename2, rm as rm3, writeFile as writeFile13 } from "node:fs/promises";
48719
+ import { basename as basename8, dirname as dirname7, join as join19 } from "node:path";
48720
+ import { gunzipSync } from "node:zlib";
48721
+ init_errors();
48722
+ init_version();
48723
+ var DAY = 24 * 60 * 60 * 1e3;
48724
+ var QUIET = /* @__PURE__ */ new Set(["mcp", "git-credential", "update"]);
48725
+ async function latestVersion2(releases, signal) {
48726
+ const response = await fetch(`${releases}/latest.json`, { signal });
48727
+ if (!response.ok) throw new Error(`${releases}/latest.json answered ${response.status}`);
48728
+ const { version: version2 } = await response.json();
48729
+ if (typeof version2 !== "string" || !import_semver3.default.valid(version2)) {
48730
+ throw new Error(`${releases}/latest.json names no version`);
48731
+ }
48732
+ return version2;
48733
+ }
48734
+ function parseSums(text) {
48735
+ const sums = /* @__PURE__ */ new Map();
48736
+ for (const line of text.split(/\r?\n/)) {
48737
+ const match = /^([0-9a-f]{64}) [ *](.+)$/i.exec(line.trim());
48738
+ if (match) sums.set(match[2], match[1].toLowerCase());
48739
+ }
48740
+ return sums;
48741
+ }
48742
+ function notReplaceable(kind) {
48743
+ const command = updateCommand(kind);
48744
+ if (kind === "desktop")
48745
+ return { command, message: "This pch came with Patch Desktop, and updates with it." };
48746
+ if (command)
48747
+ return {
48748
+ command,
48749
+ message: `This pch was installed with ${kind}. To update it, run: ${command}`
48750
+ };
48751
+ return {
48752
+ command,
48753
+ message: `This pch runs from ${process.argv[1]}, which pch update doesn't manage. Update it the way you installed it.`
48754
+ };
48755
+ }
48756
+ async function download2(url2) {
48757
+ const response = await fetch(url2, { signal: AbortSignal.timeout(10 * 6e4) }).catch(
48758
+ (error62) => {
48759
+ throw new CliError(
48760
+ EXIT.error,
48761
+ "download_failed",
48762
+ `Couldn't download ${url2}: ${error62.message}`
48763
+ );
48764
+ }
48765
+ );
48766
+ if (!response.ok) {
48767
+ throw new CliError(EXIT.error, "download_failed", `${url2} answered ${response.status}.`);
48768
+ }
48769
+ return Buffer.from(await response.arrayBuffer());
48770
+ }
48771
+ async function update(ctx) {
48772
+ const kind = installKind();
48773
+ if (kind !== "executable") {
48774
+ const { command, message } = notReplaceable(kind);
48775
+ ctx.out.result(
48776
+ { version: VERSION, install: kind, updated: false, command },
48777
+ () => ctx.out.line(message)
48778
+ );
48779
+ return EXIT.ok;
48780
+ }
48781
+ const releases = releasesUrl(ctx.target.env);
48782
+ const name = executableName();
48783
+ if (!releases || !name) {
48784
+ throw new CliError(
48785
+ EXIT.usage,
48786
+ "no_releases",
48787
+ releases ? `There's no pch executable for ${process.platform} ${process.arch}.` : `${ctx.target.env} has no releases of pch to update from.`
48788
+ );
48789
+ }
48790
+ ctx.out.step("Looking for a newer pch");
48791
+ const latest2 = await latestVersion2(releases, AbortSignal.timeout(15e3)).catch(
48792
+ (error62) => {
48793
+ throw new CliError(
48794
+ EXIT.error,
48795
+ "download_failed",
48796
+ `Couldn't check for a newer pch: ${error62.message}`
48797
+ );
48798
+ }
48799
+ );
48800
+ if (!import_semver3.default.gt(latest2, VERSION)) {
48801
+ ctx.out.result(
48802
+ { version: VERSION, latest: latest2, install: kind, updated: false },
48803
+ () => ctx.out.line(`pch ${VERSION} is the newest.`)
48804
+ );
48805
+ return EXIT.ok;
48806
+ }
48807
+ const file2 = `${name}.gz`;
48808
+ ctx.out.step(`Downloading pch ${latest2}`);
48809
+ const [sums, packed] = await Promise.all([
48810
+ download2(`${releases}/${latest2}/SHA256SUMS`),
48811
+ download2(`${releases}/${latest2}/${file2}`)
48812
+ ]);
48813
+ const expected = parseSums(sums.toString("utf8")).get(file2);
48814
+ const actual = createHash4("sha256").update(packed).digest("hex");
48815
+ if (!expected || expected !== actual) {
48816
+ throw new CliError(
48817
+ EXIT.error,
48818
+ "checksum_mismatch",
48819
+ `pch ${latest2} for this machine doesn't match its SHA-256, so it wasn't installed.`,
48820
+ { file: file2, expected: expected ?? null, actual }
48821
+ );
48822
+ }
48823
+ const target = process.execPath;
48824
+ const fresh = `${target}.new`;
48825
+ await writeFile13(fresh, gunzipSync(packed), { mode: 493 });
48826
+ await chmod3(fresh, 493);
48827
+ const said2 = spawnSync2(fresh, ["--version"], { encoding: "utf8", timeout: 6e4 });
48828
+ if (said2.status !== 0 || said2.stdout.trim() !== latest2) {
48829
+ await rm3(fresh, { force: true });
48830
+ throw new CliError(
48831
+ EXIT.error,
48832
+ "update_failed",
48833
+ `pch ${latest2} didn't start on this machine, so pch ${VERSION} stays.`,
48834
+ { stderr: (said2.stderr ?? said2.error?.message ?? "").trim().slice(-2e3) }
48835
+ );
48836
+ }
48837
+ await replace(target, fresh);
48838
+ ctx.out.result(
48839
+ { version: latest2, previous: VERSION, install: kind, updated: true },
48840
+ () => ctx.out.line(`Updated pch from ${VERSION} to ${latest2}.`)
48841
+ );
48842
+ return EXIT.ok;
48843
+ }
48844
+ async function replace(target, fresh) {
48845
+ if (process.platform !== "win32") return rename2(fresh, target);
48846
+ const old = `${target}.${process.pid}.old`;
48847
+ await rename2(target, old);
48848
+ try {
48849
+ await rename2(fresh, target);
48850
+ } catch (error62) {
48851
+ await rename2(old, target);
48852
+ throw error62;
48853
+ }
48854
+ }
48855
+ function removeOldExecutables(target = process.execPath) {
48856
+ if (!isExecutable || process.platform !== "win32") return;
48857
+ const folder = dirname7(target);
48858
+ const old = new RegExp(`^${basename8(target).replace(/\./g, "\\.")}\\.\\d+\\.old$`, "i");
48859
+ try {
48860
+ for (const name of readdirSync(folder)) {
48861
+ if (old.test(name)) rmSync(join19(folder, name), { force: true });
48862
+ }
48863
+ } catch {
48864
+ }
48865
+ }
48866
+ function startUpdateNotice(ctx, command, now = Date.now()) {
48867
+ const how = updateCommand(installKind());
48868
+ const releases = releasesUrl(ctx.target.env);
48869
+ if (!how || !releases || ctx.out.json || QUIET.has(command)) return null;
48870
+ if (!process.stderr.isTTY || process.env.CI) return null;
48871
+ const stamp = join19(dirname7(configPath()), "update-check.json");
48872
+ try {
48873
+ const { checked_at } = JSON.parse(readFileSync(stamp, "utf8"));
48874
+ if (checked_at && now - Date.parse(checked_at) < DAY) return null;
48875
+ } catch {
48876
+ }
48877
+ try {
48878
+ mkdirSync(dirname7(stamp), { recursive: true, mode: 448 });
48879
+ writeFileSync(stamp, `${JSON.stringify({ checked_at: new Date(now).toISOString() })}
48880
+ `);
48881
+ } catch {
48882
+ return null;
48883
+ }
48884
+ const stop = new AbortController();
48885
+ const giveUp = setTimeout(() => stop.abort(), 2e3);
48886
+ giveUp.unref();
48887
+ const latest2 = latestVersion2(releases, stop.signal).catch(() => null).finally(() => clearTimeout(giveUp));
48888
+ return {
48889
+ async show() {
48890
+ let timer;
48891
+ const late = new Promise((done) => {
48892
+ timer = setTimeout(() => done(null), 500);
48893
+ });
48894
+ const version2 = await Promise.race([latest2, late]);
48895
+ clearTimeout(timer);
48896
+ stop.abort();
48897
+ if (version2 && import_semver3.default.gt(version2, VERSION)) {
48898
+ process.stderr.write(
48899
+ `
48900
+ ${import_picocolors21.default.dim("\u203A")} pch ${version2} is out (you have ${VERSION}). To update, run: ${how}
48901
+ `
48902
+ );
48903
+ }
48904
+ }
48905
+ };
48906
+ }
48907
+
48908
+ // src/commands/validate.ts
48909
+ var import_picocolors22 = __toESM(require_picocolors(), 1);
48657
48910
  init_errors();
48658
48911
  async function validate2(ctx, dir) {
48659
48912
  const project2 = await loadProject(dir);
@@ -48712,12 +48965,12 @@ async function validate2(ctx, dir) {
48712
48965
  notes
48713
48966
  };
48714
48967
  ctx.out.result(result, () => {
48715
- const mark = result.ok ? import_picocolors21.default.green("\u2713") : import_picocolors21.default.red("\u2717");
48968
+ const mark = result.ok ? import_picocolors22.default.green("\u2713") : import_picocolors22.default.red("\u2717");
48716
48969
  ctx.out.line(
48717
48970
  `${mark} ${file2.project}@${file2.version} \xB7 ${project2.kind} \xB7 ${result.trust_tier} \xB7 ${project2.files.length} files, ${formatSize(total)}`
48718
48971
  );
48719
- for (const issue2 of issues) ctx.out.line(` ${import_picocolors21.default.red(issue2.path || "\xB7")} ${issue2.message}`);
48720
- for (const note2 of notes) ctx.out.line(` ${import_picocolors21.default.dim(note2)}`);
48972
+ for (const issue2 of issues) ctx.out.line(` ${import_picocolors22.default.red(issue2.path || "\xB7")} ${issue2.message}`);
48973
+ for (const note2 of notes) ctx.out.line(` ${import_picocolors22.default.dim(note2)}`);
48721
48974
  });
48722
48975
  return result.ok ? EXIT.ok : EXIT.usage;
48723
48976
  }
@@ -48768,21 +49021,32 @@ function action(run3) {
48768
49021
  return async (...args2) => {
48769
49022
  const command = args2.at(-1);
48770
49023
  const globals = command.optsWithGlobals();
49024
+ let notice = null;
48771
49025
  try {
48772
49026
  const ctx = await createContext(globals);
49027
+ notice = startUpdateNotice(ctx, topLevel(command).name());
48773
49028
  const code = await run3(ctx, ...args2.slice(0, -1));
48774
49029
  process.exitCode = typeof code === "number" ? code : EXIT.ok;
48775
49030
  } catch (error62) {
48776
49031
  process.exitCode = new Output(Boolean(globals.json)).error(error62);
48777
49032
  }
49033
+ await notice?.show();
48778
49034
  };
48779
49035
  }
49036
+ function topLevel(command) {
49037
+ let top = command;
49038
+ while (top.parent?.parent) top = top.parent;
49039
+ return top;
49040
+ }
48780
49041
  var collect = (value, previous = []) => [...previous, value];
48781
49042
  program2.command("login").description(
48782
49043
  "Sign in through the browser (device code), and sign git in with it. It waits for a person, which can take a few minutes, so don't run it under a short timeout"
48783
49044
  ).option("--scope <scope>", "API key scope: read, publish, admin (repeatable)", collect).option("--no-browser", "print the link instead of opening a browser").action(action((ctx, options) => login(ctx, options)));
48784
49045
  program2.command("logout").description("Revoke and forget the active login, and sign git out").action(action((ctx) => logout(ctx)));
48785
49046
  program2.command("whoami").description("Who you're signed in as, and where").action(action((ctx) => whoami(ctx)));
49047
+ program2.command("update").description(
49048
+ "Update pch to the newest version, when the installer put it here; installed another way, it says how to update"
49049
+ ).action(action((ctx) => update(ctx)));
48786
49050
  program2.command("guide").description("How to make, test and publish on Patch, for you or your agent").action(action(async (ctx) => (await Promise.resolve().then(() => (init_mcp(), mcp_exports))).guide(ctx)));
48787
49051
  program2.command("mcp").description(
48788
49052
  "Patch's MCP server for your agent, signed in as pch is: add `pch mcp` to its servers"
@@ -49219,6 +49483,7 @@ var words = process.argv.slice(2);
49219
49483
  if (!program2.commands.some((one) => [one.name(), ...one.aliases()].some((n) => words.includes(n)))) {
49220
49484
  program2.version(VERSION);
49221
49485
  }
49486
+ removeOldExecutables();
49222
49487
  try {
49223
49488
  await program2.parseAsync(process.argv);
49224
49489
  } catch (error62) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patchgg/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "pch: the Patch command-line tool for creators and their agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://patch.gg",