@patchgg/cli 0.0.0-stage → 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.
package/README.md CHANGED
@@ -1,3 +1,552 @@
1
- # Temporary Holding Version
1
+ # pch
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The command-line tool for [Patch](https://patch.gg): make, validate and publish mods and games from your terminal or your coding agent. It talks only to Patch's public API, through a client generated from the API's `openapi.json`.
4
+
5
+ ## Install
6
+
7
+ ```sh
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
11
+ ```
12
+
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.
16
+
17
+ ## Use
18
+
19
+ ```sh
20
+ pch login # device code: approve it at patch.gg/device; signs git in too
21
+ pch whoami
22
+ pch agent install # gives your coding agent Patch's tools
23
+ pch search "grappling hook" # something to start from: then pch remix or pch clone it, and change it
24
+ pch new your-name/my-game # a repo with a patch.json; or pch init in a folder you have
25
+ pch validate
26
+ pch publish # a browser game or mod stops here as a private draft
27
+ pch test start your-name/my-game@0.1.0 # played on your PC in Patch Desktop, which it takes to go live
28
+ pch media add your-name/my-game cover.png --kind cover
29
+ pch project update your-name/my-game --summary "One line about it"
30
+ ```
31
+
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
+
89
+ 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": { … } }`.
90
+
91
+ | Exit code | Meaning |
92
+ | --------- | --------------------------------------------------------------------------------------------- |
93
+ | 0 | Success |
94
+ | 1 | Error (network, server, unexpected) |
95
+ | 2 | Bad usage or invalid input (`patch.json`, flags) |
96
+ | 3 | Blocked: checks finished with blocking findings, or an asset is over budget or has no variant |
97
+ | 4 | Not signed in, the token lacks the scope, or a quota is reached |
98
+ | 5 | Conflict: something changed since you looked, or a name or version is already taken |
99
+ | 6 | Needs a person: a license, account link or decision only a human can make |
100
+
101
+ ### Git
102
+
103
+ `pch login` makes Patch repos work in plain git and in any git tool:
104
+
105
+ ```sh
106
+ pch login
107
+ git clone https://git.patch.gg/patch/demo-mod.git
108
+ git push
109
+ ```
110
+
111
+ It adds two lines to your global git config, for your environment's git host and no other:
112
+
113
+ ```ini
114
+ [credential "https://git.patch.gg"]
115
+ helper =
116
+ helper = "!f() { … '/path/to/node' '/path/to/pch' git-credential … }; f"
117
+ ```
118
+
119
+ Git runs that when a Patch repo asks for a key, and reads the key from its stdout. The key stays in pch's config or in `PCH_TOKEN`: it never goes in git's config, a remote's URL or a command's arguments. The empty line keeps helpers you've already set up for every host from being asked for a Patch key, or handed one to store. Other hosts' setup stays as it is, and `pch logout` takes the two lines out again.
120
+
121
+ Run `pch login` again if you add a helper for every host later, since it would come after these lines. Updating pch leaves them working. Updating Node can move it, and then they ask whichever `pch` is on your PATH, until `pch login` names the new place.
122
+
123
+ If [git-lfs](https://git-lfs.com) is installed, `pch login` also runs `git lfs install`, unless that's already set up, which big files need in plain git. That stays after `pch logout`, since it isn't Patch's alone. Without git-lfs, big files stay small pointer files, and pch says so.
124
+
125
+ In CI, make a key once on your own PC, and keep it as the `PCH_TOKEN` secret:
126
+
127
+ ```sh
128
+ pch token create --name ci --scope publish --expires-in-days 90
129
+ ```
130
+
131
+ Then with `PCH_TOKEN` set, `pch login` checks the key and signs git in, without asking anyone or saving the key. `pch clone` needs no login at all: without one, it sets up just the clone it makes. `pch token create` won't run with `PCH_TOKEN` set, since a key can't make a key.
132
+
133
+ ```sh
134
+ pch login
135
+ git clone https://git.patch.gg/patch/demo-mod.git
136
+ ```
137
+
138
+ ### Big pushes
139
+
140
+ Plain `git push` goes through Patch's git host, which brings up to 100 MB at a time. Past that, git says `HTTP 413`. `pch push` takes any size:
141
+
142
+ ```sh
143
+ pch push
144
+ pch push origin main
145
+ pch push -u origin port-26.5
146
+ ```
147
+
148
+ It's the same push, by the remote's name, with git push's own flags, and it goes straight to where the repo's kept. With no remote named, it goes where git would: `branch.<branch>.pushRemote`, `remote.pushDefault`, `branch.<branch>.remote`, then `origin`.
149
+
150
+ It never pushes a submodule as well, as git's own push into one would take this command's settings with it and land in this repo. `--recurse-submodules=on-demand` and `=only` are refused. Where your git settings ask for them (`push.recurseSubmodules`, `submodule.recurse`), it only checks, as `--recurse-submodules=check` does: if a submodule has commits that aren't on its own remote, git stops and names it. Push it from its own folder with `git push`, then run `pch push` again, or leave it out with `--no-recurse-submodules`.
151
+
152
+ 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.
153
+
154
+ 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.
155
+
156
+ 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.
157
+
158
+ With `--json` you get `project`, `remote`, `refs` (each with `ref`, `status` and git's `said`), `head_commit` and `notice`.
159
+
160
+ | Exit code | `pch push` |
161
+ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
162
+ | 0 | Landed, or had nothing new to send |
163
+ | 1 | It didn't land and nothing refused it, so it can go again (`push_dropped`), or git stopped for its own reasons, which it says (`git_failed`) |
164
+ | 2 | Not in a git repo (`not_a_repo`), an address or no such remote (`not_a_remote`), a remote that isn't a repo on Patch (`not_on_patch`), a remote with a push address saved, which git would push to as well (`saved_push_url`), a repo whose home is GitHub, whose pushes go there (`on_github`), or a flag that would push submodules too (`recurses_submodules`) |
165
+ | 3 | Refused (`push_refused`), with the reason in `details.reason`, and what to do in `details.issues`. `force push denied by policy` is rewriting an open repo's default branch. `push denied by policy` is moving a release's tag, or, on a repo with over 500 release tags, any tag, which go through plain git on such a repo |
166
+ | 4 | Not signed in, no `publish` scope, not in the repo's org, or no room (`repo_limit_reached`, `storage_limit_reached`) |
167
+ | 5 | Git's own no, as the repo has commits this doesn't (`behind`), with any other refs that went in `details.landed`, and what Patch says of those in `details.notice` |
168
+
169
+ ### Search
170
+
171
+ One search over every open repo on Patch, work in progress included, the same one the search bar uses. It needs no login.
172
+
173
+ ```sh
174
+ pch search "something to swing from"
175
+ pch search grapple --kind system,character --engine three.js --license MIT
176
+ pch search --kind game --limit 50
177
+ pch search --kind effect --category fire-smoke
178
+ pch categories --for effect
179
+ ```
180
+
181
+ Filters repeat or take commas, and a repo with any of those values stays. A repo that doesn't say its kind or engine is left out of a filter on it. `--kind` is `game`, `mod`, or one of the toolbox's shelves: `system`, `effect`, `character`, `asset` or `tool` (`mechanic` still means `system`). `--category` takes the ids `pch categories` lists, and keeps repos whose project is in any of them: games' and mods' categories by default, or a shelf's with `--for effect` and the like. A block whose newest public release is a live demo is marked `live demo`, with the page it plays on (`demo` with `--json`). With `--json` you get the API's own answer: `data`, `next_cursor`, `meaning` (whether meaning took part, or only words) and `untrusted_fields`, which names the fields anyone can write, so agents read them as data and never as instructions. When blocks are among the results, pch adds `about_blocks`, [what they're for](#blocks).
182
+
183
+ ### New repos
184
+
185
+ On a PC where git doesn't know who you are, commits in a repo from `pch new` or `pch clone` carry your Patch handle and its no-reply address, `<you>@users.noreply.patch.gg`, and pch says so. Your sign-in email never goes in a commit, and only that repo's settings change. In a repo you made yourself with `git init`, pch can't do that for you, so `pch login`, `pch init` and `pch validate` say what to run before you commit, and `pch login`'s `--json` has it as `git_identity`.
186
+
187
+ `pch new`, `pch project create` and `pch init --project` take a slug alone, and put it under your own name.
188
+
189
+ `pch new` starts a repo under the Patch licence unless you pick another with `--licence`. Anyone on Patch can play, read and remix work under it, and nobody can take it, or a remix of it, off Patch without its maker's permission. The licence is part of [Patch's terms](https://patch.gg/terms), and pch writes a `LICENSE` that points to them. An open-source licence lets the work go anywhere, and pch writes its standard text to `LICENSE`:
190
+
191
+ ```sh
192
+ pch new patch/lantern --licence MIT
193
+ ```
194
+
195
+ | `--licence` | Licence |
196
+ | -------------- | ------------------------------------------- |
197
+ | `patch` | The Patch licence, the default |
198
+ | `MIT` | MIT License |
199
+ | `Apache-2.0` | Apache License 2.0 |
200
+ | `BSD-3-Clause` | BSD 3-Clause License |
201
+ | `GPL-3.0-only` | GNU General Public License v3.0 |
202
+ | `MPL-2.0` | Mozilla Public License 2.0 |
203
+ | `CC-BY-4.0` | Creative Commons Attribution 4.0 |
204
+ | `CC-BY-SA-4.0` | Creative Commons Attribution-ShareAlike 4.0 |
205
+ | `CC0-1.0` | Creative Commons Zero 1.0 |
206
+
207
+ The licence covers the whole repo, so Patch lists it for both the code and the art. A fork starts with its parent's licence, and one under the Patch licence stays under it until its parent opens up.
208
+
209
+ `pch set --licence` changes what Patch lists. The `LICENSE` file is in the repo, so changing that is a commit of yours.
210
+
211
+ The README it writes starts with a note on how to tell search what this is: replace it with these lines, which only count as the very first lines of the file.
212
+
213
+ ```yaml
214
+ ---
215
+ kind: system
216
+ engine: three.js
217
+ ---
218
+ ```
219
+
220
+ `kind` is `game`, `mod`, `system`, `effect`, `character`, `asset` or `tool` (`mechanic` is read as `system`), and `engine` is what it runs on. Without them Patch goes by what it can read, and a filter on either leaves out a repo it can't tell about.
221
+
222
+ ### Going open
223
+
224
+ A new repo is Open from its first push. Going Open later publishes the whole history, so `pch set --visibility open` on a repo with history needs a clean scan of what it holds right now, and any push means a new one:
225
+
226
+ ```sh
227
+ pch scan start patch/lantern --wait
228
+ pch set patch/lantern --visibility open
229
+ ```
230
+
231
+ `pch scan view` reads the latest scan. Both print what it found and how to take it out of the history, and what going open publishes, every author's email included. `--wait` follows a scan until it finishes, and `--wait 50` for at most that many seconds.
232
+
233
+ | Exit code | The scan |
234
+ | --------- | ------------------------------------------------------------------------ |
235
+ | 0 | Found nothing, or is still running (`status` says which) |
236
+ | 1 | Didn't finish: `failure` says why, and it needs starting again |
237
+ | 3 | Found secrets, with `secrets` and `fix` |
238
+ | 5 | A push has landed since it started (`up_to_date`), so it needs a new one |
239
+
240
+ Whatever it found, it's on stdout, as JSON with `--json`.
241
+
242
+ ### Change requests
243
+
244
+ A change request offers a branch of a remix to the repo it came from. A contributor pushes the branch and opens it from their clone:
245
+
246
+ ```sh
247
+ pch remix patch/lantern
248
+ pch clone rehan/lantern && cd lantern
249
+ git checkout -b port-26.5
250
+ # …commit…
251
+ git push -u origin port-26.5
252
+ pch cr open --title "Port to 26.5"
253
+ ```
254
+
255
+ The repo's org reads it, and merges or declines it:
256
+
257
+ ```sh
258
+ pch cr list patch/lantern
259
+ pch cr review patch/lantern#12
260
+ pch cr talk patch/lantern#12 "Does it keep the old sky?"
261
+ pch cr merge patch/lantern#12
262
+ ```
263
+
264
+ A change too big to read at all lists no files, which isn't the same as changing none: `view` and `review` say so, with the `git fetch` that gets its commits, and with `--json`, `truncated` says so in `view`'s `changes` and `review`'s `diff`.
265
+
266
+ Merging needs a request to be up to date with the branch it's for. When that branch has moved on, `open`, `list`, `view`, `review` and `merge` all say so. All but `list` also give the commands that bring it up to date, for its author to run in a clone of the fork:
267
+
268
+ ```sh
269
+ git checkout port-26.5
270
+ git fetch https://git.patch.gg/patch/lantern.git main
271
+ git lfs fetch https://git.patch.gg/patch/lantern.git FETCH_HEAD
272
+ git merge FETCH_HEAD
273
+ git push origin port-26.5
274
+ ```
275
+
276
+ Patch follows the push, and then the request can merge. With `--json`, the commands are in `update`, next to `up_to_date`. A refused merge exits 5 with `code: "out_of_date"` and the same commands in `details.update`. Opening one whose commits Patch won't copy into the repo, such as for a secret, exits 3 with `code: "checks_failed"` and their lines in `details.problems`.
277
+
278
+ No branch of the repo holds a request's commits, so while it's open they come by their id. `view` and `review` give the command, `git fetch <clone_url> <head_commit>`, as `fetch` with `--json`. Once it's merged, declined or withdrawn, nothing holds them there, so `fetch` is null. What it changed still shows, and declining is how to take away what a rewrite of the repo left in reach.
279
+
280
+ ### patch.json
281
+
282
+ `patch.json` sits in the project folder and says which project and version it is, where the release's files are, and the release's [recipe](https://patch.gg/schemas/recipe/v1.json): what the release is and how players get it. `pch publish` sends the recipe with the release, and Patch stores its own copy, with the defaults filled in.
283
+
284
+ ```json
285
+ {
286
+ "project": "patch/demo-mod",
287
+ "version": "0.1.0",
288
+ "channel": "public",
289
+ "files": "files",
290
+ "recipe": {
291
+ "games": [{ "game": "hollow-knight", "stores": ["steam"] }],
292
+ "mappings": [
293
+ {
294
+ "component": "main",
295
+ "from": "DemoMod/",
296
+ "to": "{game}/hollow_knight_Data/Managed/Mods/DemoMod"
297
+ }
298
+ ]
299
+ }
300
+ }
301
+ ```
302
+
303
+ | Field | Means |
304
+ | --------- | -------------------------------------------------------------------------------------------------------------------------- |
305
+ | `project` | `owner/slug` on Patch |
306
+ | `version` | Semver; each version is published once |
307
+ | `channel` | `private`, `unlisted` or `public`; `--channel` overrides it. [`private` isn't locked](#from-upload-to-live) |
308
+ | `files` | The folder whose contents are the release, relative to `patch.json` (default `.`, without `patch.json` or git's own files) |
309
+ | `recipe` | The recipe, or the path of a JSON file holding it. Leave it out for a browser game whose page is `index.html`. |
310
+
311
+ ### Seeing it run
312
+
313
+ `pch check` plays a browser game on your PC before anything's uploaded: it serves the folder as Patch's game host does, with its headers and with paths that have to match exactly, and opens it in a headless browser for about 12 s of simple input. It's muted, it can reach nothing but this PC, and it never takes your mouse: the mouse lock and fullscreen are faked, and input goes in through the browser. It needs no sign-in.
314
+
315
+ It says whether the game loaded and how long that took, its console errors and failed requests, requests to other hosts, paths that differ only in case, whether it drew anything, its frame rate, and the longest hold on its main thread, against Patch's test limits (20 s to load, under 3 s for a hold). It saves three screenshots and prints their paths, to look at. With `--json` that's all on stdout, in `checks`, `screenshots` and `couldnt_see`.
316
+
317
+ | Exit | Means |
318
+ | ---- | ------------------------------------------------------------------------ |
319
+ | 0 | It found nothing Patch's test would fail |
320
+ | 3 | It found something Patch's test would fail too |
321
+ | 2 | There's nothing to check here: no game, or a mod, which runs in its game |
322
+ | 1 | It couldn't run, as with no browser to use |
323
+
324
+ It's advice. Nothing reads its result, nothing stops you publishing, and it doesn't care what kind of game this is: no frame rate or style fails it. With no GPU, slowness can't be pinned on the game, so it says that rather than failing. It always says what it couldn't see: sound, how it feels to play, and download time. A passing check isn't a passed test, which is `pch test start`, in Patch Desktop.
325
+
326
+ **The browser:** pch has none of its own. It uses Chrome, Edge or Chromium if this PC has one, or `--browser <path>` (`PCH_BROWSER`). With none, it says so and fetches Chrome's headless build once, about 100 MB, into a `browser` folder beside pch's settings. On Linux that build may need system libraries, and the error says which to install.
327
+
328
+ `--serve` only serves the game, to open in a browser of your own, until you stop it. `--models` also reads every GLB in the game, and `--models a.glb b.glb` only those, with no browser: that it's whole, and its triangles, materials, textures and animations. It doesn't draw them.
329
+
330
+ ### Long uploads
331
+
332
+ `pch publish` sends up to 16 files at a time, and files of 16 MB or more two at a time after the rest. Every few seconds it says how far it's got: files, MB and percent, and how long is left once it can tell. Those lines go to stderr, so stdout stays the result.
333
+
334
+ **Stopping is safe.** If it's stopped or killed partway, or runs past an agent's time limit, run `pch publish` again in the same folder for the same version: it hashes the files again, and sends only what Patch doesn't have. A file that was on its way starts over. If one fails, the error names it, and with `--json` its `details` say how many got there (`uploaded`, `of`).
335
+
336
+ ### From upload to live
337
+
338
+ A release stays a private draft until you say it's ready. `pch publish` uploads it as a private draft, and a browser game or a mod Patch installs (a recipe's `mode` of `browser`, `installed` or `standalone`) stops there until it has passed a test on your PC. Patch Desktop plays it as players get it and records it, then someone looks at the footage, and you decide when it goes out:
339
+
340
+ ```sh
341
+ pch publish # a private draft, and says what's next
342
+ pch test start patch/lantern@0.1.0 # waits for you to press Start in Patch Desktop
343
+ pch test view patch/lantern@0.1.0 --wait # follows it until it's been played
344
+ pch test footage patch/lantern@0.1.0 # saves its pictures, and prints where
345
+ pch test review patch/lantern@0.1.0 --verdict pass --plays yes --harmful no \
346
+ --summary "It starts and plays" \
347
+ --moment "1 00:04 the title screen draws" --moment "1 00:10 a round of play"
348
+ pch release submit patch/lantern@0.1.0 # your yes: Patch reviews it, and it goes live
349
+ ```
350
+
351
+ - Patch's review after `pch release submit` can take several minutes, and nothing tells you when it's over: `pch status` says, and `--wait 50` holds its answer until something changes, for up to that many seconds.
352
+ - Only your team can find a release published on the `private` channel, and they play it signed in. It isn't locked: anyone who has a browser game's or a block demo's game address could load it, so don't put anything secret in it. The same goes for one in a private repo, though its source stays private. A mod's files aren't on the game host. Once a project that isn't private has a public release, only a public release changes what's played, so a newer private one can't be played yet. `pch status` and `pch project view` say which it is, and where. `pch release promote` puts it out for everyone.
353
+ - A passed test shows it starts and runs, not that it plays well. So before a game's first public release, `pch test review`, `pch test view` and `pch status` ask you to play it yourself, as `ask` with `--json`, in words for an agent to pass on. `pch guide` says how. Asking is all they do: nothing checks you played, and a later public release, a private one or a block's demo isn't asked.
354
+ - Nothing starts a test but `pch test start`, and nothing publishes but `pch release submit`. Each command says what comes next, as `next` with `--json`, and `pch status` does too.
355
+ - Patch Desktop has to be open and signed in on your PC. A test started from pch waits there until you press Start or Not now: only one asked for from a chat in Patch Desktop starts by itself. If it isn't open, the test says so, as `ask` with `--json`, in words for an agent to pass on.
356
+ - `--wait` with no number follows it to the end for someone at a terminal, and for 100 seconds with nobody there, since an agent's shell commands get about 2 minutes. If it isn't over by then, it says so and exits 0: run the next command to keep following.
357
+ - With a test open, `pch test start` follows that one, since starting another would cancel it. `pch test cancel` first to start over.
358
+ - A review needs a verdict, whether it plays, whether it does anything harmful, a summary, and at least two moments you saw, with at least one from each run: `"<run> <mm:ss> <what you saw>"`, with the time on a frame.
359
+ - A second `pch publish` of a version that's past draft submits nothing: a release never changes, so it says how to finish that one, or to move the version on.
360
+ - A mod players install some other way (`workshop` or `manual`) has no test, so `pch publish` submits it from its draft. A staff key publishes directly.
361
+
362
+ | Exit code | `pch test` |
363
+ | --------- | ------------------------------------------------------------------------------------------------------- |
364
+ | 0 | Passed, still going, or waiting for its review: `state` says which, and `next` what to run |
365
+ | 1 | Couldn't run, or was cancelled: `outcome` says why, and it needs starting again |
366
+ | 3 | Failed, on a check on the PC or in its review. A version that failed can't be tested or published again |
367
+
368
+ `pch publish` and `pch release promote` exit 0 when they leave a private draft that needs its test, since that's the usual first stop: with `--json`, `release.status` is `draft` and `next` says what to run. So a 0 from them doesn't mean published, as a release in review doesn't either. `pch release submit` exits 5 with `test_first` or `test_not_passed` until it has passed, and `pch publish` with `test_not_passed` for a draft whose test hasn't. Both exit 3 with `files_failed` when the file checks stop it, as `pch test start` does. Each error has what to run in `details.next`.
369
+
370
+ ### Blocks
371
+
372
+ A block is a system, effect, character, asset or tool for games. It's a strong starting point, not a part to use as it is: if one fits, start from it, change it and improve it, keeping its licence and credit. Nothing makes you use one.
373
+
374
+ A block's live demo is a browser release whose recipe has a `block` section: the controls Patch's page draws beside the demo, its presets and its triggers. `pch validate` checks the section as Patch does, and `pch publish` sends it with the release.
375
+
376
+ A block goes on its toolbox shelf when its repo is open, which can't be taken back, so start it with `pch new --visibility private` and let `pch guide`'s Publishing a block say what to have ready and when to ask. A tool with nothing to demo needs no release, so `pch validate` and `pch publish` don't apply to it.
377
+
378
+ ```json
379
+ {
380
+ "project": "patch/elemental-abilities",
381
+ "version": "0.2.0",
382
+ "files": "dist",
383
+ "recipe": {
384
+ "mode": "browser",
385
+ "block": {
386
+ "controls": [
387
+ {
388
+ "id": "element",
389
+ "label": "Element",
390
+ "type": "choice",
391
+ "essential": true,
392
+ "options": [
393
+ { "value": "ember", "label": "Ember", "group": "Warm", "image": "thumbs/ember.webp" },
394
+ { "value": "frost", "label": "Frost", "group": "Cold", "image": "thumbs/frost.webp" }
395
+ ]
396
+ },
397
+ { "id": "speed", "label": "Speed", "type": "number", "min": 0, "max": 4, "default": 1 }
398
+ ],
399
+ "presets": [
400
+ { "id": "blizzard", "label": "Blizzard", "values": { "element": "frost", "speed": 4 } }
401
+ ],
402
+ "triggers": [{ "id": "cast", "label": "Cast" }]
403
+ }
404
+ }
405
+ }
406
+ ```
407
+
408
+ - A control is a `number`, `choice`, `toggle` or `color`, and at most 5 are `essential`: the few big ones people see first.
409
+ - A choice's options can be in groups (`group`), and each can have a picture (`image`), drawn as a tile with its label under it. Every option has one or none does.
410
+ - A picture is a `.png`, `.webp`, `.gif` or `.jpg` file in the release, at its path from the release's top, and at most 48 KB. `pch validate` and `pch publish` find a missing or heavier one before anything is uploaded, with what to do in each issue's `message`.
411
+
412
+ ### Publishing from a repo
413
+
414
+ In a clone of the project's repo on Patch, `pch publish` records where the release came from: it tags the commit `v<version>`, pushes the tag, uploads the build and submits, then pushes the branch you're on, so the repo shows what you released. Anywhere else, it just uploads and submits. It signs git in with the login it's using, for that host and that push alone, so there's nothing to set up.
415
+ - **Uncommitted work is refused** (exit 2, `uncommitted_changes`). The release records the commit as what it was built from, and its review reads that commit's source, so a build with work that isn't in it would be a false record. Files git ignores don't count, so keep build output in `.gitignore`.
416
+ - **A version past `draft` is refused** (exit 5, `version_exists`) before any tag is made: a release never changes.
417
+ - **A tag on another commit is refused** (exit 5, `tag_exists`), here or on the remote: tags don't move. Move the version on, or, if an earlier publish left it behind and nothing uses it, take it back as the error says.
418
+ - **The same commit again resumes**, finding its tag and uploading only what's missing.
419
+ - **A branch is never forced.** If the repo's branch has commits yours doesn't, the release still goes ahead, the branch stays where it is, and pch says so, with what to run. It says the same if you're not on a branch.
420
+ - **A push the git host refuses** stops there (exit 3, `push_refused`, with its reasons in `details.issues`), leaving no tag behind. One ref that can't go refuses the whole push, so none of it lands. A draft the API refuses stops it the same way.
421
+ - **A full org** stops it too (exit 4, `storage_limit_reached`), in the git host's words, leaving no tag behind.
422
+ - **A push that's too big for the git host** goes [straight to where the repo's kept](#big-pushes), on a key for each push, as uploading can outlast one. It's the same push, with the same refusals, and the same way to retry it: `pch push`.
423
+
424
+ A repo whose home is GitHub takes its tag on GitHub: see [Connecting a GitHub repo](#connecting-a-github-repo).
425
+
426
+ This is on the `repos` branch until repos launch.
427
+
428
+ ### Connecting a GitHub repo
429
+
430
+ A repo on GitHub can be a project's home. Patch keeps none of its files: releases name its commits, and their review reads them from GitHub. In a clone of the repo:
431
+
432
+ ```sh
433
+ pch connect # a new project of yours, named after the repo
434
+ pch connect patch/lantern # or the project you name, made if it isn't there yet
435
+ ```
436
+
437
+ `pch connect` finds the repo from `origin`. The first time, it needs two things that only you can do on GitHub, and says what they are:
438
+ 1. **Link your GitHub account to Patch**, with a code to enter on GitHub, so Patch knows who you are there.
439
+ 2. **Install Patch's GitHub app** on the repo's account, with access to the repo. On an organisation it takes an owner, who installs it or approves your request, and until then it's waiting on an owner. If the app is there without this repo, add a repo in the app's settings on GitHub.
440
+
441
+ At a terminal, pch opens each page and waits. With nobody at the terminal, as for an agent, it says what's needed and exits 6, with `ask` (words to pass on), `link` and, for the code, `user_code` in `details`. Once the person has done it, run `pch connect` again: it goes on with the same code rather than a new one. `--wait` makes it wait anyway.
442
+ - **Connecting takes admin or maintain on the repo** (exit 4, `github_role_needed`), so a collaborator can't list someone else's game.
443
+ - **A repo has one project.** Connecting it to another project is refused (exit 5, `already_connected`), saying where it is. In the clone of anyone on the repo who can see that project on Patch, `pch connect` names it in `patch.json`. Neither pch nor Patch's MCP tools disconnect a repo: that project's team does, on its source tab on Patch, which the refusal links to (`link` in `details`).
444
+ - **A project you can't see isn't named**, such as a private one of an org you're not in. That's refused too (exit 5, `already_connected`, with `project: null` in `details`), and nothing is written. Ask whoever connected the repo to add you to that org, then run `pch connect` again.
445
+ - **`patch.json` names the project** once it's connected: pch adds `project` to yours, or starts one at version 0.1.0. Commit it. A `patch.json` naming another project stops it before anything happens.
446
+ - **Who can find it and read its source on Patch** follows GitHub: open for a public repo with a licence of its own, and private for any other, unless `--visibility` says otherwise. Open needs a public repo (`needs_public`) with a licence GitHub can find in it, even one it can't name (`needs_license`), until the Patch licence is ready. Either refusal (exit 2) says what to change on GitHub, or to connect it as `closed_source` or `private` with `--visibility`.
447
+
448
+ From then on, `pch publish` in a clone tags the commit `v<version>` and pushes the tag to GitHub with your own git login, from whichever remote is the repo on GitHub, before it makes the release. It doesn't push your branch there, as that can set off your own rules and builds, but it says if the branch doesn't have the commit. Patch's key never goes to GitHub.
449
+ - **A clone with no remote on GitHub** is refused (exit 2, `no_github_remote`), with the `git remote add` that fixes it. A clone through Patch's address reads from GitHub, but can't push there.
450
+ - **A login GitHub doesn't take** stops it (exit 4, `github_sign_in_failed`): sign git in to GitHub as someone who can push to the repo. **A push GitHub refuses**, such as a tag its rules protect, stops it too (exit 3, `push_refused`, with GitHub's reasons in `details.issues`). Neither leaves a tag behind.
451
+
452
+ `pch status` shows the repo's home, as `Home: GitHub, patch/lantern · https://github.com/patch/lantern`.
453
+
454
+ | Exit code | `pch connect` |
455
+ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
456
+ | 0 | Connected, now or already |
457
+ | 1 | Connecting GitHub isn't on where you're signed in yet (`github_off`) |
458
+ | 2 | Not a clone of a repo on GitHub (`not_github`, `no_origin`), `patch.json` names another project (`project_mismatch`), or Open was asked for a repo that can't be (`needs_public`, `needs_license`) |
459
+ | 4 | Not admin or maintain on the repo (`github_role_needed`), or linking was turned down on GitHub |
460
+ | 5 | Connected to another project already, or to one you can't see (`already_connected`) |
461
+ | 6 | Only a person can go on, on GitHub: `github_not_linked`, `github_not_installed`, `github_install_requested`, `github_install_suspended`, `github_repo_not_added` or `github_repo_not_found` |
462
+
463
+ This is on the `repos` branch, and Patch has it behind a flag, until it launches.
464
+
465
+ ### Environments and credentials
466
+
467
+ | Flag | Env var | Meaning |
468
+ | ----------- | ------------- | ------------------------------------------------------------ |
469
+ | `--env` | `PCH_ENV` | `production` (default), `staging` or `local` |
470
+ | `--account` | `PCH_ACCOUNT` | Which saved login to use |
471
+ | | `PCH_TOKEN` | An API key; overrides any saved login. For agents and CI. |
472
+ | | `PCH_API_URL` | Point at another server, e.g. `http://localhost:3000` |
473
+ | | `PCH_GIT_URL` | Where that server's repos live, e.g. `http://localhost:8787` |
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` |
476
+
477
+ ## Your agent
478
+
479
+ `pch new` and `pch init` write `AGENTS.md`, which Claude Code, Codex and Cursor read when they start in that folder: what Patch is, how to run, check and publish here, and that the person's wishes come first. `pch new` commits it. They never overwrite one, and a folder with a `CLAUDE.md` of its own is left alone too. For a clone or a remix with none, `pch clone` says so and `pch agent guide` adds it. A release doesn't include it.
480
+
481
+ A game's brief keeps your idea with it, so you don't repeat it in every new session: `BRIEF.md` at the top of the repo, with pictures in `brief/`, holding the idea, what the game is going for and your rules. `AGENTS.md` tells agents to read it first and follow it, and what you say to them now wins over it. `pch agent brief` asks you and writes your own words, with almost nothing added, and an agent running it gets the questions to ask you. Nothing makes you have one, and pch never overwrites one. In an Open repo it's public, like every other file, so keep anything private out. A remix brings its parent's: that's someone else's idea, so keep what you want, then change or delete it. A release leaves out `BRIEF.md` and `brief/` at the top of the repo, so a game can't use that folder for its own files unless they're in a build folder.
482
+
483
+ After `pch login`, run `pch agent install` and restart your agent: it gets Patch's tools from `pch mcp`, a command it runs as one of its MCP servers. `pch agent install` finds Claude Code, Codex and Cursor on this PC, or takes one by name. It writes through each agent's own command where it has one, and into `.cursor/mcp.json` in this folder for Cursor. It names the environment it ran on, so `pch --env staging agent install` adds a server called `patch-staging`, and exit 6 means it couldn't do it here and says what to do by hand. To add it yourself, the server is `pch mcp` (`cmd /c pch mcp` on Windows). It hands each message to Patch's hosted server, `<origin>/api/mcp`, as the account pch is signed in as, so the agent's settings hold a command and never a key. Most agents load a new server when they restart; until then, `pch guide` prints the same guide the `guide` tool gives. A login made inside Claude Code names its key after it, `Claude Code on <host>`.
484
+
485
+ ## Develop
486
+
487
+ Working on pch takes Node 24 and pnpm. `pnpm pch` runs it from source, in pch's own folder. For commands that work in the folder you're in, such as `clone`, use an alias:
488
+
489
+ ```sh
490
+ pnpm install
491
+ pnpm pch --help
492
+ alias pch="$PWD/node_modules/.bin/tsx $PWD/src/cli.ts"
493
+ ```
494
+
495
+ The API contract and the recipe validator come from the Patch monorepo, so pch checks exactly what the API checks:
496
+
497
+ ```sh
498
+ pnpm sync ../patch # copies the contract and @patchgg/manifest's recipe validator
499
+ pnpm generate # rebuilds src/api/schema.ts from contract/openapi.json
500
+ pnpm lint && pnpm typecheck && pnpm test
501
+ ```
502
+
503
+ CI fails if `src/api/schema.ts` doesn't match `contract/openapi.json`.
504
+
505
+ ### What npm installs
506
+
507
+ `pnpm build` makes `dist/`. `dist/cli.js` holds pch and every dependency, so running it takes only Node, with the licence texts and the dependencies' notices beside it.
508
+
509
+ ```sh
510
+ pnpm smoke # packs it, installs it as npm would, and runs it from an empty folder
511
+ PCH_BIN=dist/cli.js pnpm test # the tests that run pch as a command, against the build
512
+ ```
513
+
514
+ CI runs both, the first on Windows, macOS and Linux.
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
+
527
+ ### Releasing
528
+
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.
545
+
546
+ ### End to end
547
+
548
+ `pnpm e2e -- --env staging` runs pch from source against a real environment: it creates `patch/demo-mod`, validates and publishes [`examples/demo-mod`](examples/demo-mod), downloads every file back to check its SHA-256, and checks the game page. It needs a staff login on that environment, which `pnpm --filter @patch/api seed -- --env staging` in the monorepo writes to your pch config.
549
+
550
+ ## License
551
+
552
+ MIT. The packages bundled into pch keep their own licences, in `dist/THIRD-PARTY-NOTICES.txt`.