@patchgg/cli 0.1.1 → 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 +61 -59
  2. package/dist/cli.js +293 -79
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -29,64 +29,66 @@ pch media add your-name/my-game cover.png --kind cover
29
29
  pch project update your-name/my-game --summary "One line about it"
30
30
  ```
31
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": { … } }`.
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": { … } }`.
90
92
 
91
93
  | Exit code | Meaning |
92
94
  | --------- | --------------------------------------------------------------------------------------------- |
@@ -151,7 +153,7 @@ It never pushes a submodule as well, as git's own push into one would take this
151
153
 
152
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.
153
155
 
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.
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.
155
157
 
156
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.
157
159
 
package/dist/cli.js CHANGED
@@ -119,7 +119,9 @@ function fromResponse(response, body) {
119
119
  const error62 = body?.error;
120
120
  const code = error62?.code ?? `http_${response.status}`;
121
121
  const message = error62?.message ?? `The API answered ${response.status} ${response.statusText}`;
122
- return new CliError(exitCodeFor(response.status), code, message, error62?.details);
122
+ const failure2 = new CliError(exitCodeFor(response.status), code, message, error62?.details);
123
+ failure2.fromApi = true;
124
+ return failure2;
123
125
  }
124
126
  function exitCodeFor(status2) {
125
127
  if (status2 === 401 || status2 === 403 || status2 === 507) return EXIT.auth;
@@ -163,6 +165,8 @@ var init_errors = __esm({
163
165
  exitCode;
164
166
  code;
165
167
  details;
168
+ /** Patch's API said it, rather than pch itself. */
169
+ fromApi;
166
170
  };
167
171
  }
168
172
  });
@@ -172,7 +176,7 @@ var VERSION;
172
176
  var init_version = __esm({
173
177
  "src/version.ts"() {
174
178
  "use strict";
175
- VERSION = true ? "0.1.1" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
179
+ VERSION = true ? "0.1.2" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
176
180
  }
177
181
  });
178
182
 
@@ -39977,6 +39981,12 @@ init_guide_words();
39977
39981
  // src/output.ts
39978
39982
  var import_picocolors = __toESM(require_picocolors(), 1);
39979
39983
  init_errors();
