@patchgg/cli 0.1.0 → 0.1.2

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 +95 -65
  2. package/dist/cli.js +653 -174
  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
 
@@ -25,64 +29,66 @@ pch media add your-name/my-game cover.png --kind cover
25
29
  pch project update your-name/my-game --summary "One line about it"
26
30
  ```
27
31
 
28
- | Command | Does |
29
- | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
- | `login [--scope …] [--no-browser]` | Device flow, then saves an API key for this machine and [signs git in](#git). With `PCH_TOKEN` set, checks that key and signs git in, and saves nothing |
31
- | `logout` | Revokes and forgets the active login, and signs git out of its git host unless another saved login is on it |
32
- | `whoami` | Who you're signed in as, where, and with which scopes |
33
- | `guide` | How to make, test and publish on Patch, signed in or out |
34
- | `mcp` | Patch's MCP server for your agent, signed in as pch is (below) |
35
- | `agent guide [dir]` | Writes `AGENTS.md` in a folder, for coding agents working there, as `new` and `init` do. A folder with its own guide is left alone |
36
- | `agent brief [dir]` | Starts a game's `BRIEF.md` by asking you ([below](#your-agent)). It never overwrites one |
37
- | `agent install [claude\|codex\|cursor]` | Adds `pch mcp` to an agent's MCP servers, or to every agent found on this PC ([below](#your-agent)) |
38
- | `account list` / `account use <name>` | Saved logins |
39
- | `token create --name <n> [--scope …]` / `list` / `revoke` | API keys for agents and CI (printed once). Making one takes approving a code in the browser, since keys can't make keys. Pushing with one takes the `publish` scope |
40
- | `search [query] [--kind …] [--category …] [--engine …] [--license …] [--limit <n>]` | [Finds open repos](#search) by words and meaning, each with what it is, its licence, its latest release and its clone URL |
41
- | `categories [--for <kind>]` | The categories `search --category` takes: games' and mods' by default, or a toolbox shelf's, each with how many projects are in it |
42
- | `assets search [query] [--type …] [--where …] [--license …] [--cap …] [--sort …]` / `assets facets` / `assets view <id>` | Patch's asset library: 3D models, materials, HDRIs, sounds, music and sprites, each with its licence. Works signed out |
43
- | `assets add <ids…> [--to <folder>]` | Copies assets into your game, each file checked against its SHA-256, with their licences in `LICENSES/`, and says how to load them. `--to` puts them straight in that folder under their own names. Their credits go in `CREDITS.md` and in the recipe in `patch.json`. Exit 3 if one can't be used, with alternatives |
44
- | `clone <owner/slug> [dir]` | Clones a repo, with git sign-in and Git LFS set up for it |
45
- | `push [remote] [refspec…]` | [Pushes to a repo on Patch at any size](#big-pushes), straight to where it's kept, where plain git brings up to 100 MB at a time. It takes git push's flags |
46
- | `new <owner/slug> [dir] [--licence <id>] [--name …] [--summary …]` | Makes an [Open repo](#new-repos) with a README to fill in, its licence, big-file rules and ignores, and pushes its first commit |
47
- | `remix <owner/slug> [--to <owner>] [--slug …] [--name …] [--visibility private]` | Makes a project from all of a repo (also `pch fork`), of yours or the org you name, which records its parent. It's as open as what it copies, unless `--visibility private` starts it private |
48
- | `connect [owner/slug] [--to <owner>] [--slug …] [--name …] [--visibility …] [--wait]` | [Makes GitHub the home](#connecting-a-github-repo) of a new project, or the one you name, for the GitHub repo this folder is a clone of, and names it in `patch.json` |
49
- | `status [owner/slug[@version]] [--wait <seconds>]` | Where a release is in review and whether it's live, with the repo's state and home: a project's latest release, the one you name, or with nothing named, the project this folder is |
50
- | `init [dir] [--project …] [--version …] [--files …] [--channel …] [--force]` | Writes `patch.json`: the project named, or this repo's, or your name and this folder's. Finds a build folder with a page in it, and a `recipe.json`. `pch new` writes one too |
51
- | `set <owner/slug> [--visibility …] [--licence <id>] [--training on\|off] [--change-requests on\|off]` | Changes who can find and read a repo, what Patch lists as its licence, and its switches |
52
- | `scan start <owner/slug> [--wait [seconds]]` | [Reads a repo's whole history for secrets](#going-open), or joins the scan that's running; `--wait` follows it until it finishes, or for that many seconds |
53
- | `scan view <owner/slug> [--wait [seconds]]` | The latest scan: what it found and how to take it out, and what going open publishes |
54
- | `change-request open [owner/slug] --title … [--description …] [--from <owner/slug>] [--branch …] [--into …]` | Offers a remix's branch to the repo it came from. In a clone of the fork, it works out the fork, the branch and the repo. `cr` for short |
55
- | `change-request list <owner/slug> [--state …] [--limit <n>]` | A repo's requests, newest first, saying which open ones are out of date |
56
- | `change-request view <owner/slug#number>` | One request: what it is, whether it can merge, what it changes, and its talk |
57
- | `change-request review <owner/slug#number>` | All of that, with its commits and patches |
58
- | `change-request talk <owner/slug#number> <text>` | Posts in its talk, to ask for changes or to answer |
59
- | `change-request merge <owner/slug#number> [--squash] [--message …]` | Merges it, once it's up to date |
60
- | `change-request decline <owner/slug#number>` / `withdraw <owner/slug#number>` | Turns it down, as the repo's org, or takes it back, as whoever offers it |
61
- | `check [dir] [--serve] [--seconds …] [--out …] [--browser …] [--models [files…]]` | [Sees a browser game run](#seeing-it-run) as Patch serves it, and says what it saw and couldn't. Advice, not a gate |
62
- | `validate [dir]` | `patch.json`, its recipe and files locally, then the same recipe checked by Patch. For a [block](#blocks), its controls, presets and triggers, and its option pictures |
63
- | `publish [dir] [--channel …] [--changelog …]` | Hash, tag the commit in a clone of the project's repo, upload only what Patch doesn't have, submit. [Most releases stop there as a private draft until they've passed a test](#from-upload-to-live) |
64
- | `test start <owner/slug@version> [--wait [seconds]]` | [Starts a test on your PC](#from-upload-to-live), which waits for you to press Start in Patch Desktop (unless your tests start by themselves), or follows the one that's open |
65
- | `test autostart <on or off>` | Whether your tests of browser games and blocks start in the person's Patch Desktop by themselves, with a notice each time. Ask them first, and change it only after a clear yes |
66
- | `test view <owner/slug@version> [--wait [seconds]]` | Where a release's newest test has got to, and what to do next |
67
- | `test footage <owner/slug@version> [--run <n>] [--out <dir>]` | A test's contact sheets, saved as pictures, in a new temporary folder unless `--out` names one, with their paths printed |
68
- | `test review <owner/slug@version> --verdict … --plays … --harmful … --summary … --moment …` | Sends your review of the footage, which decides the test |
69
- | `test cancel <owner/slug@version>` | Cancels a test that hasn't been decided |
70
- | `project create\|view <owner/slug>` / `project list` | |
71
- | `project update <owner/slug> [--name …] [--summary …] [--description …\|--description-file …] [--category …] [--link name=url]` | Changes a project's listing. Only what you name changes |
72
- | `project list --mine` | Your projects, drafts included |
73
- | `media add <owner/slug> <file> --kind cover\|screenshot\|clip [--alt …] [--wait [seconds]]` | Adds a picture or clip to a listing. It shows once Patch has made its copies, and a new cover takes the old one's place |
74
- | `media list <owner/slug>` / `media remove <owner/slug> <id>` / `media order <owner/slug> <ids…>` | A listing's media, in order, with where each one stands |
75
- | `covers <owner/slug>` | Free covers: the takes and fixes left, and the latest covers. Patch pays, up to 12 takes and 4 fixes a game once it's published. Only where Patch has turned them on (staging, for now) |
76
- | `covers make <owner/slug> --subject … [--shape …] [--still <file>…] [--screenshot <id>…] [--wait]` | Titled covers, wide and tall, from 1 to 3 stills of the game, once the person agreed a direction. Uploads each `--still`; `--wait` follows and saves the takes |
77
- | `covers view <owner/slug> <job>` / `covers fix <owner/slug> <take> <instruction>` | A job's takes, saved to look at; one new take, changed as you say |
78
- | `covers set <owner/slug> <take> --as cover\|tall_cover` | Sets the take the person picked as the cover (a wide take) or the tall cover (a tall one) |
79
- | `release list <owner/slug>` / `release view <owner/slug@version>` | A release's files. A library repo's are Patch's conversions, such as `web/` for browsers, each with a link that takes no login, and a `LICENSE` with their credit |
80
- | `release submit <owner/slug@version>` | Sends a release on to be published, once it has passed its test: this is your yes |
81
- | `release promote <owner/slug@version> [--channel …] [--as <version>]` | Puts a published release on another channel, public by default: a new version with the same files, recipe and commit, which uploads nothing |
82
- | `host-game search [q]` / `host-game view <slug>` | Host games mods run in. Signed in, `view` adds their wiki pages; `view engines/<slug>` or `view methods/<slug>` gives such a page alone |
83
- | `note suggest <about> <fact> [--link <url>] [--build <build>]` | Suggests one fact for Patch's wiki, about a game, `engines/<slug>` or `methods/<slug>`, with how it's known: a public page or the game build you saw it on. It only goes to a queue, and a person at Patch reads it before any agent does. Facts only, never about your own project, code or PC |
84
-
85
- Every command takes `--json`: results on stdout, errors as `{ "error": { "code", "message", "details" } }` on stderr. A long upload also says how far it's got on stderr, as lines of `{ "progress": { … } }`.
32
+ | Command | Does |
33
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | `login [--scope …] [--no-browser]` | Device flow, then saves an API key for this machine and [signs git in](#git). With `PCH_TOKEN` set, checks that key and signs git in, and saves nothing |
35
+ | `logout` | Revokes and forgets the active login, and signs git out of its git host unless another saved login is on it |
36
+ | `whoami` | Who you're signed in as, where, and with which scopes |
37
+ | `guide` | How to make, test and publish on Patch, signed in or out |
38
+ | `mcp` | Patch's MCP server for your agent, signed in as pch is (below) |
39
+ | `agent guide [dir]` | Writes `AGENTS.md` in a folder, for coding agents working there, as `new` and `init` do. A folder with its own guide is left alone |
40
+ | `agent brief [dir]` | Starts a game's `BRIEF.md` by asking you ([below](#your-agent)). It never overwrites one |
41
+ | `agent install [claude\|codex\|cursor]` | Adds `pch mcp` to an agent's MCP servers, or to every agent found on this PC ([below](#your-agent)) |
42
+ | `account list` / `account use <name>` | Saved logins |
43
+ | `token create --name <n> [--scope …]` / `list` / `revoke` | API keys for agents and CI (printed once). Making one takes approving a code in the browser, since keys can't make keys. Pushing with one takes the `publish` scope |
44
+ | `search [query] [--kind …] [--category …] [--engine …] [--license …] [--limit <n>]` | [Finds open repos](#search) by words and meaning, each with what it is, its licence, its latest release and its clone URL |
45
+ | `categories [--for <kind>]` | The categories `search --category` takes: games' and mods' by default, or a toolbox shelf's, each with how many projects are in it |
46
+ | `assets search [query] [--type …] [--where …] [--license …] [--cap …] [--sort …]` / `assets facets` / `assets view <id>` | Patch's asset library: 3D models, materials, HDRIs, sounds, music and sprites, each with its licence. Works signed out |
47
+ | `assets add <ids…> [--to <folder>]` | Copies assets into your game, each file checked against its SHA-256, with their licences in `LICENSES/`, and says how to load them. `--to` puts them straight in that folder under their own names. Their credits go in `CREDITS.md` and in the recipe in `patch.json`. Exit 3 if one can't be used, with alternatives |
48
+ | `clone <owner/slug> [dir]` | Clones a repo, with git sign-in and Git LFS set up for it |
49
+ | `push [remote] [refspec…]` | [Pushes to a repo on Patch at any size](#big-pushes), straight to where it's kept, where plain git brings up to 100 MB at a time. It takes git push's flags |
50
+ | `new <owner/slug> [dir] [--licence <id>] [--name …] [--summary …]` | Makes an [Open repo](#new-repos) with a README to fill in, its licence, big-file rules and ignores, and pushes its first commit |
51
+ | `remix <owner/slug> [--to <owner>] [--slug …] [--name …] [--visibility private]` | Makes a project from all of a repo (also `pch fork`), of yours or the org you name, which records its parent. It's as open as what it copies, unless `--visibility private` starts it private |
52
+ | `connect [owner/slug] [--to <owner>] [--slug …] [--name …] [--visibility …] [--wait]` | [Makes GitHub the home](#connecting-a-github-repo) of a new project, or the one you name, for the GitHub repo this folder is a clone of, and names it in `patch.json` |
53
+ | `status [owner/slug[@version]] [--wait <seconds>]` | Where a release is in review and whether it's live, with the repo's state and home: a project's latest release, the one you name, or with nothing named, the project this folder is |
54
+ | `init [dir] [--project …] [--version …] [--files …] [--channel …] [--force]` | Writes `patch.json`: the project named, or this repo's, or your name and this folder's. Finds a build folder with a page in it, and a `recipe.json`. `pch new` writes one too |
55
+ | `set <owner/slug> [--visibility …] [--licence <id>] [--training on\|off] [--change-requests on\|off]` | Changes who can find and read a repo, what Patch lists as its licence, and its switches |
56
+ | `scan start <owner/slug> [--wait [seconds]]` | [Reads a repo's whole history for secrets](#going-open), or joins the scan that's running; `--wait` follows it until it finishes, or for that many seconds |
57
+ | `scan view <owner/slug> [--wait [seconds]]` | The latest scan: what it found and how to take it out, and what going open publishes |
58
+ | `change-request open [owner/slug] --title … [--description …] [--from <owner/slug>] [--branch …] [--into …]` | Offers a remix's branch to the repo it came from. In a clone of the fork, it works out the fork, the branch and the repo. `cr` for short |
59
+ | `change-request list <owner/slug> [--state …] [--limit <n>]` | A repo's requests, newest first, saying which open ones are out of date |
60
+ | `change-request view <owner/slug#number>` | One request: what it is, whether it can merge, what it changes, and its talk |
61
+ | `change-request review <owner/slug#number>` | All of that, with its commits and patches |
62
+ | `change-request talk <owner/slug#number> <text>` | Posts in its talk, to ask for changes or to answer |
63
+ | `change-request merge <owner/slug#number> [--squash] [--message …]` | Merges it, once it's up to date |
64
+ | `change-request decline <owner/slug#number>` / `withdraw <owner/slug#number>` | Turns it down, as the repo's org, or takes it back, as whoever offers it |
65
+ | `check [dir] [--serve] [--seconds …] [--out …] [--browser …] [--models [files…]]` | [Sees a browser game run](#seeing-it-run) as Patch serves it, and says what it saw and couldn't. Advice, not a gate |
66
+ | `validate [dir]` | `patch.json`, its recipe and files locally, then the same recipe checked by Patch. For a [block](#blocks), its controls, presets and triggers, and its option pictures |
67
+ | `publish [dir] [--channel …] [--changelog …]` | Hash, tag the commit in a clone of the project's repo, upload only what Patch doesn't have, submit. [Most releases stop there as a private draft until they've passed a test](#from-upload-to-live) |
68
+ | `test start <owner/slug@version> [--wait [seconds]]` | [Starts a test on your PC](#from-upload-to-live), which waits for you to press Start in Patch Desktop (unless your tests start by themselves), or follows the one that's open |
69
+ | `test autostart <on or off>` | Whether your tests of browser games and blocks start in the person's Patch Desktop by themselves, with a notice each time. Ask them first, and change it only after a clear yes |
70
+ | `test view <owner/slug@version> [--wait [seconds]]` | Where a release's newest test has got to, and what to do next |
71
+ | `test footage <owner/slug@version> [--run <n>] [--out <dir>]` | A test's contact sheets, saved as pictures, in a new temporary folder unless `--out` names one, with their paths printed |
72
+ | `test review <owner/slug@version> --verdict … --plays … --harmful … --summary … --moment …` | Sends your review of the footage, which decides the test |
73
+ | `test cancel <owner/slug@version>` | Cancels a test that hasn't been decided |
74
+ | `project create\|view <owner/slug>` / `project list` | |
75
+ | `project update <owner/slug> [--name …] [--summary …] [--description …\|--description-file …] [--category …] [--link name=url]` | Changes a project's listing. Only what you name changes |
76
+ | `project list --mine` | Your projects, drafts included |
77
+ | `media add <owner/slug> <file> --kind cover\|screenshot\|clip [--alt …] [--wait [seconds]]` | Adds a picture or clip to a listing. It shows once Patch has made its copies, and a new cover takes the old one's place |
78
+ | `media list <owner/slug>` / `media remove <owner/slug> <id>` / `media order <owner/slug> <ids…>` | A listing's media, in order, with where each one stands |
79
+ | `covers <owner/slug>` | Free covers: the takes and fixes left, and the latest covers. Patch pays, up to 12 takes and 4 fixes a game once it's published. Only where Patch has turned them on (staging, for now) |
80
+ | `covers make <owner/slug> --subject … [--shape …] [--still <file>…] [--screenshot <id>…] [--wait]` | Titled covers, wide and tall, from 1 to 3 stills of the game, once the person agreed a direction. Uploads each `--still`; `--wait` follows and saves the takes |
81
+ | `covers view <owner/slug> <job>` / `covers fix <owner/slug> <take> <instruction>` | A job's takes, saved to look at; one new take, changed as you say |
82
+ | `covers set <owner/slug> <take> --as cover\|tall_cover` | Sets the take the person picked as the cover (a wide take) or the tall cover (a tall one) |
83
+ | `release list <owner/slug>` / `release view <owner/slug@version>` | A release's files. A library repo's are Patch's conversions, such as `web/` for browsers, each with a link that takes no login, and a `LICENSE` with their credit |
84
+ | `release submit <owner/slug@version>` | Sends a release on to be published, once it has passed its test: this is your yes |
85
+ | `release promote <owner/slug@version> [--channel …] [--as <version>]` | Puts a published release on another channel, public by default: a new version with the same files, recipe and commit, which uploads nothing |
86
+ | `host-game search [q]` / `host-game view <slug>` | Host games mods run in. Signed in, `view` adds their wiki pages; `view engines/<slug>` or `view methods/<slug>` gives such a page alone |
87
+ | `note suggest <about> <fact> [--link <url>] [--build <build>]` | Suggests one fact for Patch's wiki, about a game, `engines/<slug>` or `methods/<slug>`, with how it's known: a public page or the game build you saw it on. It only goes to a queue, and a person at Patch reads it before any agent does. Facts only, never about your own project, code or PC |
88
+ | `feedback send <summary> --kind <kind> [--goal …] [--expected …] [--actual …] [--tried …] [--workaround …] [--would-help …] [--cost …] [--severity …] [--add-to <id>]` | Tells Patch's team what got in your way, then you carry on: `failed` (you couldn't do what was asked), `bug` (Patch did something wrong), `call_error` (a call failed and its error didn't say what to do), `unhelpful` (a command or its answer didn't help) or `idea`. Write it for the engineer who'll fix it: what you were doing, what you expected, what happened in Patch's own words, what you tried, any workaround, what it cost (`--severity blocked`, `slowed` or `annoyed`) and what would have helped. It asks back the one thing a new report is missing that would help most, which `--add-to` adds. `--tool`, `--error-code`, `--project`, `--agent` and `--details` say more. A person at Patch reads every one, anything that looks like a key is taken out, and if the team already answered a report like it, it says what they said |
89
+ | `feedback list` | Your reports, newest first, with what Patch's team answered |
90
+
91
+ Every command takes `--json`: results on stdout, errors as `{ "error": { "code", "message", "details" } }` on stderr. An error from Patch's API, or one of pch's own that shouldn't happen (exit 1), adds `"feedback": { "say" }`: the `pch feedback send` to run if the error didn't help. Once a test is over or a release is live, `pch test view` and `pch status` ask what in Patch's tools cost you time getting there (`feedback_ask` with `--json`). A long upload also says how far it's got on stderr, as lines of `{ "progress": { … } }`.
86
92
 
87
93
  | Exit code | Meaning |
88
94
  | --------- | --------------------------------------------------------------------------------------------- |
@@ -147,7 +153,7 @@ It never pushes a submodule as well, as git's own push into one would take this
147
153
 
148
154
  Patch gives a key for that one command, and it needs the `publish` scope and a place in the repo's org. The key is only in git's environment, as `PCH_PUSH_KEY`, read by a credential helper on the command line, so no key goes in any file. Git LFS may note `locksverify` for its own address in `.git/config`, which holds no key. The remote keeps its address, and git's record of where the remote is moves as it does for any push. Big files still go to Patch's git host, signed in with the login pch is using.
149
155
 
150
- A key lasts an hour, and it's only checked as each of git's requests starts. When big files take longer than that to upload, pch gets one more key and pushes again, and they don't go twice. Git may print `Everything up-to-date` on the way: pch goes by what it says of each ref.
156
+ A key lasts 10 minutes, and it's only checked as each of git's requests starts. When big files take longer than that to upload, pch gets one more key and pushes again, and they don't go twice. Git may print `Everything up-to-date` on the way: pch goes by what it says of each ref.
151
157
 
152
158
  What's in a push is never why it's refused: an open repo's new commits are read for secrets after they land, and a secret that's landed takes a rewrite to get out. One ref that can't go refuses the whole push, `--atomic` or not, and nothing in it lands, so pch says the reason once. Once a push lands, pch tells Patch and prints whatever Patch has to say about it, such as a scan it's waiting on. If Patch can't be told, the push has still landed, and Patch finds out by itself a little later.
153
159
 
@@ -468,6 +474,7 @@ This is on the `repos` branch, and Patch has it behind a flag, until it launches
468
474
  | | `PCH_API_URL` | Point at another server, e.g. `http://localhost:3000` |
469
475
  | | `PCH_GIT_URL` | Where that server's repos live, e.g. `http://localhost:8787` |
470
476
  | | `PCH_CONFIG` | Config file (default `~/.pch/config.json`, mode 600) |
477
+ | | `PCH_CDN_URL` | Where pch looks for its own releases, for `pch update` |
471
478
 
472
479
  ## Your agent
473
480
 
@@ -508,12 +515,35 @@ PCH_BIN=dist/cli.js pnpm test # the tests that run pch as a command, against t
508
515
 
509
516
  CI runs both, the first on Windows, macOS and Linux.
510
517
 
518
+ ### What the installers put in place
519
+
520
+ 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.
521
+
522
+ ```sh
523
+ node scripts/build-binary.mjs dist out # on Node 26: out/pch-<os>-<arch>[.exe]
524
+ node scripts/smoke.mjs --executable out/pch-... # the smoke test, and pch update, against it
525
+ ```
526
+
527
+ 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.
528
+
511
529
  ### Releasing
512
530
 
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>`.
531
+ **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.
532
+
533
+ `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:
534
+ - `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.
535
+ - `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.
536
+ - `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)).
537
+ - `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.
538
+ - `publish` sends the tarball to npm, with provenance once this repo is public.
539
+ - `production` puts the same files on cdn.patch.gg and moves production's `latest.json`.
540
+
541
+ 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).
542
+ To release what's on `main`:
543
+ 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.
544
+ 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`.
545
+
546
+ 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
547
 
518
548
  ### End to end
519
549