39984
+ var UNICODE = process.platform !== "win32" || Boolean(process.env.WT_SESSION || process.env.TERM_PROGRAM);
39985
+ var TICK = UNICODE ? "\u2714" : "\u221A";
39986
+ var SPINNER = UNICODE ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["-", "\\", "|", "/"];
39987
+ function forAPerson(json2, agent2) {
39988
+ return !json2 && Boolean(process.stdout.isTTY && process.stderr.isTTY) && !agent2;
39989
+ }
39980
39990
  var Output = class {
39981
39991
  constructor(json2) {
39982
39992
  this.json = json2;
@@ -39997,6 +40007,21 @@ var Output = class {
39997
40007
  if (!this.json) process.stderr.write(`${import_picocolors.default.dim("\u203A")} ${text}
39998
40008
  `);
39999
40009
  }
40010
+ /**
40011
+ * A spinner on stderr, for a person at a terminal while pch waits on them. Call what it returns
40012
+ * to stop it: that takes its line away, so whatever comes next starts on a clean one.
40013
+ */
40014
+ spin(text) {
40015
+ let frame = 0;
40016
+ const draw = () => process.stderr.write(`\r${import_picocolors.default.cyan(SPINNER[frame++ % SPINNER.length])} ${text}`);
40017
+ draw();
40018
+ const timer = setInterval(draw, 80);
40019
+ timer.unref();
40020
+ return () => {
40021
+ clearInterval(timer);
40022
+ process.stderr.write("\r\x1B[2K");
40023
+ };
40024
+ }
40000
40025
  /**
40001
40026
  * How far a long job has got. With --json it's a line of JSON on stderr, so whoever's waiting can
40002
40027
  * see it's moving while stdout stays the one result.
@@ -40006,15 +40031,17 @@ var Output = class {
40006
40031
  `);
40007
40032
  else this.step(text);
40008
40033
  }
40009
- error(error62) {
40034
+ error(error62, command) {
40010
40035
  const failure2 = error62 instanceof CliError ? error62 : new CliError(
40011
40036
  EXIT.error,
40012
40037
  "unexpected_error",
40013
40038
  error62 instanceof Error ? error62.message : String(error62)
40014
40039
  );
40040
+ const feedback2 = feedbackFor(failure2, command);
40015
40041
  if (this.json) {
40016
40042
  const body = {
40017
- error: { code: failure2.code, message: failure2.message, details: failure2.details }
40043
+ error: { code: failure2.code, message: failure2.message, details: failure2.details },
40044
+ ...feedback2 ? { feedback: feedback2 } : {}
40018
40045
  };
40019
40046
  process.stderr.write(`${JSON.stringify(body)}
40020
40047
  `);
@@ -40027,10 +40054,19 @@ var Output = class {
40027
40054
  process.stderr.write(` ${import_picocolors.default.dim(issue2.path || "\xB7")} ${issue2.message}
40028
40055
  `);
40029
40056
  }
40057
+ if (feedback2) process.stderr.write(`${import_picocolors.default.dim(feedback2.say)}
40058
+ `);
40030
40059
  }
40031
40060
  return failure2.exitCode;
40032
40061
  }
40033
40062
  };
40063
+ function feedbackFor(failure2, command) {
40064
+ if (!failure2.fromApi && failure2.exitCode !== EXIT.error) return null;
40065
+ const tool = command ? ` --tool "${command}"` : "";
40066
+ return {
40067
+ say: `If this error didn't tell you what to do, or it looks like Patch's fault, tell Patch's team, then carry on: pch feedback send --kind call_error${tool} --error-code ${failure2.code} "<what got in your way>"`
40068
+ };
40069
+ }
40034
40070
  function issuesOf(details) {
40035
40071
  const issues = details?.issues;
40036
40072
  return Array.isArray(issues) ? issues : [];
@@ -40077,7 +40113,7 @@ async function login(ctx, options) {
40077
40113
  })
40078
40114
  )
40079
40115
  }));
40080
- const name = ctx.target.accountName ?? (target.env === "production" ? "default" : target.env);
40116
+ const name = ctx.target.accountName ?? defaultAccount(target.env);
40081
40117
  const account2 = {
40082
40118
  env: target.env,
40083
40119
  token: key.key,
@@ -40103,9 +40139,13 @@ async function login(ctx, options) {
40103
40139
  look_first: LOOK_FIRST
40104
40140
  },
40105
40141
  () => {
40106
- out.line(
40107
- `Signed in as ${import_picocolors2.default.bold(me.username ? `@${me.username}` : me.email)} on ${target.env} (account "${name}").`
40108
- );
40142
+ const who = import_picocolors2.default.bold(me.username ? `@${me.username}` : me.email);
40143
+ if (forAPerson(out.json, runningIn())) {
40144
+ const where = target.env === "production" ? "" : ` on ${target.env}`;
40145
+ const named = name === defaultAccount(target.env) ? "" : import_picocolors2.default.dim(` (account "${name}")`);
40146
+ return signedIn(ctx, `Signed in as ${who}${where}${named}`, me.username, git, gitIdentity);
40147
+ }
40148
+ out.line(`Signed in as ${who} on ${target.env} (account "${name}").`);
40109
40149
  if (!me.username) out.line(`Choose a username at ${target.origin}/welcome to publish.`);
40110
40150
  if (git.helper) out.line(`Git is signed in too: Patch repos at ${git.url} just work.`);
40111
40151
  if (gitIdentity) out.line(gitIdentity);
@@ -40132,15 +40172,53 @@ async function loginWithKey(ctx) {
40132
40172
  look_first: LOOK_FIRST
40133
40173
  },
40134
40174
  () => {
40135
- out.line(
40136
- `PCH_TOKEN works: ${import_picocolors2.default.bold(me.username ? `@${me.username}` : me.email)} on ${target.env}.`
40137
- );
40175
+ const who = import_picocolors2.default.bold(me.username ? `@${me.username}` : me.email);
40176
+ if (forAPerson(out.json, runningIn())) {
40177
+ const where = target.env === "production" ? "" : ` on ${target.env}`;
40178
+ return signedIn(ctx, `PCH_TOKEN works: ${who}${where}`, me.username, git, gitIdentity);
40179
+ }
40180
+ out.line(`PCH_TOKEN works: ${who} on ${target.env}.`);
40138
40181
  if (git.helper) out.line(`Git is signed in too: Patch repos at ${git.url} just work.`);
40139
40182
  if (gitIdentity) out.line(gitIdentity);
40140
40183
  out.line(LOOK_FIRST);
40141
40184
  }
40142
40185
  );
40143
40186
  }
40187
+ function defaultAccount(env) {
40188
+ return env === "production" ? "default" : env;
40189
+ }
40190
+ function signedIn(ctx, headline, username, git, gitIdentity) {
40191
+ const { out, target } = ctx;
40192
+ out.line(`${import_picocolors2.default.green(TICK)} ${headline}`);
40193
+ if (git.helper) out.line(import_picocolors2.default.dim(" Git works with Patch repos too."));
40194
+ if (!username) out.line(` Choose a username at ${target.origin}/welcome before you publish.`);
40195
+ if (gitIdentity) out.line(indented(gitIdentity));
40196
+ const pch = pchOn(target.env);
40197
+ const next = [
40198
+ [`${pch} search "<idea>"`, "find a game or block to start from"],
40199
+ [`${pch} agent install`, "give your coding agent Patch's tools"],
40200
+ [`${pch} guide`, "how to make, test and publish"]
40201
+ ];
40202
+ const width = Math.max(...next.map(([command]) => command.length));
40203
+ out.line();
40204
+ for (const [command, what] of next) {
40205
+ out.line(` ${import_picocolors2.default.cyan(command.padEnd(width))} ${import_picocolors2.default.dim(what)}`);
40206
+ }
40207
+ out.line();
40208
+ }
40209
+ function indented(text) {
40210
+ const width = Math.min(process.stdout.columns || 80, 100) - 2;
40211
+ const lines = [];
40212
+ let line = "";
40213
+ for (const word of text.split(" ")) {
40214
+ if (line && line.length + 1 + word.length > width) {
40215
+ lines.push(line);
40216
+ line = word;
40217
+ } else line = line ? `${line} ${word}` : word;
40218
+ }
40219
+ if (line) lines.push(line);
40220
+ return lines.map((each) => ` ${each}`).join("\n");
40221
+ }
40144
40222
  async function setUpGit(ctx) {
40145
40223
  const { gitUrl: url2 } = ctx.target;
40146
40224
  if (!url2) return { url: url2, helper: false, lfs: false };
@@ -40161,19 +40239,55 @@ async function withDeviceSession(ctx, options, use) {
40161
40239
  if (!started.response.ok) throw authError(started.body, "Couldn't start a login");
40162
40240
  const device = started.body;
40163
40241
  const code = formatUserCode(device.user_code);
40164
- process.stderr.write(
40165
- `This waits for a person to approve it in a browser, which can take a few minutes. The code only works while this keeps running, so don't put a short timeout on it.
40242
+ const opened = options.browser !== false && Boolean(process.stdout.isTTY);
40243
+ if (opened) openBrowser(device.verification_uri_complete);
40244
+ let stopWaiting = () => {
40245
+ };
40246
+ if (forAPerson(out.json, runningIn())) {
40247
+ const how = opened ? "It's open in your browser. Approve it there if it shows this code." : "Open the link, enter the code and approve it.";
40248
+ process.stderr.write(
40249
+ `
40250
+ ${import_picocolors2.default.dim("Open")} ${import_picocolors2.default.cyan(device.verification_uri)}
40251
+ ${import_picocolors2.default.dim("Code")} ${import_picocolors2.default.bold(code)}
40252
+
40253
+ ${import_picocolors2.default.dim(how)}
40254
+
40255
+ `
40256
+ );
40257
+ stopWaiting = out.spin("Waiting for you to approve\u2026");
40258
+ } else {
40259
+ process.stderr.write(
40260
+ `This waits for a person to approve it in a browser, which can take a few minutes. The code only works while this keeps running, so don't put a short timeout on it.
40166
40261
 
40167
40262
  To sign in, open ${import_picocolors2.default.bold(device.verification_uri)} and enter ${import_picocolors2.default.bold(code)}
40168
40263
 
40169
40264
  `
40170
- );
40171
- if (options.browser !== false && process.stdout.isTTY)
40172
- openBrowser(device.verification_uri_complete);
40173
- out.step("Waiting for you to approve in the browser\u2026");
40265
+ );
40266
+ out.step("Waiting for you to approve in the browser\u2026");
40267
+ }
40268
+ let sessionToken = null;
40269
+ try {
40270
+ sessionToken = await approval(ctx, device, options);
40271
+ } finally {
40272
+ stopWaiting();
40273
+ }
40274
+ if (!sessionToken) {
40275
+ throw new CliError(EXIT.auth, "expired_token", "The code expired. Run pch login again.");
40276
+ }
40277
+ try {
40278
+ return await use(createApi(target, { token: sessionToken }));
40279
+ } finally {
40280
+ await fetch(`${auth2}/sign-out`, {
40281
+ method: "POST",
40282
+ headers: { authorization: `Bearer ${sessionToken}`, "content-type": "application/json" },
40283
+ body: "{}"
40284
+ }).catch(() => void 0);
40285
+ }
40286
+ }
40287
+ async function approval(ctx, device, options) {
40288
+ const auth2 = `${ctx.target.origin}/api/auth`;
40174
40289
  let interval = (options.interval ?? device.interval) * 1e3;
40175
40290
  const deadline = Date.now() + device.expires_in * 1e3;
40176
- let sessionToken = null;
40177
40291
  while (Date.now() < deadline) {
40178
40292
  await sleep(interval);
40179
40293
  const polled = await postJson(`${auth2}/device/token`, {
@@ -40181,10 +40295,7 @@ To sign in, open ${import_picocolors2.default.bold(device.verification_uri)} and
40181
40295
  device_code: device.device_code,
40182
40296
  client_id: CLIENT_ID
40183
40297
  });
40184
- if (polled.response.ok) {
40185
- sessionToken = polled.body.access_token;
40186
- break;
40187
- }
40298
+ if (polled.response.ok) return polled.body.access_token;
40188
40299
  const error62 = polled.body.error;
40189
40300
  if (error62 === "authorization_pending") continue;
40190
40301
  if (error62 === "slow_down") {
@@ -40197,18 +40308,7 @@ To sign in, open ${import_picocolors2.default.bold(device.verification_uri)} and
40197
40308
  if (error62 === "expired_token") break;
40198
40309
  throw authError(polled.body, "The login failed");
40199
40310
  }
40200
- if (!sessionToken) {
40201
- throw new CliError(EXIT.auth, "expired_token", "The code expired. Run pch login again.");
40202
- }
40203
- try {
40204
- return await use(createApi(target, { token: sessionToken }));
40205
- } finally {
40206
- await fetch(`${auth2}/sign-out`, {
40207
- method: "POST",
40208
- headers: { authorization: `Bearer ${sessionToken}`, "content-type": "application/json" },
40209
- body: "{}"
40210
- }).catch(() => void 0);
40211
- }
40311
+ return null;
40212
40312
  }
40213
40313
  function runningIn(vars = process.env) {
40214
40314
  if (vars.CLAUDECODE === "1") return "Claude Code";
@@ -42598,8 +42698,8 @@ async function agentInstall(ctx, named, options = {}) {
42598
42698
  const agents = [];
42599
42699
  for (const agent2 of wanted) agents.push(await install(agent2, server));
42600
42700
  const manual = agents.filter((entry) => entry.state === "manual");
42601
- const signedIn = Boolean(ctx.target.token);
42602
- ctx.out.result({ server, agents, signed_in: signedIn }, () => {
42701
+ const signedIn2 = Boolean(ctx.target.token);
42702
+ ctx.out.result({ server, agents, signed_in: signedIn2 }, () => {
42603
42703
  for (const entry of agents) {
42604
42704
  const name = AGENT_NAMES[entry.agent];
42605
42705
  if (entry.state === "manual") {
@@ -42611,7 +42711,7 @@ async function agentInstall(ctx, named, options = {}) {
42611
42711
  }
42612
42712
  }
42613
42713
  ctx.out.line(import_picocolors6.default.dim(`It runs ${commandLine(server)}, and holds no key.`));
42614
- if (!signedIn) ctx.out.line(`Sign in first, or its tools won't work: ${pchLogin(ctx)}.`);
42714
+ if (!signedIn2) ctx.out.line(`Sign in first, or its tools won't work: ${pchLogin(ctx)}.`);
42615
42715
  ctx.out.line(
42616
42716
  "Restart your agent to load its tools. Until then, pch guide prints the same guide."
42617
42717
  );
@@ -43173,9 +43273,9 @@ async function projectView(ctx, ref) {
43173
43273
  );
43174
43274
  }
43175
43275
  if (project2.urls?.play) {
43176
- const signedIn = ownOnly?.channel === "private" && !unserved ? ", signed in" : "";
43276
+ const signedIn2 = ownOnly?.channel === "private" && !unserved ? ", signed in" : "";
43177
43277
  const which = unserved && project2.latest_release ? ` (${project2.latest_release.version})` : "";
43178
- ctx.out.line(`Plays at: ${project2.urls.play}${signedIn}${which}`);
43278
+ ctx.out.line(`Plays at: ${project2.urls.play}${signedIn2}${which}`);
43179
43279
  }
43180
43280
  ctx.out.line(
43181
43281
  `Source: ${repo ? VISIBILITY_LABELS[repo.visibility] : SOURCE_LABELS[project2.source_visibility]} \xB7 code ${project2.code_license} \xB7 art ${project2.art_license}`
@@ -46075,6 +46175,11 @@ function showReview({ out }, project2, review) {
46075
46175
  out.line(
46076
46176
  ` ${ours ? nextStep(name, review.status, review.test, cloud).say : plain(review.next)}`
46077
46177
  );
46178
+ if (review.feedback_ask) {
46179
+ out.line(
46180
+ ` ${plain(review.feedback_ask.ask.replaceAll("send_feedback", "pch feedback send"))}`
46181
+ );
46182
+ }
46078
46183
  for (const run3 of review.check_runs) {
46079
46184
  out.line(` Checks (${run3.kind}): ${run3.status}${run3.summary ? `, ${run3.summary}` : ""}`);
46080
46185
  for (const finding of run3.findings) {
@@ -46995,8 +47100,83 @@ function outFolder(out, byDefault) {
46995
47100
  return byDefault ? "" : void 0;
46996
47101
  }
46997
47102
 
46998
- // src/commands/git.ts
47103
+ // src/commands/feedback.ts
46999
47104
  var import_picocolors16 = __toESM(require_picocolors(), 1);
47105
+ var FEEDBACK_KINDS = ["failed", "bug", "call_error", "unhelpful", "idea"];
47106
+ var FEEDBACK_SEVERITIES = ["blocked", "slowed", "annoyed"];
47107
+ async function feedbackSend(ctx, summary, options) {
47108
+ requireToken(ctx.target);
47109
+ const sent = await unwrap(
47110
+ ctx.api.POST("/feedback", {
47111
+ body: {
47112
+ kind: options.kind,
47113
+ summary,
47114
+ goal: options.goal,
47115
+ expected: options.expected,
47116
+ actual: options.actual,
47117
+ tried: options.tried,
47118
+ workaround: options.workaround,
47119
+ cost: options.cost,
47120
+ severity: options.severity,
47121
+ add_to: options.addTo,
47122
+ details: options.details,
47123
+ would_help: options.wouldHelp,
47124
+ tool: options.tool,
47125
+ error_code: options.errorCode,
47126
+ project: options.project,
47127
+ agent: options.agent
47128
+ }
47129
+ })
47130
+ );
47131
+ const shown = { ...sent, next: sent.next.replaceAll("send_feedback", "pch feedback send") };
47132
+ ctx.out.result(shown, () => {
47133
+ ctx.out.line(
47134
+ `${options.addTo ? "Added to your report" : "Told Patch's team"}: ${import_picocolors16.default.bold(plain(sent.summary))}`
47135
+ );
47136
+ if (sent.known) {
47137
+ ctx.out.line(`Patch's team already answered a report like it: ${plain(sent.known.answer)}`);
47138
+ }
47139
+ if (sent.follow_up) {
47140
+ const flag = sent.follow_up.field.replaceAll("_", "-");
47141
+ ctx.out.line(`One more thing would help most: ${plain(sent.follow_up.question)}`);
47142
+ ctx.out.line(
47143
+ import_picocolors16.default.dim(
47144
+ ` pch feedback send --kind ${sent.kind} --add-to ${sent.id} --${flag} "..." "${plain(sent.summary)}"`
47145
+ )
47146
+ );
47147
+ } else {
47148
+ ctx.out.line(import_picocolors16.default.dim(plain(shown.next)));
47149
+ }
47150
+ });
47151
+ }
47152
+ async function feedbackList(ctx) {
47153
+ requireToken(ctx.target);
47154
+ const { data } = await unwrap(ctx.api.GET("/feedback"));
47155
+ ctx.out.result({ data }, () => {
47156
+ if (!data.length) {
47157
+ ctx.out.line("No reports yet. pch feedback send tells Patch's team what got in your way.");
47158
+ return;
47159
+ }
47160
+ for (const report of data) {
47161
+ ctx.out.line(
47162
+ `${import_picocolors16.default.dim(report.created_at.slice(0, 10))} ${statusWord(report.status)} ${plain(report.summary)}`
47163
+ );
47164
+ if (report.answer) ctx.out.line(` ${import_picocolors16.default.dim("Patch:")} ${plain(report.answer)}`);
47165
+ if (report.status === "open" && report.quality < 50) {
47166
+ ctx.out.line(import_picocolors16.default.dim(` Add to it: pch feedback send --add-to ${report.id} ...`));
47167
+ }
47168
+ }
47169
+ });
47170
+ }
47171
+ function statusWord(status2) {
47172
+ if (status2 === "fixed") return import_picocolors16.default.green("fixed ");
47173
+ if (status2 === "planned") return import_picocolors16.default.cyan("planned ");
47174
+ if (status2 === "declined") return import_picocolors16.default.dim("declined");
47175
+ return import_picocolors16.default.yellow("open ");
47176
+ }
47177
+
47178
+ // src/commands/git.ts
47179
+ var import_picocolors17 = __toESM(require_picocolors(), 1);
47000
47180
  import { stat as stat5 } from "node:fs/promises";
47001
47181
  import { basename as basename6, join as join17, resolve as resolve13 } from "node:path";
47002
47182
 
@@ -47731,8 +47911,8 @@ async function push(ctx, given) {
47731
47911
  },
47732
47912
  () => {
47733
47913
  const list = sent.map(({ ref, status: status2 }) => `${shortRef(ref)}${status2 === "deleted" ? " (deleted)" : ""}`).join(", ");
47734
- if (list) out.line(`${dryRun ? "Would push" : "Pushed"} ${list} to ${import_picocolors16.default.bold(repo)}.`);
47735
- else if (!quiet) out.line(`Nothing new to push to ${import_picocolors16.default.bold(repo)}.`);
47914
+ if (list) out.line(`${dryRun ? "Would push" : "Pushed"} ${list} to ${import_picocolors17.default.bold(repo)}.`);
47915
+ else if (!quiet) out.line(`Nothing new to push to ${import_picocolors17.default.bold(repo)}.`);
47736
47916
  if (pushed.notice) out.line(pushed.notice);
47737
47917
  }
47738
47918
  );
@@ -47779,10 +47959,10 @@ async function clone2(ctx, ref, dir) {
47779
47959
  brief: brief ? BRIEF_FILE : null
47780
47960
  },
47781
47961
  () => {
47782
- out.line(`Cloned ${import_picocolors16.default.bold(`${project2.owner}/${project2.slug}`)} into ${basename6(into)}`);
47962
+ out.line(`Cloned ${import_picocolors17.default.bold(`${project2.owner}/${project2.slug}`)} into ${basename6(into)}`);
47783
47963
  if (commitsAs) out.line(saidCommitsAs(commitsAs));
47784
- out.line(import_picocolors16.default.dim("Commit and push with plain git."));
47785
- if (!guide2) out.line(import_picocolors16.default.dim("It has no guide for coding agents: pch agent guide adds one."));
47964
+ out.line(import_picocolors17.default.dim("Commit and push with plain git."));
47965
+ if (!guide2) out.line(import_picocolors17.default.dim("It has no guide for coding agents: pch agent guide adds one."));
47786
47966
  if (brief) {
47787
47967
  out.line(
47788
47968
  repo.forked_from ? `Its ${BRIEF_FILE} came from ${repo.forked_from}, so it's that game's idea: keep what you want, then change or delete it.` : `Its ${BRIEF_FILE} is this game's idea and rules: read it first.`
@@ -47793,7 +47973,7 @@ async function clone2(ctx, ref, dir) {
47793
47973
  }
47794
47974
 
47795
47975
  // src/commands/media.ts
47796
- var import_picocolors17 = __toESM(require_picocolors(), 1);
47976
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
47797
47977
  import { readFile as readFile14 } from "node:fs/promises";
47798
47978
  import { basename as basename7, extname as extname4, resolve as resolve14 } from "node:path";
47799
47979
  import { setTimeout as sleep8 } from "node:timers/promises";
@@ -47889,11 +48069,11 @@ function showAdded(ctx, ref, media2) {
47889
48069
  );
47890
48070
  }
47891
48071
  ctx.out.result(media2, () => {
47892
- ctx.out.line(`${import_picocolors17.default.green(`Added the ${media2.kind}`)} to ${import_picocolors17.default.bold(ref)}: ${media2.status}.`);
47893
- if (media2.status === "ready") ctx.out.line(import_picocolors17.default.dim(media2.url));
48072
+ ctx.out.line(`${import_picocolors18.default.green(`Added the ${media2.kind}`)} to ${import_picocolors18.default.bold(ref)}: ${media2.status}.`);
48073
+ if (media2.status === "ready") ctx.out.line(import_picocolors18.default.dim(media2.url));
47894
48074
  else {
47895
48075
  ctx.out.line(
47896
- import_picocolors17.default.dim(
48076
+ import_picocolors18.default.dim(
47897
48077
  `It shows once it's ready, which takes seconds for a picture, and for a clip about as long as it plays. Check with pch media list ${ref}.`
47898
48078
  )
47899
48079
  );
@@ -47913,7 +48093,7 @@ async function mediaList(ctx, ref) {
47913
48093
  for (const entry of media2.data) {
47914
48094
  const failed3 = entry.status === "failed" && entry.error ? `: ${entry.error}` : "";
47915
48095
  ctx.out.line(
47916
- `${entry.id} ${entry.kind.padEnd(10)} ${entry.status.padEnd(10)} ${formatSize(entry.size).padStart(8)}${failed3}${entry.alt ? import_picocolors17.default.dim(` ${entry.alt}`) : ""}`
48096
+ `${entry.id} ${entry.kind.padEnd(10)} ${entry.status.padEnd(10)} ${formatSize(entry.size).padStart(8)}${failed3}${entry.alt ? import_picocolors18.default.dim(` ${entry.alt}`) : ""}`
47917
48097
  );
47918
48098
  }
47919
48099
  });
@@ -47941,7 +48121,7 @@ async function mediaOrder(ctx, ref, ids) {
47941
48121
  }
47942
48122
 
47943
48123
  // src/commands/publish.ts
47944
- var import_picocolors18 = __toESM(require_picocolors(), 1);
48124
+ var import_picocolors19 = __toESM(require_picocolors(), 1);
47945
48125
  var import_semver2 = __toESM(require_semver2(), 1);
47946
48126
  import { open as open2, readFile as readFile15 } from "node:fs/promises";
47947
48127
  init_errors();
@@ -48048,7 +48228,7 @@ async function publish(ctx, dir, options) {
48048
48228
  out.result(result, () => {
48049
48229
  if (!submitted) {
48050
48230
  out.line(
48051
- `Uploaded ${import_picocolors18.default.bold(name)} as a private draft. It can't be published until it has passed a test on your PC, in Patch Desktop, where someone presses Start.`
48231
+ `Uploaded ${import_picocolors19.default.bold(name)} as a private draft. It can't be published until it has passed a test on your PC, in Patch Desktop, where someone presses Start.`
48052
48232
  );
48053
48233
  out.line(privateWords(release2.kind, true));
48054
48234
  if (openCode) out.line(openCode);
@@ -48066,7 +48246,7 @@ async function publish(ctx, dir, options) {
48066
48246
  ` ${entry.path.padEnd(40)} ${formatSize(entry.size).padStart(8)} sha256 ${entry.sha256}`
48067
48247
  );
48068
48248
  }
48069
- out.line(import_picocolors18.default.dim(url2));
48249
+ out.line(import_picocolors19.default.dim(url2));
48070
48250
  });
48071
48251
  }
48072
48252
  function fromCommit(source) {
@@ -48086,7 +48266,7 @@ async function releaseSubmit(ctx, ref) {
48086
48266
  ctx.out.result({ ...submitted, next, ...whoCanSee }, () => {
48087
48267
  showSubmitted(ctx, release2, submitted.status, submitted);
48088
48268
  if (openCode) ctx.out.line(openCode);
48089
- ctx.out.line(import_picocolors18.default.dim(projectUrl(ctx.target.origin, release2)));
48269
+ ctx.out.line(import_picocolors19.default.dim(projectUrl(ctx.target.origin, release2)));
48090
48270
  });
48091
48271
  }
48092
48272
  async function releasePromote(ctx, ref, options) {
@@ -48140,7 +48320,7 @@ async function releasePromote(ctx, ref, options) {
48140
48320
  const next = nextStep(name, "draft", null).command;
48141
48321
  ctx.out.result({ from: ref, release: { version: version2, status: "draft" }, url: url2, next }, () => {
48142
48322
  ctx.out.line(
48143
- `Made ${import_picocolors18.default.bold(name)} from ${ref}, as a draft for ${options.channel}. It needs a passed test of its own.`
48323
+ `Made ${import_picocolors19.default.bold(name)} from ${ref}, as a draft for ${options.channel}. It needs a passed test of its own.`
48144
48324
  );
48145
48325
  ctx.out.line(`Next: ${next}`);
48146
48326
  });
@@ -48148,7 +48328,7 @@ async function releasePromote(ctx, ref, options) {
48148
48328
  }
48149
48329
  ctx.out.result({ from: ref, release: submitted, url: url2 }, () => {
48150
48330
  showSubmitted(ctx, to, submitted.status, submitted);
48151
- ctx.out.line(import_picocolors18.default.dim(url2));
48331
+ ctx.out.line(import_picocolors19.default.dim(url2));
48152
48332
  });
48153
48333
  }
48154
48334
  function draftUntilTested(error62) {
@@ -48185,7 +48365,7 @@ function whoCanSeeIt(...words2) {
48185
48365
  function showSubmitted(ctx, ref, status2, { channel, kind }) {
48186
48366
  const name = releaseName(ref);
48187
48367
  if (status2 === "published") {
48188
- ctx.out.line(import_picocolors18.default.green(`Published ${name} to ${channel}`));
48368
+ ctx.out.line(import_picocolors19.default.green(`Published ${name} to ${channel}`));
48189
48369
  if (channel === "private") {
48190
48370
  ctx.out.line(privateWords(kind, false));
48191
48371
  ctx.out.line(`pch status ${name} says whether your team can play it yet, and where.`);
@@ -48339,7 +48519,7 @@ async function uploadFailed(file2, upload, response) {
48339
48519
  }
48340
48520
 
48341
48521
  // src/commands/release-test.ts
48342
- var import_picocolors19 = __toESM(require_picocolors(), 1);
48522
+ var import_picocolors20 = __toESM(require_picocolors(), 1);
48343
48523
  import { createHash as createHash3 } from "node:crypto";
48344
48524
  import { mkdir as mkdir9, mkdtemp as mkdtemp3, writeFile as writeFile12 } from "node:fs/promises";
48345
48525
  import { tmpdir as tmpdir3 } from "node:os";
@@ -48451,9 +48631,19 @@ async function show3(ctx, release2, test2, { followed, ...extra } = {}) {
48451
48631
  const play2 = test2.state === "passed" ? await playFirst(ctx, release2) : null;
48452
48632
  const ask = test2.state === "waiting" ? askThePerson(test2) : play2;
48453
48633
  const noteAsk = test2.note_ask?.replaceAll("suggest_note", "pch note suggest");
48454
- const shown = { ...test2, ...extra, next: command, ...noteAsk ? { note_ask: noteAsk } : {} };
48634
+ const feedbackAsk = test2.feedback_ask ? {
48635
+ ...test2.feedback_ask,
48636
+ ask: test2.feedback_ask.ask.replaceAll("send_feedback", "pch feedback send")
48637
+ } : void 0;
48638
+ const shown = {
48639
+ ...test2,
48640
+ ...extra,
48641
+ next: command,
48642
+ ...noteAsk ? { note_ask: noteAsk } : {},
48643
+ ...feedbackAsk ? { feedback_ask: feedbackAsk } : {}
48644
+ };
48455
48645
  out.result({ ...shown, ...ask ? { ask } : {} }, () => {
48456
- const bold = import_picocolors19.default.bold(name);
48646
+ const bold = import_picocolors20.default.bold(name);
48457
48647
  if (test2.state === "waiting") {
48458
48648
  out.line(
48459
48649
  !test2.needs_person ? `${extra.started ? "Started a test of" : "The test of"} ${bold}. It's waiting for Patch Desktop to take it on.` : extra.started ? `Started a test of ${bold}. It's waiting for you in Patch Desktop: press Start on it there.` : `${bold}: its test is waiting for you in Patch Desktop. Press Start on it there.`
@@ -48469,30 +48659,31 @@ async function show3(ctx, release2, test2, { followed, ...extra } = {}) {
48469
48659
  `${bold}: it has been played and recorded. Look at the footage, then send your review.`
48470
48660
  );
48471
48661
  } else if (test2.state === "passed") {
48472
- out.line(`${bold}: ${import_picocolors19.default.green("its test passed.")} It's still private.`);
48662
+ out.line(`${bold}: ${import_picocolors20.default.green("its test passed.")} It's still private.`);
48473
48663
  if (test2.review) out.line(plain(test2.review.summary));
48474
48664
  if (play2) out.line(play2);
48475
48665
  out.line(
48476
48666
  "Publish it once you're happy it's ready: that's your yes, and nothing does it for you."
48477
48667
  );
48478
48668
  } else if (test2.state === "failed") {
48479
- out.line(`${bold}: ${import_picocolors19.default.red("its test failed")}, so this version can't be published.`);
48669
+ out.line(`${bold}: ${import_picocolors20.default.red("its test failed")}, so this version can't be published.`);
48480
48670
  showWhatFailed(ctx, test2);
48481
48671
  out.line(`pch status ${name} says how to fix it.`);
48482
48672
  out.line("Then move the version on in patch.json and publish that.");
48483
48673
  } else if (test2.state === "couldnt_run") {
48484
- out.line(`${bold}: ${import_picocolors19.default.yellow("its test couldn't run.")}`);
48674
+ out.line(`${bold}: ${import_picocolors20.default.yellow("its test couldn't run.")}`);
48485
48675
  if (test2.outcome) out.line(plain(test2.outcome.reason));
48486
48676
  } else {
48487
48677
  out.line(`${bold}: its test was cancelled.`);
48488
48678
  }
48489
48679
  if (noteAsk) out.line(plain(noteAsk));
48680
+ if (feedbackAsk) out.line(plain(feedbackAsk.ask));
48490
48681
  if (followed && GOING.has(test2.state)) {
48491
48682
  out.line("It isn't over, and nothing's wrong: that's as long as --wait follows it here.");
48492
48683
  }
48493
48684
  if (command) out.line(`Next: ${command}`);
48494
48685
  if (OPEN.has(test2.state) && !extra.started) {
48495
- out.line(import_picocolors19.default.dim(`To start over: pch test cancel ${name}, then pch test start ${name}`));
48686
+ out.line(import_picocolors20.default.dim(`To start over: pch test cancel ${name}, then pch test start ${name}`));
48496
48687
  }
48497
48688
  });
48498
48689
  if (test2.state === "failed") return EXIT.blocked;
@@ -48529,7 +48720,7 @@ async function testFootage(ctx, ref, options) {
48529
48720
  const next = data.length ? reviewCommand(name) : nextStep(name, "testing", test2).command;
48530
48721
  out.result({ test: test2.id, state: test2.state, data: sheets, next }, () => {
48531
48722
  if (!sheets.length) {
48532
- out.line(`${import_picocolors19.default.bold(name)} has no footage yet: its test is ${test2.state.replace("_", " ")}.`);
48723
+ out.line(`${import_picocolors20.default.bold(name)} has no footage yet: its test is ${test2.state.replace("_", " ")}.`);
48533
48724
  for (const result of test2.results) {
48534
48725
  if (result.footage_error) out.line(plain(result.footage_error));
48535
48726
  }
@@ -48604,7 +48795,7 @@ async function testCancel(ctx, ref) {
48604
48795
  }
48605
48796
  const next = `pch test start ${name}`;
48606
48797
  ctx.out.result({ ...answer.data, next }, () => {
48607
- ctx.out.line(`Cancelled ${import_picocolors19.default.bold(name)}'s test. Start another: ${next}`);
48798
+ ctx.out.line(`Cancelled ${import_picocolors20.default.bold(name)}'s test. Start another: ${next}`);
48608
48799
  });
48609
48800
  }
48610
48801
  function withNext(failure2, release2, test2) {
@@ -48634,7 +48825,7 @@ async function testAutostart(ctx, state) {
48634
48825
  }
48635
48826
 
48636
48827
  // src/commands/search.ts
48637
- var import_picocolors20 = __toESM(require_picocolors(), 1);
48828
+ var import_picocolors21 = __toESM(require_picocolors(), 1);
48638
48829
  init_guide_words();
48639
48830
  var filter = (values) => values?.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean).join(",") || void 0;
48640
48831
  var licences = (values) => values.flatMap((value) => value.split(",")).map((value) => licenceId(value.trim()) ?? value.trim());
@@ -48672,13 +48863,13 @@ async function search(ctx, q, options) {
48672
48863
  out.result({ ...answer, ...aboutBlocks }, () => {
48673
48864
  if (!data.length) out.line("Nothing found.");
48674
48865
  for (const result of plain(data)) {
48675
- out.line(`${import_picocolors20.default.bold(result.handle)} ${import_picocolors20.default.dim(facts(result))}`);
48866
+ out.line(`${import_picocolors21.default.bold(result.handle)} ${import_picocolors21.default.dim(facts(result))}`);
48676
48867
  if (result.summary) out.line(` ${result.summary}`);
48677
48868
  if (result.demo) out.line(` Live demo: ${ctx.target.origin}/${result.owner}/${result.slug}`);
48678
- out.line(import_picocolors20.default.dim(` ${result.clone_url}`));
48869
+ out.line(import_picocolors21.default.dim(` ${result.clone_url}`));
48679
48870
  }
48680
48871
  if (blocks) out.line(BLOCK_WORDS);
48681
- if (cursor) out.line(import_picocolors20.default.dim("There are more; raise --limit."));
48872
+ if (cursor) out.line(import_picocolors21.default.dim("There are more; raise --limit."));
48682
48873
  });
48683
48874
  }
48684
48875
  function facts(result) {
@@ -48703,14 +48894,14 @@ async function categories(ctx, options) {
48703
48894
  const width = Math.max(0, ...data.map((category) => category.id.length));
48704
48895
  for (const category of data) {
48705
48896
  ctx.out.line(
48706
- `${import_picocolors20.default.bold(category.id.padEnd(width))} ${category.name} ${import_picocolors20.default.dim(`\xB7 ${category.for.join(", ")} \xB7 ${category.projects === 1 ? "1 project" : `${category.projects} projects`}`)}`
48897
+ `${import_picocolors21.default.bold(category.id.padEnd(width))} ${category.name} ${import_picocolors21.default.dim(`\xB7 ${category.for.join(", ")} \xB7 ${category.projects === 1 ? "1 project" : `${category.projects} projects`}`)}`
48707
48898
  );
48708
48899
  }
48709
48900
  });
48710
48901
  }
48711
48902
 
48712
48903
  // src/commands/update.ts
48713
- var import_picocolors21 = __toESM(require_picocolors(), 1);
48904
+ var import_picocolors22 = __toESM(require_picocolors(), 1);
48714
48905
  var import_semver3 = __toESM(require_semver2(), 1);
48715
48906
  import { spawnSync as spawnSync2 } from "node:child_process";
48716
48907
  import { createHash as createHash4 } from "node:crypto";
@@ -48897,7 +49088,7 @@ function startUpdateNotice(ctx, command, now = Date.now()) {
48897
49088
  if (version2 && import_semver3.default.gt(version2, VERSION)) {
48898
49089
  process.stderr.write(
48899
49090
  `
48900
- ${import_picocolors21.default.dim("\u203A")} pch ${version2} is out (you have ${VERSION}). To update, run: ${how}
49091
+ ${import_picocolors22.default.dim("\u203A")} pch ${version2} is out (you have ${VERSION}). To update, run: ${how}
48901
49092
  `
48902
49093
  );
48903
49094
  }
@@ -48906,7 +49097,7 @@ ${import_picocolors21.default.dim("\u203A")} pch ${version2} is out (you have ${
48906
49097
  }
48907
49098
 
48908
49099
  // src/commands/validate.ts
48909
- var import_picocolors22 = __toESM(require_picocolors(), 1);
49100
+ var import_picocolors23 = __toESM(require_picocolors(), 1);
48910
49101
  init_errors();
48911
49102
  async function validate2(ctx, dir) {
48912
49103
  const project2 = await loadProject(dir);
@@ -48965,12 +49156,12 @@ async function validate2(ctx, dir) {
48965
49156
  notes
48966
49157
  };
48967
49158
  ctx.out.result(result, () => {
48968
- const mark = result.ok ? import_picocolors22.default.green("\u2713") : import_picocolors22.default.red("\u2717");
49159
+ const mark = result.ok ? import_picocolors23.default.green("\u2713") : import_picocolors23.default.red("\u2717");
48969
49160
  ctx.out.line(
48970
49161
  `${mark} ${file2.project}@${file2.version} \xB7 ${project2.kind} \xB7 ${result.trust_tier} \xB7 ${project2.files.length} files, ${formatSize(total)}`
48971
49162
  );
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)}`);
49163
+ for (const issue2 of issues) ctx.out.line(` ${import_picocolors23.default.red(issue2.path || "\xB7")} ${issue2.message}`);
49164
+ for (const note2 of notes) ctx.out.line(` ${import_picocolors23.default.dim(note2)}`);
48974
49165
  });
48975
49166
  return result.ok ? EXIT.ok : EXIT.usage;
48976
49167
  }
@@ -49010,13 +49201,22 @@ Start here:
49010
49201
  pch media add, pch project update the cover and the listing
49011
49202
 
49012
49203
  Add --json to any command for results an agent can read. Exit codes: 0 fine, 1 error, 2 bad usage,
49013
- 3 blocked, 4 sign-in or quota, 5 conflict, 6 needs a person.`
49204
+ 3 blocked, 4 sign-in or quota, 5 conflict, 6 needs a person.
49205
+
49206
+ Whenever Patch gets in your way, tell its team: pch feedback send --kind <failed|bug|call_error|unhelpful|idea>
49207
+ "<what got in your way>", with --tool, --error-code, --expected, --actual and --would-help.
49208
+ pch guide has an example. Then carry on.`
49014
49209
  ).showHelpAfterError(!wantsJson).configureOutput({
49015
49210
  outputError: (text, write) => write(
49016
49211
  wantsJson ? `${JSON.stringify({ error: { code: "bad_usage", message: text.replace(/^error: /, "").trim() } })}
49017
49212
  ` : text
49018
49213
  )
49019
49214
  }).exitOverride();
49215
+ function commandPath(command) {
49216
+ const names = [];
49217
+ for (let at = command; at; at = at.parent) names.unshift(at.name());
49218
+ return names.join(" ");
49219
+ }
49020
49220
  function action(run3) {
49021
49221
  return async (...args2) => {
49022
49222
  const command = args2.at(-1);
@@ -49028,7 +49228,7 @@ function action(run3) {
49028
49228
  const code = await run3(ctx, ...args2.slice(0, -1));
49029
49229
  process.exitCode = typeof code === "number" ? code : EXIT.ok;
49030
49230
  } catch (error62) {
49031
- process.exitCode = new Output(Boolean(globals.json)).error(error62);
49231
+ process.exitCode = new Output(Boolean(globals.json)).error(error62, commandPath(command));
49032
49232
  }
49033
49233
  await notice?.show();
49034
49234
  };
@@ -49479,6 +49679,20 @@ note.command("suggest <about> <fact>").description(
49479
49679
  (ctx, about, fact, options) => noteSuggest(ctx, about, fact, options)
49480
49680
  )
49481
49681
  );
49682
+ var feedback = program2.command("feedback").description("Tell Patch's team what got in your way, and see their answers");
49683
+ feedback.command("send <summary>").description(
49684
+ "Tell Patch's team what got in your way, in one specific line, then carry on: you couldn't do what was asked (failed), Patch did something wrong (bug), a call failed and its error didn't say what to do (call_error), a command or its answer didn't help (unhelpful), or something would have made it easier (idea). Write it for the engineer who'll fix it. A person at Patch reads every one, and anything that looks like a key is taken out. It may ask back the one thing that would help most: answer with --add-to"
49685
+ ).addOption(
49686
+ new Option("--kind <kind>", "failed, bug, call_error, unhelpful or idea").choices(FEEDBACK_KINDS).makeOptionMandatory()
49687
+ ).option("--goal <text>", "what you were trying to do for the person").option("--expected <text>", "what you expected Patch to do or say").option("--actual <text>", "what it did or said instead, in its own words").option("--tried <text>", "what you tried, in order").option("--workaround <text>", "what worked around it in the end, if anything did").option(
49688
+ "--would-help <text>",
49689
+ "exactly what would have made it easy: the words, the command, the flag"
49690
+ ).option("--cost <text>", "what it cost: extra calls, retries, minutes, or a step given up").addOption(
49691
+ new Option("--severity <severity>", "blocked, slowed or annoyed").choices(FEEDBACK_SEVERITIES)
49692
+ ).option("--details <text>", "anything else").option("--add-to <id>", "a report of yours to add this to, as when answering its follow-up").option("--tool <command>", "the command or MCP tool it's about, e.g. pch publish").option("--error-code <code>", "the error's code, for one that named no failed call").option("--project <owner/slug>", "the project it's about").option("--agent <name>", "which coding agent and model you are").action(
49693
+ action((ctx, summary, options) => feedbackSend(ctx, summary, options))
49694
+ );
49695
+ feedback.command("list").description("Your reports, newest first, with what Patch's team answered").action(action((ctx) => feedbackList(ctx)));
49482
49696
  var words = process.argv.slice(2);
49483
49697
  if (!program2.commands.some((one) => [one.name(), ...one.aliases()].some((n) => words.includes(n)))) {
49484
49698
  program2.version(VERSION);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patchgg/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "pch: the Patch command-line tool for creators and their agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://patch.gg",