@patchgg/cli 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +21 -30
  2. package/dist/cli.js +1212 -800
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -130,11 +130,17 @@ function exitCodeFor(status2) {
130
130
  return EXIT.error;
131
131
  }
132
132
  function notSignedIn() {
133
- return new CliError(
133
+ const failure2 = new CliError(
134
134
  EXIT.auth,
135
135
  "not_signed_in",
136
136
  "You're not signed in. Run pch login, or set PCH_TOKEN to an API key."
137
137
  );
138
+ failure2.forPeople = {
139
+ title: "You're not signed in",
140
+ next: [["pch login", "sign in, in your browser"]],
141
+ also: "For an agent or CI, set PCH_TOKEN to an API key instead."
142
+ };
143
+ return failure2;
138
144
  }
139
145
  var EXIT, CliError;
140
146
  var init_errors = __esm({
@@ -167,6 +173,11 @@ var init_errors = __esm({
167
173
  details;
168
174
  /** Patch's API said it, rather than pch itself. */
169
175
  fromApi;
176
+ /**
177
+ * What a person at a terminal sees instead of the message, when that's shorter: a line, and a
178
+ * command or two that fix it. Agents and --json always get the whole message.
179
+ */
180
+ forPeople;
170
181
  };
171
182
  }
172
183
  });
@@ -176,17 +187,17 @@ var VERSION;
176
187
  var init_version = __esm({
177
188
  "src/version.ts"() {
178
189
  "use strict";
179
- VERSION = true ? "0.1.3" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
190
+ VERSION = true ? "0.3.0" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
180
191
  }
181
192
  });
182
193
 
183
194
  // src/guide-words.ts
184
- function openCodeWords(name, repo, release2) {
195
+ function openCodeWords(name, repo, release3) {
185
196
  if (repo?.visibility !== "open") return null;
186
- if (release2.status === "yanked" || release2.status === "quarantined") return null;
187
- const seen = release2.channel === "private" ? "private" : release2.status === "published" ? release2.channel : "private until it's published";
197
+ if (release3.status === "yanked" || release3.status === "quarantined") return null;
198
+ const seen = release3.channel === "private" ? "private" : release3.status === "published" ? release3.channel : "private until it's published";
188
199
  if (seen === "public") return null;
189
- const what = release2.kind === "mod" ? "mod" : release2.kind === "block" ? "block" : "game";
200
+ const what = release3.kind === "mod" ? "mod" : release3.kind === "block" ? "block" : "game";
190
201
  const start = `The ${what} is ${seen}, but its code isn't:`;
191
202
  const pass = "Say so when you tell anyone who can see it.";
192
203
  if (repo.home === "github") {
@@ -236,13 +247,13 @@ git add -A && git commit -m "..."
236
247
  ${pch} publish
237
248
  \`\`\`
238
249
 
239
- A browser game, a mod Patch installs or a block's demo stays a private draft until it has passed a test on your PC, in Patch Desktop:
250
+ That's the whole flow: it uploads, tests a browser game, a mod Patch installs or a block's demo on your PC in Patch Desktop, saves the footage, and stops for your review of it. Look at every sheet, then send the review as it says:
240
251
 
241
252
  \`\`\`sh
242
- ${pch} test start <you>/<game>@<version>
253
+ ${pch} publish <you>/<game>@<version> --review pass --summary "<what you saw>" --moment "1 00:04 <what you saw>" --moment "1 00:30 <what you saw>"
243
254
  \`\`\`
244
255
 
245
- Someone has to press Start there. A passed test shows it starts and runs, not that it plays well, so play it yourself before its first public release. \`${pch} status\` says where a release is and what's next at every step.
256
+ That sends it to Patch's review and follows it until it's live. Running \`${pch} publish\` is your yes, so run it once the person wants it out (\`${pch} test start\` tries a build without publishing it). A passed test shows it starts and runs, not that it plays well, so play it yourself before its first public release.
246
257
 
247
258
  ## 4. Its listing
248
259
 
@@ -293,6 +304,7 @@ var init_guide_words = __esm({
293
304
  ["search_assets, asset_facets, get_asset", "assets search, assets facets, assets view"],
294
305
  ["get_asset_files", "assets add"],
295
306
  ["validate_recipe", "validate"],
307
+ ["publish", "publish, then publish <release> --review \u2026"],
296
308
  ["create_release, complete_upload", "publish, from a folder with a patch.json"],
297
309
  ["submit_release", "publish, or release submit"],
298
310
  ["list_releases, release_status", "release list, status"],
@@ -654,8 +666,8 @@ var require_semver = __commonJS({
654
666
  }
655
667
  // preminor will bump the version up to the next minor release, and immediately
656
668
  // down to pre-release. premajor and prepatch work the same way.
657
- inc(release2, identifier, identifierBase) {
658
- if (release2.startsWith("pre")) {
669
+ inc(release3, identifier, identifierBase) {
670
+ if (release3.startsWith("pre")) {
659
671
  if (!identifier && identifierBase === false) {
660
672
  throw new Error("invalid increment argument: identifier is empty");
661
673
  }
@@ -666,7 +678,7 @@ var require_semver = __commonJS({
666
678
  }
667
679
  }
668
680
  }
669
- switch (release2) {
681
+ switch (release3) {
670
682
  case "premajor":
671
683
  this.prerelease.length = 0;
672
684
  this.patch = 0;
@@ -758,7 +770,7 @@ var require_semver = __commonJS({
758
770
  break;
759
771
  }
760
772
  default:
761
- throw new Error(`invalid increment argument: ${release2}`);
773
+ throw new Error(`invalid increment argument: ${release3}`);
762
774
  }
763
775
  this.raw = this.format();
764
776
  if (this.build.length) {
@@ -824,7 +836,7 @@ var require_inc = __commonJS({
824
836
  "node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/inc.js"(exports, module) {
825
837
  "use strict";
826
838
  var SemVer = require_semver();
827
- var inc = (version2, release2, options, identifier, identifierBase) => {
839
+ var inc = (version2, release3, options, identifier, identifierBase) => {
828
840
  if (typeof options === "string") {
829
841
  identifierBase = identifier;
830
842
  identifier = options;
@@ -834,7 +846,7 @@ var require_inc = __commonJS({
834
846
  return new SemVer(
835
847
  version2 instanceof SemVer ? version2.version : version2,
836
848
  options
837
- ).inc(release2, identifier, identifierBase).version;
849
+ ).inc(release3, identifier, identifierBase).version;
838
850
  } catch (er) {
839
851
  return null;
840
852
  }
@@ -6836,7 +6848,7 @@ function isRef(value) {
6836
6848
  function cloneIssues(issues) {
6837
6849
  return issues.map((iss) => iss.path ? { ...iss, path: iss.path.slice() } : { ...iss });
6838
6850
  }
6839
- function isRecursive(inst, stack, resolve16) {
6851
+ function isRecursive(inst, stack, resolve17) {
6840
6852
  const cached2 = recursive.get(inst);
6841
6853
  if (cached2 !== void 0)
6842
6854
  return cached2 ? PROVEN : NONE;
@@ -6846,7 +6858,7 @@ function isRecursive(inst, stack, resolve16) {
6846
6858
  let result = NONE;
6847
6859
  const check3 = (child) => {
6848
6860
  if (result !== PROVEN && child?._zod) {
6849
- const answer = isRecursive(child, stack, resolve16);
6861
+ const answer = isRecursive(child, stack, resolve17);
6850
6862
  if (answer > result)
6851
6863
  result = answer;
6852
6864
  }
@@ -6857,7 +6869,7 @@ function isRecursive(inst, stack, resolve16) {
6857
6869
  const desc = Object.getOwnPropertyDescriptor(sh, key);
6858
6870
  if (spread && !desc.enumerable)
6859
6871
  continue;
6860
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve16) : NONE;
6872
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve17) : NONE;
6861
6873
  if (child > answer)
6862
6874
  answer = child;
6863
6875
  }
@@ -6921,7 +6933,7 @@ function isRecursive(inst, stack, resolve16) {
6921
6933
  break;
6922
6934
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
6923
6935
  case "lazy": {
6924
- const inner = def._cachedInner ?? (resolve16 ? inst._zod.innerType : void 0);
6936
+ const inner = def._cachedInner ?? (resolve17 ? inst._zod.innerType : void 0);
6925
6937
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
6926
6938
  break;
6927
6939
  }
@@ -25289,7 +25301,7 @@ var init_protocol = __esm({
25289
25301
  return;
25290
25302
  }
25291
25303
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
25292
- await new Promise((resolve16) => setTimeout(resolve16, pollInterval));
25304
+ await new Promise((resolve17) => setTimeout(resolve17, pollInterval));
25293
25305
  options?.signal?.throwIfAborted();
25294
25306
  }
25295
25307
  } catch (error62) {
@@ -25306,7 +25318,7 @@ var init_protocol = __esm({
25306
25318
  */
25307
25319
  request(request, resultSchema, options) {
25308
25320
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
25309
- return new Promise((resolve16, reject) => {
25321
+ return new Promise((resolve17, reject) => {
25310
25322
  const earlyReject = (error62) => {
25311
25323
  reject(error62);
25312
25324
  };
@@ -25384,7 +25396,7 @@ var init_protocol = __esm({
25384
25396
  if (!parseResult.success) {
25385
25397
  reject(parseResult.error);
25386
25398
  } else {
25387
- resolve16(parseResult.data);
25399
+ resolve17(parseResult.data);
25388
25400
  }
25389
25401
  } catch (error62) {
25390
25402
  reject(error62);
@@ -25646,12 +25658,12 @@ var init_protocol = __esm({
25646
25658
  }
25647
25659
  } catch {
25648
25660
  }
25649
- return new Promise((resolve16, reject) => {
25661
+ return new Promise((resolve17, reject) => {
25650
25662
  if (signal.aborted) {
25651
25663
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
25652
25664
  return;
25653
25665
  }
25654
- const timeoutId = setTimeout(resolve16, interval);
25666
+ const timeoutId = setTimeout(resolve17, interval);
25655
25667
  signal.addEventListener("abort", () => {
25656
25668
  clearTimeout(timeoutId);
25657
25669
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -28678,7 +28690,7 @@ var require_compile = __commonJS({
28678
28690
  const schOrFunc = root.refs[ref];
28679
28691
  if (schOrFunc)
28680
28692
  return schOrFunc;
28681
- let _sch = resolve16.call(this, root, ref);
28693
+ let _sch = resolve17.call(this, root, ref);
28682
28694
  if (_sch === void 0) {
28683
28695
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
28684
28696
  const { schemaId } = this.opts;
@@ -28705,7 +28717,7 @@ var require_compile = __commonJS({
28705
28717
  function sameSchemaEnv(s1, s2) {
28706
28718
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
28707
28719
  }
28708
- function resolve16(root, ref) {
28720
+ function resolve17(root, ref) {
28709
28721
  let sch;
28710
28722
  while (typeof (sch = this.refs[ref]) == "string")
28711
28723
  ref = sch;
@@ -29569,7 +29581,7 @@ var require_fast_uri = __commonJS({
29569
29581
  }
29570
29582
  return uri;
29571
29583
  }
29572
- function resolve16(baseURI, relativeURI, options) {
29584
+ function resolve17(baseURI, relativeURI, options) {
29573
29585
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
29574
29586
  const {
29575
29587
  parsed: baseParsed,
@@ -29960,7 +29972,7 @@ var require_fast_uri = __commonJS({
29960
29972
  var fastUri = {
29961
29973
  SCHEMES,
29962
29974
  normalize,
29963
- resolve: resolve16,
29975
+ resolve: resolve17,
29964
29976
  resolveComponent,
29965
29977
  equal,
29966
29978
  serialize,
@@ -35633,12 +35645,12 @@ var init_stdio2 = __esm({
35633
35645
  this.onclose?.();
35634
35646
  }
35635
35647
  send(message) {
35636
- return new Promise((resolve16) => {
35648
+ return new Promise((resolve17) => {
35637
35649
  const json2 = serializeMessage(message);
35638
35650
  if (this._stdout.write(json2)) {
35639
- resolve16();
35651
+ resolve17();
35640
35652
  } else {
35641
- this._stdout.once("drain", resolve16);
35653
+ this._stdout.once("drain", resolve17);
35642
35654
  }
35643
35655
  });
35644
35656
  }
@@ -35657,8 +35669,8 @@ async function mcp(ctx, streams = {}) {
35657
35669
  const remote = patchServer(ctx, "start pch mcp again");
35658
35670
  const stdin = streams.stdin ?? process.stdin;
35659
35671
  const local = new StdioServerTransport(stdin, streams.stdout ?? takeStdout());
35660
- const hungUp = new Promise((resolve16) => {
35661
- local.onclose = resolve16;
35672
+ const hungUp = new Promise((resolve17) => {
35673
+ local.onclose = resolve17;
35662
35674
  });
35663
35675
  stdin.once("end", () => void local.close());
35664
35676
  let landed = Promise.resolve();
@@ -35755,7 +35767,9 @@ var init_mcp = __esm({
35755
35767
  });
35756
35768
 
35757
35769
  // src/plain-when-piped.ts
35758
- if (!process.stdout.isTTY && !process.env.FORCE_COLOR) process.env.NO_COLOR = "1";
35770
+ if ((!process.stdout.isTTY || process.env.TERM === "dumb") && !process.env.FORCE_COLOR) {
35771
+ process.env.NO_COLOR = "1";
35772
+ }
35759
35773
 
35760
35774
  // node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
35761
35775
  var CommanderError = class extends Error {
@@ -36330,13 +36344,13 @@ var Help = class {
36330
36344
  (sub) => sub.helpGroup() || "Commands:"
36331
36345
  );
36332
36346
  commandGroups.forEach((commands, group) => {
36333
- const commandList = commands.map((sub) => {
36347
+ const commandList2 = commands.map((sub) => {
36334
36348
  return callFormatItem(
36335
36349
  helper.styleSubcommandTerm(helper.subcommandTerm(sub)),
36336
36350
  helper.styleSubcommandDescription(helper.subcommandDescription(sub))
36337
36351
  );
36338
36352
  });
36339
- output2 = output2.concat(this.formatItemList(group, commandList, helper));
36353
+ output2 = output2.concat(this.formatItemList(group, commandList2, helper));
36340
36354
  });
36341
36355
  return output2.join("\n");
36342
36356
  }
@@ -36490,10 +36504,10 @@ ${itemIndentStr}`);
36490
36504
  let sumChunks = [chunks.shift()];
36491
36505
  let sumWidth = this.displayWidth(sumChunks[0]);
36492
36506
  chunks.forEach((chunk) => {
36493
- const visibleWidth = this.displayWidth(chunk);
36494
- if (sumWidth + visibleWidth <= width) {
36507
+ const visibleWidth2 = this.displayWidth(chunk);
36508
+ if (sumWidth + visibleWidth2 <= width) {
36495
36509
  sumChunks.push(chunk);
36496
- sumWidth += visibleWidth;
36510
+ sumWidth += visibleWidth2;
36497
36511
  return;
36498
36512
  }
36499
36513
  wrappedLines.push(sumChunks.join(""));
@@ -39126,7 +39140,7 @@ function useColor() {
39126
39140
  var program = new Command();
39127
39141
 
39128
39142
  // src/commands/account.ts
39129
- var import_picocolors3 = __toESM(require_picocolors(), 1);
39143
+ var import_picocolors4 = __toESM(require_picocolors(), 1);
39130
39144
 
39131
39145
  // node_modules/.pnpm/openapi-fetch@0.17.0/node_modules/openapi-fetch/dist/index.mjs
39132
39146
  var PATH_PARAM_RE = /\{[^{}]+\}/g;
@@ -39970,20 +39984,188 @@ var LFS_FLAGS = [
39970
39984
  ];
39971
39985
  var NO_LFS = "Git LFS isn't installed, so big files stay small pointer files. Install it from https://git-lfs.com, then run git lfs install.";
39972
39986
 
39987
+ // src/commands/account.ts
39988
+ init_guide_words();
39989
+
39990
+ // src/terminal.ts
39991
+ var import_picocolors = __toESM(require_picocolors(), 1);
39992
+ import { spawnSync } from "node:child_process";
39993
+ import { release } from "node:os";
39994
+ function canRedraw() {
39995
+ return Boolean(process.stderr.isTTY) && process.env.TERM !== "dumb";
39996
+ }
39997
+ function unicode(vars = process.env, platform = process.platform) {
39998
+ if (platform !== "win32") return vars.TERM !== "linux";
39999
+ return Boolean(
40000
+ vars.WT_SESSION || vars.TERMINUS_SUBLIME || vars.ConEmuTask === "{cmd::Cmder}" || vars.TERM_PROGRAM === "vscode" || vars.TERM_PROGRAM === "mintty" || vars.TERM === "xterm-256color" || vars.TERM === "alacritty" || vars.TERMINAL_EMULATOR === "JetBrains-JediTerm"
40001
+ );
40002
+ }
40003
+ function colourDepth(vars = process.env, platform = process.platform) {
40004
+ if (!import_picocolors.default.isColorSupported || vars.TERM === "dumb") return 0;
40005
+ if (vars.COLORTERM === "truecolor" || vars.COLORTERM === "24bit") return 3;
40006
+ if (vars.WT_SESSION || vars.TERM_PROGRAM === "mintty" || vars.TERM_PROGRAM === "vscode") return 3;
40007
+ if (platform === "win32" && Number(release().split(".")[2]) >= 14931) return 3;
40008
+ if (vars.TERM?.includes("256")) return 2;
40009
+ return 1;
40010
+ }
40011
+ function pink(text, depth = colourDepth()) {
40012
+ if (depth === 3) return `\x1B[38;2;245;60;209m${text}\x1B[39m`;
40013
+ if (depth === 2) return `\x1B[38;5;206m${text}\x1B[39m`;
40014
+ return depth === 1 ? import_picocolors.default.magenta(text) : text;
40015
+ }
40016
+ function copyText(text, platform = process.platform) {
40017
+ const tools = platform === "win32" ? [["clip", []]] : platform === "darwin" ? [["pbcopy", []]] : [
40018
+ ["wl-copy", []],
40019
+ ["xclip", ["-selection", "clipboard"]],
40020
+ ["xsel", ["--clipboard", "--input"]]
40021
+ ];
40022
+ for (const [command, args2] of tools) {
40023
+ try {
40024
+ const done = spawnSync(command, args2, {
40025
+ input: text,
40026
+ stdio: ["pipe", "ignore", "ignore"],
40027
+ timeout: 2e3
40028
+ });
40029
+ if (done.status === 0) return true;
40030
+ } catch {
40031
+ }
40032
+ }
40033
+ return false;
40034
+ }
40035
+ var symbols = () => unicode() ? { done: "\u2713", failed: "\xD7", note: "!" } : { done: "\u221A", failed: "\xD7", note: "!" };
40036
+ var FRAMES = {
40037
+ unicode: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
40038
+ plain: ["|", "/", "-", "\\"]
40039
+ };
40040
+ function spinner(text, hint = "") {
40041
+ if (!canRedraw()) {
40042
+ process.stderr.write(` ${text}
40043
+ `);
40044
+ return { stop() {
40045
+ }, say(_) {
40046
+ } };
40047
+ }
40048
+ const frames = unicode() ? FRAMES.unicode : FRAMES.plain;
40049
+ let frame = 0;
40050
+ const draw = () => {
40051
+ const room = (process.stderr.columns || 80) - 5;
40052
+ const said2 = text.length > room ? `${text.slice(0, Math.max(0, room - 1))}\u2026` : text;
40053
+ const more = hint && said2.length + 2 + hint.length <= room ? ` ${import_picocolors.default.dim(hint)}` : "";
40054
+ const spin = frames[frame++ % frames.length];
40055
+ process.stderr.write(`\r\x1B[K ${spin} ${said2}${more}`);
40056
+ };
40057
+ const clear = () => process.stderr.write("\r\x1B[K\x1B[?25h");
40058
+ const cancel = () => {
40059
+ clear();
40060
+ process.stderr.write(` ${import_picocolors.default.dim("Cancelled.")}
40061
+ `);
40062
+ process.exit(130);
40063
+ };
40064
+ process.stderr.write("\x1B[?25l");
40065
+ draw();
40066
+ const timer = setInterval(draw, 80);
40067
+ process.once("SIGINT", cancel);
40068
+ let stopped = false;
40069
+ return {
40070
+ stop() {
40071
+ if (stopped) return;
40072
+ stopped = true;
40073
+ clearInterval(timer);
40074
+ process.off("SIGINT", cancel);
40075
+ clear();
40076
+ },
40077
+ /** Says something else from now on, such as the next thing it's waiting for. */
40078
+ say(next) {
40079
+ text = next;
40080
+ hint = "";
40081
+ draw();
40082
+ }
40083
+ };
40084
+ }
40085
+ var Block = class {
40086
+ lines = [];
40087
+ write(line = "") {
40088
+ this.lines.push(line);
40089
+ process.stderr.write(`${line}
40090
+ `);
40091
+ }
40092
+ /** Takes the lines back off the screen, counting the rows the window wrapped them onto. */
40093
+ erase() {
40094
+ const columns = process.stderr.columns;
40095
+ if (!canRedraw() || !columns || !this.lines.length) return;
40096
+ const rows = this.lines.reduce(
40097
+ (sum, line) => sum + Math.max(1, Math.ceil(visibleWidth(line) / columns)),
40098
+ 0
40099
+ );
40100
+ process.stderr.write(`\x1B[${rows}A\r\x1B[J`);
40101
+ this.lines = [];
40102
+ }
40103
+ };
40104
+ function commandList(rows, indent = " ", columns = process.stdout.columns ?? 80) {
40105
+ const wide = Math.max(...rows.map(([command]) => command.length));
40106
+ const fits = indent.length + wide + 3 + Math.max(...rows.map(([, words2]) => words2.length));
40107
+ return rows.map(
40108
+ ([command, words2]) => fits <= columns ? `${indent}${import_picocolors.default.bold(command.padEnd(wide))} ${import_picocolors.default.dim(words2)}` : `${indent}${import_picocolors.default.bold(command)}
40109
+ ${indent} ${import_picocolors.default.dim(words2)}`
40110
+ );
40111
+ }
40112
+ function placeOf(origin) {
40113
+ try {
40114
+ return new URL(origin).host;
40115
+ } catch {
40116
+ return origin;
40117
+ }
40118
+ }
40119
+ var ESCAPES = /\x1b\[[0-9;?]*[A-Za-z]/g;
40120
+ function unstyled(text) {
40121
+ return text.replace(ESCAPES, "");
40122
+ }
40123
+ function visibleWidth(text) {
40124
+ return [...unstyled(text)].length;
40125
+ }
40126
+
39973
40127
  // src/commands/login.ts
39974
- var import_picocolors2 = __toESM(require_picocolors(), 1);
40128
+ var import_picocolors3 = __toESM(require_picocolors(), 1);
39975
40129
  import { spawn as spawn2 } from "node:child_process";
39976
40130
  import { hostname } from "node:os";
39977
40131
  import { setTimeout as sleep } from "node:timers/promises";
39978
40132
  init_errors();
39979
40133
  init_guide_words();
39980
40134
 
40135
+ // src/logo.ts
40136
+ var ROWS = [
40137
+ ".....................#.........#....",
40138
+ "####.................#.........#....",
40139
+ "#...#...####..####..####.#####.####.",
40140
+ "#.+.#...#...#.....#..#...#.....#...#",
40141
+ "#...#...#...#.#####..#...#.....#...#",
40142
+ "#####...#...#.#...#..#...#.....#...#",
40143
+ "........#####.#####..###.#####.#...#",
40144
+ "........#...........................",
40145
+ "........#..........................."
40146
+ ];
40147
+ var WIDTH = ROWS[0]?.length ?? 0;
40148
+ function logo(columns = process.stdout.columns ?? 80, depth = colourDepth()) {
40149
+ if (!canRedraw() || !unicode() || columns < WIDTH + 4) return null;
40150
+ const lines = [];
40151
+ for (let y = 0; y < ROWS.length; y += 2) {
40152
+ const top = ROWS[y] ?? "";
40153
+ const bottom = ROWS[y + 1] ?? "";
40154
+ let line = "";
40155
+ for (let x = 0; x < WIDTH; x++) {
40156
+ const up = top[x] ?? ".";
40157
+ const down = bottom[x] ?? ".";
40158
+ const glyph = up !== "." ? down !== "." ? "\u2588" : "\u2580" : down !== "." ? "\u2584" : " ";
40159
+ line += up === "+" || down === "+" ? pink(glyph, depth) : glyph;
40160
+ }
40161
+ lines.push(line.trimEnd());
40162
+ }
40163
+ return lines;
40164
+ }
40165
+
39981
40166
  // src/output.ts
39982
- var import_picocolors = __toESM(require_picocolors(), 1);
40167
+ var import_picocolors2 = __toESM(require_picocolors(), 1);
39983
40168
  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
40169
  function forAPerson(json2, agent2) {
39988
40170
  return !json2 && Boolean(process.stdout.isTTY && process.stderr.isTTY) && !agent2;
39989
40171
  }
@@ -40004,24 +40186,9 @@ var Output = class {
40004
40186
  }
40005
40187
  /** A progress line, e.g. "Uploading 2 files". */
40006
40188
  step(text) {
40007
- if (!this.json) process.stderr.write(`${import_picocolors.default.dim("\u203A")} ${text}
40189
+ if (!this.json) process.stderr.write(`${import_picocolors2.default.dim("\u203A")} ${text}
40008
40190
  `);
40009
40191
  }
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
- }
40025
40192
  /**
40026
40193
  * How far a long job has got. With --json it's a line of JSON on stderr, so whoever's waiting can
40027
40194
  * see it's moving while stdout stays the one result.
@@ -40045,16 +40212,40 @@ var Output = class {
40045
40212
  };
40046
40213
  process.stderr.write(`${JSON.stringify(body)}
40047
40214
  `);
40215
+ } else if (process.stderr.isTTY) {
40216
+ const failed3 = import_picocolors2.default.red(symbols().failed);
40217
+ const short = failure2.forPeople;
40218
+ if (short) process.stderr.write(`
40219
+ ${failed3} ${short.title}
40220
+
40221
+ `);
40222
+ else
40223
+ process.stderr.write(`
40224
+ ${failed3} ${failure2.message} ${import_picocolors2.default.dim(`(${failure2.code})`)}
40225
+ `);
40226
+ for (const issue2 of issuesOf(failure2.details)) {
40227
+ process.stderr.write(` ${import_picocolors2.default.dim(issue2.path || "\xB7")} ${issue2.message}
40228
+ `);
40229
+ }
40230
+ if (short) process.stderr.write(`${commandList(short.next, " ").join("\n")}
40231
+ `);
40232
+ if (short?.also) process.stderr.write(`
40233
+ ${import_picocolors2.default.dim(short.also)}
40234
+ `);
40235
+ if (feedback2) process.stderr.write(`
40236
+ ${import_picocolors2.default.dim(feedback2.say)}
40237
+ `);
40238
+ process.stderr.write("\n");
40048
40239
  } else {
40049
40240
  process.stderr.write(
40050
- `${import_picocolors.default.red("Error:")} ${failure2.message} ${import_picocolors.default.dim(`(${failure2.code})`)}
40241
+ `${import_picocolors2.default.red("Error:")} ${failure2.message} ${import_picocolors2.default.dim(`(${failure2.code})`)}
40051
40242
  `
40052
40243
  );
40053
40244
  for (const issue2 of issuesOf(failure2.details)) {
40054
- process.stderr.write(` ${import_picocolors.default.dim(issue2.path || "\xB7")} ${issue2.message}
40245
+ process.stderr.write(` ${import_picocolors2.default.dim(issue2.path || "\xB7")} ${issue2.message}
40055
40246
  `);
40056
40247
  }
40057
- if (feedback2) process.stderr.write(`${import_picocolors.default.dim(feedback2.say)}
40248
+ if (feedback2) process.stderr.write(`${import_picocolors2.default.dim(feedback2.say)}
40058
40249
  `);
40059
40250
  }
40060
40251
  return failure2.exitCode;
@@ -40105,29 +40296,33 @@ async function login(ctx, options) {
40105
40296
  const { out, target } = ctx;
40106
40297
  if (process.env.PCH_TOKEN) return loginWithKey(ctx);
40107
40298
  const scopes = options.scope?.length ? options.scope : ["read", "publish"];
40108
- const { me, key } = await withDeviceSession(ctx, options, async (session) => ({
40109
- me: await unwrap(session.GET("/me")),
40110
- key: await unwrap(
40299
+ const name = ctx.target.accountName ?? (target.env === "production" ? "default" : target.env);
40300
+ const isDefault = name === (target.env === "production" ? "default" : target.env);
40301
+ const people = forAPerson(out.json, runningIn());
40302
+ const notes = [];
40303
+ const signIn = { ...options, logo: true };
40304
+ const { me, key, git, gitIdentity } = await withDeviceSession(ctx, signIn, async (session) => {
40305
+ const me2 = await unwrap(session.GET("/me"));
40306
+ const key2 = await unwrap(
40111
40307
  session.POST("/tokens", {
40112
40308
  body: { name: `${runningIn() ?? "pch"} on ${hostname()}`, scopes }
40113
40309
  })
40114
- )
40115
- }));
40116
- const name = ctx.target.accountName ?? defaultAccount(target.env);
40117
- const account2 = {
40118
- env: target.env,
40119
- token: key.key,
40120
- token_id: key.id,
40121
- ...me.username ? { username: me.username } : {},
40122
- ...target.origin !== defaultOrigin(target.env) ? { api_url: target.origin } : {},
40123
- ...target.gitUrl && target.gitUrl !== defaultGitUrl(target.env) ? { git_url: target.gitUrl } : {}
40124
- };
40125
- const config2 = ctx.config;
40126
- config2.accounts = { ...config2.accounts, [name]: account2 };
40127
- config2.active_account = name;
40128
- await writeConfig(config2);
40129
- const git = await setUpGit(ctx);
40130
- const gitIdentity = await identityAdvice(ctx);
40310
+ );
40311
+ const account2 = {
40312
+ env: target.env,
40313
+ token: key2.key,
40314
+ token_id: key2.id,
40315
+ ...me2.username ? { username: me2.username } : {},
40316
+ ...target.origin !== defaultOrigin(target.env) ? { api_url: target.origin } : {},
40317
+ ...target.gitUrl && target.gitUrl !== defaultGitUrl(target.env) ? { git_url: target.gitUrl } : {}
40318
+ };
40319
+ const config2 = ctx.config;
40320
+ config2.accounts = { ...config2.accounts, [name]: account2 };
40321
+ config2.active_account = name;
40322
+ await writeConfig(config2);
40323
+ const git2 = await setUpGit(ctx, people ? notes : void 0);
40324
+ return { me: me2, key: key2, git: git2, gitIdentity: await identityAdvice(ctx) };
40325
+ });
40131
40326
  out.result(
40132
40327
  {
40133
40328
  account: name,
@@ -40139,13 +40334,21 @@ async function login(ctx, options) {
40139
40334
  look_first: LOOK_FIRST
40140
40335
  },
40141
40336
  () => {
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);
40337
+ if (people) {
40338
+ const who = import_picocolors3.default.bold(me.username ? `@${me.username}` : me.email);
40339
+ const which = isDefault ? "" : ` ${import_picocolors3.default.dim(`(account "${name}")`)}`;
40340
+ return signedIn(ctx, `Signed in as ${who} on ${placeOf(target.origin)}${which}`, {
40341
+ git,
40342
+ notes: [
40343
+ ...me.username ? [] : [`Choose a username at ${target.origin}/welcome to publish.`],
40344
+ ...gitIdentity ? [SHORT_IDENTITY] : [],
40345
+ ...notes
40346
+ ]
40347
+ });
40147
40348
  }
40148
- out.line(`Signed in as ${who} on ${target.env} (account "${name}").`);
40349
+ out.line(
40350
+ `Signed in as ${import_picocolors3.default.bold(me.username ? `@${me.username}` : me.email)} on ${target.env} (account "${name}").`
40351
+ );
40149
40352
  if (!me.username) out.line(`Choose a username at ${target.origin}/welcome to publish.`);
40150
40353
  if (git.helper) out.line(`Git is signed in too: Patch repos at ${git.url} just work.`);
40151
40354
  if (gitIdentity) out.line(gitIdentity);
@@ -40159,7 +40362,9 @@ async function login(ctx, options) {
40159
40362
  async function loginWithKey(ctx) {
40160
40363
  const { out, target } = ctx;
40161
40364
  const me = await unwrap(ctx.api.GET("/me"));
40162
- const git = await setUpGit(ctx);
40365
+ const people = forAPerson(out.json, runningIn());
40366
+ const notes = [];
40367
+ const git = await setUpGit(ctx, people ? notes : void 0);
40163
40368
  const gitIdentity = await identityAdvice(ctx);
40164
40369
  out.result(
40165
40370
  {
@@ -40172,63 +40377,57 @@ async function loginWithKey(ctx) {
40172
40377
  look_first: LOOK_FIRST
40173
40378
  },
40174
40379
  () => {
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);
40380
+ if (people) {
40381
+ const who = import_picocolors3.default.bold(me.username ? `@${me.username}` : me.email);
40382
+ return signedIn(ctx, `PCH_TOKEN works: ${who} on ${placeOf(target.origin)}`, {
40383
+ git,
40384
+ notes: [...gitIdentity ? [SHORT_IDENTITY] : [], ...notes]
40385
+ });
40179
40386
  }
40180
- out.line(`PCH_TOKEN works: ${who} on ${target.env}.`);
40387
+ out.line(
40388
+ `PCH_TOKEN works: ${import_picocolors3.default.bold(me.username ? `@${me.username}` : me.email)} on ${target.env}.`
40389
+ );
40181
40390
  if (git.helper) out.line(`Git is signed in too: Patch repos at ${git.url} just work.`);
40182
40391
  if (gitIdentity) out.line(gitIdentity);
40183
40392
  out.line(LOOK_FIRST);
40184
40393
  }
40185
40394
  );
40186
40395
  }
40187
- function defaultAccount(env) {
40188
- return env === "production" ? "default" : env;
40189
- }
40190
- function signedIn(ctx, headline, username, git, gitIdentity) {
40396
+ var SHORT_IDENTITY = "Git doesn't know who you are on this PC yet: pch new and pch clone set that up for each repo.";
40397
+ function signedIn(ctx, headline, { git, notes }) {
40191
40398
  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
40399
  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));
40400
+ const mark = symbols();
40203
40401
  out.line();
40204
- for (const [command, what] of next) {
40205
- out.line(` ${import_picocolors2.default.cyan(command.padEnd(width))} ${import_picocolors2.default.dim(what)}`);
40402
+ out.line(` ${import_picocolors3.default.bold(mark.done)} ${headline}`);
40403
+ if (git.helper && git.url) {
40404
+ out.line(import_picocolors3.default.dim(` Git is signed in too, for repos at ${placeOf(git.url)}.`));
40206
40405
  }
40406
+ for (const note2 of notes) out.line(` ${import_picocolors3.default.bold(mark.note)} ${note2}`);
40407
+ out.line();
40408
+ out.line(` ${import_picocolors3.default.dim("Next")}`);
40409
+ const next = commandList(
40410
+ [
40411
+ [`${pch} search "<your idea>"`, "find a block or game to start from"],
40412
+ [`${pch} agent install`, "give your coding agent Patch's tools"],
40413
+ [`${pch} guide`, "how to make and publish a game"]
40414
+ ],
40415
+ " "
40416
+ );
40417
+ for (const row of next) out.line(row);
40207
40418
  out.line();
40208
40419
  }
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
- }
40222
- async function setUpGit(ctx) {
40420
+ async function setUpGit(ctx, notes) {
40223
40421
  const { gitUrl: url2 } = ctx.target;
40422
+ const say = (text) => notes ? notes.push(text) : ctx.out.step(text);
40224
40423
  if (!url2) return { url: url2, helper: false, lfs: false };
40225
40424
  try {
40226
40425
  await setUpGitHelper(ctx);
40227
40426
  const lfs = await setUpLfs();
40228
- if (!lfs) ctx.out.step(NO_LFS);
40427
+ if (!lfs) say(NO_LFS);
40229
40428
  return { url: url2, helper: true, lfs };
40230
40429
  } catch (error62) {
40231
- ctx.out.step(`Git isn't set up: ${error62.message}`);
40430
+ say(`Git isn't set up: ${error62.message}`);
40232
40431
  return { url: url2, helper: false, lfs: false };
40233
40432
  }
40234
40433
  }
@@ -40239,55 +40438,68 @@ async function withDeviceSession(ctx, options, use) {
40239
40438
  if (!started.response.ok) throw authError(started.body, "Couldn't start a login");
40240
40439
  const device = started.body;
40241
40440
  const code = formatUserCode(device.user_code);
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");
40441
+ const opening = options.browser !== false && Boolean(process.stdout.isTTY);
40442
+ const people = forAPerson(out.json, runningIn());
40443
+ const block = new Block();
40444
+ const drawn = people && options.logo ? logo() : null;
40445
+ if (drawn) process.stderr.write(`
40446
+ ${drawn.map((line) => ` ${line}`).join("\n")}
40447
+ `);
40448
+ if (people) {
40449
+ block.write();
40450
+ block.write(" Sign in to Patch with this code");
40451
+ block.write();
40452
+ const copied = copyText(device.user_code) ? ` ${import_picocolors3.default.dim("copied")}` : "";
40453
+ block.write(`${import_picocolors3.default.bold(pink(device.user_code))}${copied}`);
40454
+ block.write();
40455
+ if (opening) {
40456
+ const page = import_picocolors3.default.bold(`${placeOf(target.origin)}/device`);
40457
+ block.write(` Opening ${page} in your browser: check the code matches, then approve.`);
40458
+ block.write(import_picocolors3.default.dim(` Didn't open? Go to ${device.verification_uri} and enter the code.`));
40459
+ } else {
40460
+ block.write(` Go to ${import_picocolors3.default.bold(device.verification_uri)} and enter the code.`);
40461
+ }
40462
+ block.write();
40258
40463
  } else {
40259
40464
  process.stderr.write(
40260
40465
  `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.
40261
40466
 
40262
- To sign in, open ${import_picocolors2.default.bold(device.verification_uri)} and enter ${import_picocolors2.default.bold(code)}
40467
+ To sign in, open ${import_picocolors3.default.bold(device.verification_uri)} and enter ${import_picocolors3.default.bold(code)}
40263
40468
 
40264
40469
  `
40265
40470
  );
40266
- out.step("Waiting for you to approve in the browser\u2026");
40267
40471
  }
40268
- let sessionToken = null;
40472
+ if (opening) openBrowser(device.verification_uri_complete);
40473
+ const waiting = people ? spinner("Waiting for you to approve", "Ctrl+C to cancel") : { stop() {
40474
+ }, say() {
40475
+ } };
40476
+ if (!people) out.step("Waiting for you to approve in the browser\u2026");
40269
40477
  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 }));
40478
+ const sessionToken = await pollForSession(ctx, device, options);
40479
+ waiting.say("Approved. Finishing up");
40480
+ let value;
40481
+ try {
40482
+ value = await use(createApi(target, { token: sessionToken }));
40483
+ } finally {
40484
+ await fetch(`${auth2}/sign-out`, {
40485
+ method: "POST",
40486
+ headers: { authorization: `Bearer ${sessionToken}`, "content-type": "application/json" },
40487
+ body: "{}"
40488
+ }).catch(() => void 0);
40489
+ }
40490
+ waiting.stop();
40491
+ block.erase();
40492
+ return value;
40279
40493
  } 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);
40494
+ waiting.stop();
40285
40495
  }
40286
40496
  }
40287
- async function approval(ctx, device, options) {
40497
+ var TRY_AGAIN = [["pch login", "get a new code and try again"]];
40498
+ async function pollForSession(ctx, device, options) {
40288
40499
  const auth2 = `${ctx.target.origin}/api/auth`;
40289
40500
  let interval = (options.interval ?? device.interval) * 1e3;
40290
40501
  const deadline = Date.now() + device.expires_in * 1e3;
40502
+ let sessionToken = null;
40291
40503
  while (Date.now() < deadline) {
40292
40504
  await sleep(interval);
40293
40505
  const polled = await postJson(`${auth2}/device/token`, {
@@ -40295,7 +40507,10 @@ async function approval(ctx, device, options) {
40295
40507
  device_code: device.device_code,
40296
40508
  client_id: CLIENT_ID
40297
40509
  });
40298
- if (polled.response.ok) return polled.body.access_token;
40510
+ if (polled.response.ok) {
40511
+ sessionToken = polled.body.access_token;
40512
+ break;
40513
+ }
40299
40514
  const error62 = polled.body.error;
40300
40515
  if (error62 === "authorization_pending") continue;
40301
40516
  if (error62 === "slow_down") {
@@ -40303,12 +40518,27 @@ async function approval(ctx, device, options) {
40303
40518
  continue;
40304
40519
  }
40305
40520
  if (error62 === "access_denied") {
40306
- throw new CliError(EXIT.auth, "access_denied", "The login was denied in the browser.");
40521
+ const denied = new CliError(
40522
+ EXIT.auth,
40523
+ "access_denied",
40524
+ "The login was denied in the browser."
40525
+ );
40526
+ denied.forPeople = { title: "The sign-in was denied in the browser", next: TRY_AGAIN };
40527
+ throw denied;
40307
40528
  }
40308
40529
  if (error62 === "expired_token") break;
40309
40530
  throw authError(polled.body, "The login failed");
40310
40531
  }
40311
- return null;
40532
+ if (!sessionToken) {
40533
+ const expired = new CliError(
40534
+ EXIT.auth,
40535
+ "expired_token",
40536
+ "The code expired. Run pch login again."
40537
+ );
40538
+ expired.forPeople = { title: "The code expired before it was approved", next: TRY_AGAIN };
40539
+ throw expired;
40540
+ }
40541
+ return sessionToken;
40312
40542
  }
40313
40543
  function runningIn(vars = process.env) {
40314
40544
  if (vars.CLAUDECODE === "1") return "Claude Code";
@@ -40347,12 +40577,25 @@ async function whoami(ctx) {
40347
40577
  requireToken(ctx.target);
40348
40578
  const me = await unwrap(ctx.api.GET("/me"));
40349
40579
  ctx.out.result({ ...me, env: ctx.target.env, account: ctx.target.accountName }, () => {
40580
+ const { out, target } = ctx;
40350
40581
  const who = me.username ? `@${me.username}` : me.email;
40351
- const role = me.role === "staff" ? import_picocolors3.default.yellow(" (staff)") : "";
40352
- const via = me.auth.via === "api_key" ? `API key [${(me.auth.scopes ?? []).join(", ")}]` : "session";
40353
- ctx.out.line(`${import_picocolors3.default.bold(who)}${role} on ${ctx.target.env} via ${via}`);
40354
- if (ctx.target.accountName) ctx.out.line(import_picocolors3.default.dim(`account "${ctx.target.accountName}"`));
40355
- for (const org of me.orgs) ctx.out.line(` ${org.slug} ${import_picocolors3.default.dim(`${org.kind}, ${org.role}`)}`);
40582
+ const role = me.role === "staff" ? import_picocolors4.default.dim(" \xB7 staff") : "";
40583
+ const via = me.auth.via === "api_key" ? `an API key (${(me.auth.scopes ?? []).join(", ") || "no scopes"})` : "a session";
40584
+ const saved = target.accountName ? `, saved as account "${target.accountName}"` : "";
40585
+ out.line();
40586
+ out.line(
40587
+ ` ${pink(symbols().done)} ${import_picocolors4.default.bold(who)} on ${placeOf(target.origin)} ${import_picocolors4.default.dim(`(${target.env})`)}${role}`
40588
+ );
40589
+ out.line(import_picocolors4.default.dim(` Signed in with ${via}${saved}.`));
40590
+ if (me.orgs.length) {
40591
+ out.line();
40592
+ out.line(` ${import_picocolors4.default.dim("Orgs")}`);
40593
+ const wide = Math.max(...me.orgs.map((org) => org.slug.length));
40594
+ for (const org of me.orgs) {
40595
+ out.line(` ${org.slug.padEnd(wide)} ${import_picocolors4.default.dim(`${org.kind}, ${org.role}`)}`);
40596
+ }
40597
+ }
40598
+ out.line();
40356
40599
  });
40357
40600
  }
40358
40601
  async function logout(ctx) {
@@ -40376,11 +40619,28 @@ async function logout(ctx) {
40376
40619
  }
40377
40620
  const git = await signOutOfGit(ctx);
40378
40621
  ctx.out.result({ account: name, revoked, git }, () => {
40379
- ctx.out.line(name ? `Signed out of "${name}".` : "PCH_TOKEN stays set; pch keeps no copy.");
40380
- if (name && !revoked) {
40381
- ctx.out.line(import_picocolors3.default.dim("The key wasn't revoked; revoke it on patch.gg if needed."));
40622
+ const { out, target } = ctx;
40623
+ const mark = symbols();
40624
+ out.line();
40625
+ if (name) {
40626
+ const which = import_picocolors4.default.dim(`(account "${name}")`);
40627
+ out.line(` ${pink(mark.done)} Signed out of ${placeOf(target.origin)} ${which}`);
40628
+ if (revoked) out.line(import_picocolors4.default.dim(" Its key is revoked, and pch keeps no copy."));
40629
+ else {
40630
+ out.line(
40631
+ ` ${import_picocolors4.default.bold(mark.note)} The key wasn't revoked: revoke it in your settings on ${placeOf(target.origin)} if you need to.`
40632
+ );
40633
+ }
40634
+ } else {
40635
+ out.line(
40636
+ ` ${pink(mark.done)} Nothing saved to sign out of: PCH_TOKEN stays set, and pch keeps no copy.`
40637
+ );
40382
40638
  }
40383
- if (git.url && !git.helper) ctx.out.line(import_picocolors3.default.dim(`Git is signed out of ${git.url}.`));
40639
+ if (git.url && !git.helper) out.line(import_picocolors4.default.dim(` Git is signed out of ${placeOf(git.url)}.`));
40640
+ out.line();
40641
+ if (name)
40642
+ out.line(` ${import_picocolors4.default.dim("Sign in again with")} ${import_picocolors4.default.bold(`${pchOn(target.env)} login`)}`);
40643
+ if (name) out.line();
40384
40644
  });
40385
40645
  }
40386
40646
  async function signOutOfGit(ctx) {
@@ -40400,7 +40660,7 @@ async function accountList(ctx) {
40400
40660
  ctx.out.result({ data: accounts }, () => {
40401
40661
  if (!accounts.length) ctx.out.line("No saved logins. Run pch login.");
40402
40662
  for (const account2 of accounts) {
40403
- const marker = account2.active ? import_picocolors3.default.green("*") : " ";
40663
+ const marker = account2.active ? import_picocolors4.default.green("*") : " ";
40404
40664
  const who = account2.username ? `@${account2.username}` : "";
40405
40665
  ctx.out.line(`${marker} ${account2.name.padEnd(12)} ${account2.env.padEnd(10)} ${who}`);
40406
40666
  }
@@ -40439,7 +40699,7 @@ async function tokenCreate(ctx, options) {
40439
40699
  ctx.out.result(created, () => {
40440
40700
  ctx.out.line(created.key);
40441
40701
  process.stderr.write(
40442
- import_picocolors3.default.dim("Shown once. Store it as PCH_TOKEN where your agent or CI runs.\n")
40702
+ import_picocolors4.default.dim("Shown once. Store it as PCH_TOKEN where your agent or CI runs.\n")
40443
40703
  );
40444
40704
  });
40445
40705
  }
@@ -40450,7 +40710,7 @@ async function tokenList(ctx) {
40450
40710
  if (!tokens.data.length) ctx.out.line("No API keys.");
40451
40711
  for (const token2 of tokens.data) {
40452
40712
  ctx.out.line(
40453
- `${token2.id} ${(token2.name ?? "").padEnd(24)} ${token2.scopes.join(",").padEnd(18)} ${import_picocolors3.default.dim(token2.start ? `${token2.start}\u2026` : "")}`
40713
+ `${token2.id} ${(token2.name ?? "").padEnd(24)} ${token2.scopes.join(",").padEnd(18)} ${import_picocolors4.default.dim(token2.start ? `${token2.start}\u2026` : "")}`
40454
40714
  );
40455
40715
  }
40456
40716
  });
@@ -40462,7 +40722,7 @@ async function tokenRevoke(ctx, id) {
40462
40722
  }
40463
40723
 
40464
40724
  // src/commands/agent-brief.ts
40465
- var import_picocolors5 = __toESM(require_picocolors(), 1);
40725
+ var import_picocolors6 = __toESM(require_picocolors(), 1);
40466
40726
  import { constants } from "node:fs";
40467
40727
  import { access as access2, copyFile, lstat, mkdir as mkdir2, readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
40468
40728
  import { basename, dirname as dirname3, extname, join as join5, resolve as resolve4 } from "node:path";
@@ -41216,7 +41476,8 @@ function parseInstallPath(value) {
41216
41476
  if (rest && !isReleasePath(rest)) return null;
41217
41477
  return { root, game: game ?? null, path: rest };
41218
41478
  }
41219
- var installPath = external_exports.string().refine(
41479
+ var PATH_MAX = 512;
41480
+ var installPath = external_exports.string().max(PATH_MAX, `must be at most ${PATH_MAX} characters`).refine(
41220
41481
  (value) => parseInstallPath(value) !== null,
41221
41482
  `must start with a folder like {game} or {profile} (${INSTALL_ROOTS.map((root) => `{${root}}`).join(", ")}, or {game:slug} for another listed game) and never use ..`
41222
41483
  );
@@ -41228,19 +41489,19 @@ var gameEntry = external_exports.object({
41228
41489
  ),
41229
41490
  version: shortLine(80).optional().describe('The game versions it works with, in words or as a range, e.g. ">=1.6".'),
41230
41491
  stores: external_exports.array(store).min(1).optional(),
41231
- builds: external_exports.partialRecord(store, external_exports.array(external_exports.string().min(1)).min(1)).optional().describe("Exact builds per store; leave out to accept any build.")
41492
+ builds: external_exports.partialRecord(store, external_exports.array(external_exports.string().min(1).max(80)).min(1).max(32)).optional().describe("Exact builds per store; leave out to accept any build.")
41232
41493
  });
41233
41494
  var component = external_exports.object({
41234
41495
  id: external_exports.string().regex(componentId, "must be lowercase letters, digits, . _ or -"),
41235
- file: external_exports.string().refine((value) => value === "" || isReleasePath(value), "must be a path inside the release").describe('One uploaded file, a folder of them ending in "/", or "" for every file.'),
41496
+ file: external_exports.string().max(PATH_MAX, `must be at most ${PATH_MAX} characters`).refine((value) => value === "" || isReleasePath(value), "must be a path inside the release").describe('One uploaded file, a folder of them ending in "/", or "" for every file.'),
41236
41497
  label: shortLine(80).optional().describe('What it is, e.g. "Main files".'),
41237
41498
  kind: external_exports.enum(["main", "optional", "extra"]).default("main").describe("main: what players need. optional: players can turn it on. extra: not installed."),
41238
41499
  required: external_exports.boolean().optional(),
41239
41500
  extract: external_exports.boolean().optional().describe("The file is a zip to unpack when it's installed.")
41240
41501
  });
41241
41502
  var mapping = external_exports.object({
41242
- component: external_exports.string(),
41243
- from: external_exports.string().refine((value) => value === "" || isReleasePath(value), "must be a path inside the component").default("").describe('"" for all of the component, "folder/" for a folder of it, or one file.'),
41503
+ component: external_exports.string().max(64),
41504
+ from: external_exports.string().max(PATH_MAX, `must be at most ${PATH_MAX} characters`).refine((value) => value === "" || isReleasePath(value), "must be a path inside the component").default("").describe('"" for all of the component, "folder/" for a folder of it, or one file.'),
41244
41505
  to: installPath
41245
41506
  });
41246
41507
  var requirement = external_exports.discriminatedUnion(
@@ -41399,10 +41660,11 @@ var recipeSchema = external_exports.object({
41399
41660
  mode: external_exports.enum(["browser", "installed", "standalone", "workshop", "manual"]).optional().describe(
41400
41661
  "browser: plays in Patch's game window. installed: into a game on the player's PC. standalone: a program of its own. workshop: a Steam Workshop item. manual: players install it by hand. Left out: browser, or installed when there's a host game."
41401
41662
  ),
41402
- games: external_exports.array(gameEntry).default([]),
41403
- components: external_exports.array(component).min(1).optional().describe('Left out: one main component, "main", holding every file.'),
41404
- mappings: external_exports.array(mapping).default([]).describe("Where each component's files go."),
41405
- requirements: external_exports.array(requirement).default([]),
41663
+ // Limits far past any real recipe, so checking one stays cheap.
41664
+ games: external_exports.array(gameEntry).max(16, "can have at most 16 games").default([]),
41665
+ components: external_exports.array(component).min(1).max(64, "can have at most 64 components").optional().describe('Left out: one main component, "main", holding every file.'),
41666
+ mappings: external_exports.array(mapping).max(64, "can have at most 64 mappings").default([]).describe("Where each component's files go."),
41667
+ requirements: external_exports.array(requirement).max(32, "can have at most 32 requirements").default([]),
41406
41668
  launch: launch.optional().describe("Left out: index.html for a browser game, the game itself otherwise."),
41407
41669
  settings: external_exports.array(
41408
41670
  external_exports.object({
@@ -41455,12 +41717,12 @@ var recipeSchema = external_exports.object({
41455
41717
  workshop: external_exports.object({ steam_app_id: external_exports.string().regex(/^\d+$/), file_id: external_exports.string().regex(/^\d+$/) }).optional(),
41456
41718
  credits: external_exports.array(
41457
41719
  external_exports.object({
41458
- files: external_exports.string().min(1),
41459
- license: external_exports.string().min(1).describe("SPDX id, e.g. CC0-1.0 or MIT."),
41460
- source: external_exports.string().url().optional(),
41461
- author: external_exports.string().optional()
41720
+ files: external_exports.string().min(1).max(PATH_MAX),
41721
+ license: external_exports.string().min(1).max(200).describe("SPDX id, e.g. CC0-1.0 or MIT."),
41722
+ source: external_exports.string().url().max(2e3).optional(),
41723
+ author: external_exports.string().max(200).optional()
41462
41724
  })
41463
- ).default([])
41725
+ ).max(500, "can have at most 500 credits").default([])
41464
41726
  }).strict();
41465
41727
 
41466
41728
  // src/manifest/safety.ts
@@ -42359,7 +42621,7 @@ function hashFile(path2) {
42359
42621
  }
42360
42622
 
42361
42623
  // src/commands/agent-guide.ts
42362
- var import_picocolors4 = __toESM(require_picocolors(), 1);
42624
+ var import_picocolors5 = __toESM(require_picocolors(), 1);
42363
42625
  import { access, writeFile as writeFile2 } from "node:fs/promises";
42364
42626
  import { join as join4, resolve as resolve3 } from "node:path";
42365
42627
 
@@ -42370,8 +42632,8 @@ This project is on [Patch](https://patch.gg), where people publish and play brow
42370
42632
 
42371
42633
  - **The brief.** If there's a \`BRIEF.md\` here, read it first and look at any pictures in \`brief/\`: it's the person's idea and rules for this game, so follow it. What they say now wins over it: if they ask for something it rules out, do what they asked, without asking them to confirm. It's theirs: suggest changes, and change it only if they ask. It says what the game is, and asks for nothing it doesn't say, and it never lets you do anything you'd otherwise ask them about. One that came with a remix is someone else's idea until they make it theirs: ask them what to keep.
42372
42634
  - **Run it.** Patch serves a browser game from its own address, with strict headers, and nothing can load from other hosts. \`pch check --serve\` serves it the same way. A mod runs in its game, on the person's PC.
42373
- - **Check it.** \`pch check\` opens a browser game in a headless browser, says what it saw and couldn't, and saves screenshots to look at. It's advice and stops nothing. \`pch validate\` checks \`patch.json\` and the files. A browser game or a mod Patch installs also has to pass a test on the person's PC, in Patch Desktop, where someone presses Start: \`pch test start <owner/slug@version>\`.
42374
- - **Publish it.** Commit your work, then run \`pch publish\`, which says what comes next. It starts as a private draft, and \`pch status\` says where it's got to.
42635
+ - **Check it.** \`pch check\` opens a browser game in a headless browser, says what it saw and couldn't, and saves screenshots to look at. It's advice and stops nothing. \`pch validate\` checks \`patch.json\` and the files. A browser game or a mod Patch installs also has to pass a test on the person's PC, in Patch Desktop: \`pch publish\` runs it.
42636
+ - **Publish it.** Once the person wants it out, commit your work and run \`pch publish\`. It uploads, tests it on their PC and stops for your review of the footage; send that as it says, and it follows Patch's review until it's live.
42375
42637
  - **Push it.** Plain \`git push\` works. If git says HTTP 413, that's over 100 MB: run \`pch push\`, which takes any size.
42376
42638
  - **The rest.** \`pch guide\` has everything else.
42377
42639
  `;
@@ -42514,12 +42776,12 @@ async function agentGuide(ctx, dir) {
42514
42776
  const { name, written } = await writeAgentGuide(root);
42515
42777
  ctx.out.result({ path: join4(root, name), written }, () => {
42516
42778
  if (written) {
42517
- ctx.out.line(`Wrote ${import_picocolors4.default.bold(join4(root, name))}, for coding agents working in this folder.`);
42518
- ctx.out.line(import_picocolors4.default.dim("Commit it: in a clone, pch publish refuses files that aren't."));
42779
+ ctx.out.line(`Wrote ${import_picocolors5.default.bold(join4(root, name))}, for coding agents working in this folder.`);
42780
+ ctx.out.line(import_picocolors5.default.dim("Commit it: in a clone, pch publish refuses files that aren't."));
42519
42781
  } else {
42520
42782
  ctx.out.line(`${join4(root, name)} is this folder's guide for agents, so it's left as it is.`);
42521
42783
  ctx.out.line(
42522
- import_picocolors4.default.dim("pch guide has how to run, check and publish, to add to it if it helps.")
42784
+ import_picocolors5.default.dim("pch guide has how to run, check and publish, to add to it if it helps.")
42523
42785
  );
42524
42786
  }
42525
42787
  });
@@ -42545,10 +42807,10 @@ async function askForBrief(lines, say) {
42545
42807
  };
42546
42808
  const answers = [];
42547
42809
  for (const { ask } of QUESTIONS) {
42548
- say(import_picocolors5.default.bold(ask));
42810
+ say(import_picocolors6.default.bold(ask));
42549
42811
  answers.push((await untilBlank()).join("\n"));
42550
42812
  }
42551
- say(import_picocolors5.default.bold(PICTURES_ASK));
42813
+ say(import_picocolors6.default.bold(PICTURES_ASK));
42552
42814
  return { answers, pictures: await untilBlank() };
42553
42815
  }
42554
42816
  function briefText(answers, pictures) {
@@ -42638,7 +42900,7 @@ async function agentBrief(ctx, dir) {
42638
42900
  ctx.out.step(
42639
42901
  `A brief stays with this game, and every agent that works on it reads it first. ${PUBLIC}`
42640
42902
  );
42641
- ctx.out.step(import_picocolors5.default.dim("A blank line ends an answer, or skips the question."));
42903
+ ctx.out.step(import_picocolors6.default.dim("A blank line ends an answer, or skips the question."));
42642
42904
  const said2 = await fromTerminal(ctx);
42643
42905
  const pictures = await gatherPictures(root, said2.pictures, (line) => ctx.out.step(line));
42644
42906
  const text = briefText(said2.answers, pictures);
@@ -42652,13 +42914,13 @@ async function agentBrief(ctx, dir) {
42652
42914
  await writeAgentGuide(root);
42653
42915
  const silent = await guidesNotNaming(root);
42654
42916
  ctx.out.result({ path: path2, written: true, pictures, guides_without_it: silent }, () => {
42655
- ctx.out.line(`Wrote ${import_picocolors5.default.bold(path2)}. Change it whenever you like: it's yours.`);
42917
+ ctx.out.line(`Wrote ${import_picocolors6.default.bold(path2)}. Change it whenever you like: it's yours.`);
42656
42918
  for (const name of silent) {
42657
42919
  ctx.out.line(
42658
42920
  `${name} doesn't mention it, so agents may not read it. Add this to it: Read ${BRIEF_FILE} first: it's my idea and rules for this game.`
42659
42921
  );
42660
42922
  }
42661
- ctx.out.line(import_picocolors5.default.dim("Commit it, and every agent that clones this repo starts with it."));
42923
+ ctx.out.line(import_picocolors6.default.dim("Commit it, and every agent that clones this repo starts with it."));
42662
42924
  });
42663
42925
  }
42664
42926
  async function guidesNotNaming(root) {
@@ -42671,9 +42933,9 @@ async function guidesNotNaming(root) {
42671
42933
  }
42672
42934
 
42673
42935
  // src/commands/agent-install.ts
42674
- var import_picocolors6 = __toESM(require_picocolors(), 1);
42936
+ var import_picocolors7 = __toESM(require_picocolors(), 1);
42675
42937
  init_errors();
42676
- import { spawnSync } from "node:child_process";
42938
+ import { spawnSync as spawnSync2 } from "node:child_process";
42677
42939
  import { existsSync as existsSync2 } from "node:fs";
42678
42940
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
42679
42941
  import { homedir as homedir2 } from "node:os";
@@ -42703,14 +42965,14 @@ async function agentInstall(ctx, named, options = {}) {
42703
42965
  for (const entry of agents) {
42704
42966
  const name = AGENT_NAMES[entry.agent];
42705
42967
  if (entry.state === "manual") {
42706
- ctx.out.line(`${import_picocolors6.default.bold(name)}: pch can't do this here. ${entry.how}`);
42968
+ ctx.out.line(`${import_picocolors7.default.bold(name)}: pch can't do this here. ${entry.how}`);
42707
42969
  } else {
42708
42970
  ctx.out.line(
42709
- `${import_picocolors6.default.green(`${name}:`)} ${entry.state} the ${import_picocolors6.default.bold(server.name)} MCP server, in ${entry.where}.`
42971
+ `${import_picocolors7.default.green(`${name}:`)} ${entry.state} the ${import_picocolors7.default.bold(server.name)} MCP server, in ${entry.where}.`
42710
42972
  );
42711
42973
  }
42712
42974
  }
42713
- ctx.out.line(import_picocolors6.default.dim(`It runs ${commandLine(server)}, and holds no key.`));
42975
+ ctx.out.line(import_picocolors7.default.dim(`It runs ${commandLine(server)}, and holds no key.`));
42714
42976
  if (!signedIn2) ctx.out.line(`Sign in first, or its tools won't work: ${pchLogin(ctx)}.`);
42715
42977
  ctx.out.line(
42716
42978
  "Restart your agent to load its tools. Until then, pch guide prints the same guide."
@@ -42828,11 +43090,11 @@ function onPath(bin) {
42828
43090
  function run(bin, args2) {
42829
43091
  const shell = process.platform === "win32";
42830
43092
  const quoted = shell ? args2.map((arg) => /\s/.test(arg) ? `"${arg}"` : arg) : args2;
42831
- return spawnSync(bin, quoted, { encoding: "utf8", shell, timeout: 6e4 });
43093
+ return spawnSync2(bin, quoted, { encoding: "utf8", shell, timeout: 6e4 });
42832
43094
  }
42833
43095
 
42834
43096
  // src/commands/assets.ts
42835
- var import_picocolors7 = __toESM(require_picocolors(), 1);
43097
+ var import_picocolors8 = __toESM(require_picocolors(), 1);
42836
43098
  import { createHash as createHash2 } from "node:crypto";
42837
43099
  import { mkdir as mkdir4, readFile as readFile5, stat as stat2, writeFile as writeFile5 } from "node:fs/promises";
42838
43100
  import { basename as basename2, dirname as dirname5, extname as extname2, isAbsolute, join as join7, relative as relative2, resolve as resolve6, sep as sep3 } from "node:path";
@@ -42873,17 +43135,17 @@ async function assetsSearch(ctx, q, options) {
42873
43135
  for (const hit of found.hits) {
42874
43136
  const size = hit.fit?.mb ? ` \xB7 ${hit.fit.mb} MB` : "";
42875
43137
  ctx.out.line(
42876
- `${import_picocolors7.default.bold(plain(hit.id))} ${import_picocolors7.default.dim(`${hit.type} \xB7 ${hit.license}${size}${hit.ai ? " \xB7 AI-generated" : ""}`)}`
43138
+ `${import_picocolors8.default.bold(plain(hit.id))} ${import_picocolors8.default.dim(`${hit.type} \xB7 ${hit.license}${size}${hit.ai ? " \xB7 AI-generated" : ""}`)}`
42877
43139
  );
42878
43140
  ctx.out.line(` ${plain(hit.title)}`);
42879
- if (hit.thumbnail_url) ctx.out.line(import_picocolors7.default.dim(` ${plain(hit.thumbnail_url)}`));
43141
+ if (hit.thumbnail_url) ctx.out.line(import_picocolors8.default.dim(` ${plain(hit.thumbnail_url)}`));
42880
43142
  }
42881
43143
  ctx.out.line(
42882
- import_picocolors7.default.dim(
43144
+ import_picocolors8.default.dim(
42883
43145
  `${found.total} in all. Look at the pictures, as titles don't say how an asset looks, then pch assets add <id>\u2026`
42884
43146
  )
42885
43147
  );
42886
- if (found.next_cursor) ctx.out.line(import_picocolors7.default.dim(`More: --cursor ${found.next_cursor}`));
43148
+ if (found.next_cursor) ctx.out.line(import_picocolors8.default.dim(`More: --cursor ${found.next_cursor}`));
42887
43149
  });
42888
43150
  }
42889
43151
  async function assetsFacets(ctx, q, options) {
@@ -42895,11 +43157,11 @@ async function assetsFacets(ctx, q, options) {
42895
43157
  async function assetsView(ctx, id) {
42896
43158
  const asset = await unwrap(ctx.api.GET("/assets/{id}", { params: { path: { id } } }));
42897
43159
  ctx.out.result(asset, () => {
42898
- ctx.out.line(`${import_picocolors7.default.bold(plain(asset.title))} ${import_picocolors7.default.dim(asset.id)}`);
43160
+ ctx.out.line(`${import_picocolors8.default.bold(plain(asset.title))} ${import_picocolors8.default.dim(asset.id)}`);
42899
43161
  ctx.out.line(`${asset.type_label} \xB7 ${asset.license.spdx} \xB7 ${plain(asset.source.name)}`);
42900
43162
  if (asset.description) ctx.out.line(plain(asset.description));
42901
43163
  ctx.out.line(`Credit: ${plain(asset.credit)}`);
42902
- ctx.out.line(import_picocolors7.default.dim(`Take it with pch assets add ${asset.id}`));
43164
+ ctx.out.line(import_picocolors8.default.dim(`Take it with pch assets add ${asset.id}`));
42903
43165
  });
42904
43166
  }
42905
43167
  async function assetsAdd(ctx, ids, options) {
@@ -42980,13 +43242,13 @@ async function assetsAdd(ctx, ids, options) {
42980
43242
  },
42981
43243
  () => {
42982
43244
  ctx.out.line(
42983
- import_picocolors7.default.green(`Copied ${copied.length} ${copied.length === 1 ? "file" : "files"} to ${root}.`)
43245
+ import_picocolors8.default.green(`Copied ${copied.length} ${copied.length === 1 ? "file" : "files"} to ${root}.`)
42984
43246
  );
42985
43247
  for (const entry of usage3) {
42986
- ctx.out.line(`${import_picocolors7.default.bold(entry.id)} is at ${entry.path}`);
43248
+ ctx.out.line(`${import_picocolors8.default.bold(entry.id)} is at ${entry.path}`);
42987
43249
  ctx.out.line(` ${plain(entry.usage)}`);
42988
43250
  }
42989
- for (const warning of plan.warnings) ctx.out.line(import_picocolors7.default.dim(plain(warning.msg)));
43251
+ for (const warning of plan.warnings) ctx.out.line(import_picocolors8.default.dim(plain(warning.msg)));
42990
43252
  ctx.out.line("Load them by relative path, and commit them: the release carries its files.");
42991
43253
  ctx.out.line(
42992
43254
  `Their credits are in ${fromHere(creditsFile)}. Keep it in the release, with the licences, and show them on a credits screen in the game.`
@@ -43113,7 +43375,7 @@ async function saveChecked(url2, path2, sha256) {
43113
43375
  }
43114
43376
 
43115
43377
  // src/commands/catalog.ts
43116
- var import_picocolors8 = __toESM(require_picocolors(), 1);
43378
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
43117
43379
  import { randomUUID } from "node:crypto";
43118
43380
  import { readFile as readFile6 } from "node:fs/promises";
43119
43381
  init_errors();
@@ -43216,18 +43478,18 @@ async function projectCreate(ctx, ref, options) {
43216
43478
  ) : (await ctx.api.GET("/projects/{owner}/{slug}/repo", path2)).data;
43217
43479
  const whoCanSee = repo?.visibility === "private" ? { who_can_see: PRIVATE_REPO } : {};
43218
43480
  ctx.out.result({ ...project2, repo_visibility: repo?.visibility ?? null, ...whoCanSee }, () => {
43219
- ctx.out.line(`Created ${import_picocolors8.default.bold(project2.handle)}`);
43481
+ ctx.out.line(`Created ${import_picocolors9.default.bold(project2.handle)}`);
43220
43482
  ctx.out.line(
43221
43483
  ` Source: ${SOURCE_LABELS[project2.source_visibility]} \xB7 code ${project2.code_license} \xB7 art ${project2.art_license}`
43222
43484
  );
43223
43485
  if (repo) ctx.out.line(` Repo: ${VISIBILITY_LABELS[repo.visibility]}`);
43224
43486
  if (repo?.visibility === "private") ctx.out.line(` ${PRIVATE_REPO_NOT_LOCKED}`);
43225
43487
  ctx.out.line(
43226
- import_picocolors8.default.dim(
43488
+ import_picocolors9.default.dim(
43227
43489
  `Change these with pch project update ${project2.handle}${repo ? ` and pch set ${project2.handle} --visibility \u2026` : ""}.`
43228
43490
  )
43229
43491
  );
43230
- ctx.out.line(import_picocolors8.default.dim(projectUrl(ctx.target.origin, { owner, slug: slug2 })));
43492
+ ctx.out.line(import_picocolors9.default.dim(projectUrl(ctx.target.origin, { owner, slug: slug2 })));
43231
43493
  });
43232
43494
  }
43233
43495
  async function projectView(ctx, ref) {
@@ -43250,7 +43512,7 @@ async function projectView(ctx, ref) {
43250
43512
  openCode
43251
43513
  ].filter(Boolean).join(" ");
43252
43514
  ctx.out.result({ ...project2, ...whoCanSee ? { who_can_see: whoCanSee } : {} }, () => {
43253
- ctx.out.line(`${import_picocolors8.default.bold(project2.name)} ${import_picocolors8.default.dim(project2.handle)}`);
43515
+ ctx.out.line(`${import_picocolors9.default.bold(project2.name)} ${import_picocolors9.default.dim(project2.handle)}`);
43254
43516
  if (project2.summary) ctx.out.line(project2.summary);
43255
43517
  const kind = project2.kind ? KIND_LABELS[project2.kind] : "not published yet";
43256
43518
  const games = project2.host_games.map((game) => `${game.name} (${game.role})`).join(", ");
@@ -43281,7 +43543,7 @@ async function projectView(ctx, ref) {
43281
43543
  `Source: ${repo ? VISIBILITY_LABELS[repo.visibility] : SOURCE_LABELS[project2.source_visibility]} \xB7 code ${project2.code_license} \xB7 art ${project2.art_license}`
43282
43544
  );
43283
43545
  if (repo?.visibility === "private" && !notLocked) ctx.out.line(PRIVATE_REPO_NOT_LOCKED);
43284
- ctx.out.line(import_picocolors8.default.dim(projectUrl(ctx.target.origin, { owner, slug: slug2 })));
43546
+ ctx.out.line(import_picocolors9.default.dim(projectUrl(ctx.target.origin, { owner, slug: slug2 })));
43285
43547
  });
43286
43548
  }
43287
43549
  var LINK_NAMES = ["discord", "x", "telegram", "youtube", "twitch", "github"];
@@ -43317,8 +43579,8 @@ async function projectUpdate(ctx, ref, options) {
43317
43579
  })
43318
43580
  );
43319
43581
  ctx.out.result(updated, () => {
43320
- ctx.out.line(`Updated ${import_picocolors8.default.bold(updated.handle)}`);
43321
- ctx.out.line(import_picocolors8.default.dim(updated.urls?.page ?? projectUrl(ctx.target.origin, updated)));
43582
+ ctx.out.line(`Updated ${import_picocolors9.default.bold(updated.handle)}`);
43583
+ ctx.out.line(import_picocolors9.default.dim(updated.urls?.page ?? projectUrl(ctx.target.origin, updated)));
43322
43584
  });
43323
43585
  }
43324
43586
  function parseLinks(values) {
@@ -43352,7 +43614,7 @@ async function myProjects(ctx) {
43352
43614
  if (!data.length) ctx.out.line("No projects yet. Start one with pch new <you>/<slug>.");
43353
43615
  for (const project2 of data) {
43354
43616
  ctx.out.line(
43355
- `${project2.handle.padEnd(32)} ${(project2.kind ?? "").padEnd(14)} ${project2.latest_version?.version ?? import_picocolors8.default.dim("nothing published")}`
43617
+ `${project2.handle.padEnd(32)} ${(project2.kind ?? "").padEnd(14)} ${project2.latest_version?.version ?? import_picocolors9.default.dim("nothing published")}`
43356
43618
  );
43357
43619
  }
43358
43620
  });
@@ -43392,41 +43654,41 @@ async function releaseList(ctx, ref) {
43392
43654
  } while (cursor);
43393
43655
  ctx.out.result({ data }, () => {
43394
43656
  if (!data.length) ctx.out.line("No releases.");
43395
- for (const release2 of data) {
43657
+ for (const release3 of data) {
43396
43658
  ctx.out.line(
43397
- `${release2.version.padEnd(12)} ${release2.channel.padEnd(9)} ${release2.status.padEnd(10)} ${import_picocolors8.default.dim(release2.published_at ?? release2.created_at)}`
43659
+ `${release3.version.padEnd(12)} ${release3.channel.padEnd(9)} ${release3.status.padEnd(10)} ${import_picocolors9.default.dim(release3.published_at ?? release3.created_at)}`
43398
43660
  );
43399
43661
  }
43400
43662
  });
43401
43663
  }
43402
43664
  async function releaseView(ctx, ref) {
43403
43665
  const { owner, slug: slug2, version: version2 } = parseReleaseRef(ref);
43404
- const release2 = await unwrap(
43666
+ const release3 = await unwrap(
43405
43667
  ctx.api.GET("/projects/{owner}/{slug}/releases/{version}", {
43406
43668
  params: { path: { owner, slug: slug2, version: version2 } }
43407
43669
  })
43408
43670
  );
43409
- const repo = release2.channel === "private" ? await ctx.api.GET("/projects/{owner}/{slug}/repo", { params: { path: { owner, slug: slug2 } } }).catch(() => null) : null;
43410
- const openCode = openCodeWords(`${owner}/${slug2}`, repo?.data, release2);
43411
- ctx.out.result({ ...release2, ...openCode ? { who_can_see: openCode } : {} }, () => {
43412
- ctx.out.line(`${import_picocolors8.default.bold(`${release2.project}@${release2.version}`)} ${release2.kind ?? ""}`);
43671
+ const repo = release3.channel === "private" ? await ctx.api.GET("/projects/{owner}/{slug}/repo", { params: { path: { owner, slug: slug2 } } }).catch(() => null) : null;
43672
+ const openCode = openCodeWords(`${owner}/${slug2}`, repo?.data, release3);
43673
+ ctx.out.result({ ...release3, ...openCode ? { who_can_see: openCode } : {} }, () => {
43674
+ ctx.out.line(`${import_picocolors9.default.bold(`${release3.project}@${release3.version}`)} ${release3.kind ?? ""}`);
43413
43675
  ctx.out.line(
43414
- `Channel ${release2.channel} \xB7 ${release2.status} \xB7 ${TRUST_LABELS[release2.trust_tier]}`
43676
+ `Channel ${release3.channel} \xB7 ${release3.status} \xB7 ${TRUST_LABELS[release3.trust_tier]}`
43415
43677
  );
43416
43678
  if (openCode) ctx.out.line(openCode);
43417
- if (release2.commit) {
43418
- ctx.out.line(`From commit ${release2.commit}${release2.tag ? `, tag ${release2.tag}` : ""}`);
43679
+ if (release3.commit) {
43680
+ ctx.out.line(`From commit ${release3.commit}${release3.tag ? `, tag ${release3.tag}` : ""}`);
43419
43681
  }
43420
- for (const requirement2 of release2.requirements) {
43682
+ for (const requirement2 of release3.requirements) {
43421
43683
  ctx.out.line(`Needs ${requirement2.name} (${requirement2.role})`);
43422
43684
  }
43423
- if (release2.changelog) ctx.out.line(plain(release2.changelog));
43424
- for (const file2 of release2.files) {
43685
+ if (release3.changelog) ctx.out.line(plain(release3.changelog));
43686
+ for (const file2 of release3.files) {
43425
43687
  ctx.out.line(
43426
- ` ${file2.path.padEnd(40)} ${formatSize(file2.size).padStart(8)} ${import_picocolors8.default.dim(file2.sha256)}`
43688
+ ` ${file2.path.padEnd(40)} ${formatSize(file2.size).padStart(8)} ${import_picocolors9.default.dim(file2.sha256)}`
43427
43689
  );
43428
43690
  if (file2.url) ctx.out.line(` ${file2.url}`);
43429
- else if (file2.url === null) ctx.out.line(import_picocolors8.default.dim(" can't be had right now"));
43691
+ else if (file2.url === null) ctx.out.line(import_picocolors9.default.dim(" can't be had right now"));
43430
43692
  }
43431
43693
  });
43432
43694
  }
@@ -43435,7 +43697,7 @@ async function hostGameSearch(ctx, q, limit2) {
43435
43697
  ctx.out.result(games, () => {
43436
43698
  if (!games.data.length) ctx.out.line("No host games found.");
43437
43699
  for (const game of games.data) ctx.out.line(`${game.slug.padEnd(28)} ${game.name}`);
43438
- if (games.next_cursor) ctx.out.line(import_picocolors8.default.dim("More match; narrow the search or raise --limit."));
43700
+ if (games.next_cursor) ctx.out.line(import_picocolors9.default.dim("More match; narrow the search or raise --limit."));
43439
43701
  });
43440
43702
  }
43441
43703
  var WIKI_PAGE = /^(engines|methods)\/([a-z0-9][a-z0-9-]*)(?:\.md)?$/;
@@ -43449,7 +43711,7 @@ function showWikiPage(ctx, page) {
43449
43711
  page.build_seen && `build: ${page.build_seen}`
43450
43712
  ].filter(Boolean).join(" \xB7 ");
43451
43713
  ctx.out.line();
43452
- ctx.out.line(`${import_picocolors8.default.bold(page.path)} ${import_picocolors8.default.dim(plain(about))}`);
43714
+ ctx.out.line(`${import_picocolors9.default.bold(page.path)} ${import_picocolors9.default.dim(plain(about))}`);
43453
43715
  ctx.out.line(plain(page.text));
43454
43716
  }
43455
43717
  async function hostGameView(ctx, slug2) {
@@ -43477,13 +43739,13 @@ async function hostGameView(ctx, slug2) {
43477
43739
  ...unavailable ? { wiki_pages_unavailable: unavailable } : {}
43478
43740
  };
43479
43741
  ctx.out.result(result, () => {
43480
- ctx.out.line(`${import_picocolors8.default.bold(game.name)} ${import_picocolors8.default.dim(game.slug)}`);
43742
+ ctx.out.line(`${import_picocolors9.default.bold(game.name)} ${import_picocolors9.default.dim(game.slug)}`);
43481
43743
  for (const store2 of game.stores) ctx.out.line(` ${store2.store} ${store2.ref}`);
43482
43744
  for (const build of game.builds) {
43483
43745
  ctx.out.line(` build ${build.store} ${build.build_ref} ${build.version_label ?? ""}`);
43484
43746
  }
43485
- if (!ctx.target.token) ctx.out.line(import_picocolors8.default.dim("Sign in with pch login for Patch's wiki pages."));
43486
- if (unavailable) ctx.out.line(import_picocolors8.default.dim(`No wiki pages this time: ${plain(unavailable)}`));
43747
+ if (!ctx.target.token) ctx.out.line(import_picocolors9.default.dim("Sign in with pch login for Patch's wiki pages."));
43748
+ if (unavailable) ctx.out.line(import_picocolors9.default.dim(`No wiki pages this time: ${plain(unavailable)}`));
43487
43749
  for (const page of pages) showWikiPage(ctx, page);
43488
43750
  });
43489
43751
  }
@@ -43506,11 +43768,11 @@ async function noteSuggest(ctx, about, fact, options) {
43506
43768
  }
43507
43769
  const note2 = data;
43508
43770
  ctx.out.result(note2, () => {
43509
- ctx.out.line(`Suggested for ${import_picocolors8.default.bold(plain(note2.page))}. ${plain(note2.next)}`);
43771
+ ctx.out.line(`Suggested for ${import_picocolors9.default.bold(plain(note2.page))}. ${plain(note2.next)}`);
43510
43772
  for (const finding of note2.findings) {
43511
43773
  ctx.out.line(` ${finding.rule_id}: ${plain(finding.message)}`);
43512
- if (finding.line) ctx.out.line(import_picocolors8.default.dim(` ${plain(finding.line)}`));
43513
- if (finding.suggestion) ctx.out.line(import_picocolors8.default.dim(` ${plain(finding.suggestion)}`));
43774
+ if (finding.line) ctx.out.line(import_picocolors9.default.dim(` ${plain(finding.line)}`));
43775
+ if (finding.suggestion) ctx.out.line(import_picocolors9.default.dim(` ${plain(finding.suggestion)}`));
43514
43776
  }
43515
43777
  });
43516
43778
  }
@@ -43519,7 +43781,7 @@ function titleCase(slug2) {
43519
43781
  }
43520
43782
 
43521
43783
  // src/commands/change-requests.ts
43522
- var import_picocolors9 = __toESM(require_picocolors(), 1);
43784
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
43523
43785
  init_errors();
43524
43786
  var handle = (ref) => `${ref.owner}/${ref.slug}`;
43525
43787
  var counted = (count, thing) => `${count} ${thing}${count === 1 ? "" : "s"}`;
@@ -43561,7 +43823,7 @@ async function changeRequestOpen(ctx, ref, options) {
43561
43823
  const name = `${handle(project2)}#${request.number}`;
43562
43824
  out.result({ project: handle(project2), ...request, ...ahead.fields }, () => {
43563
43825
  const shown = plain(request);
43564
- out.line(`Opened ${import_picocolors9.default.bold(name)}: ${shown.title}`);
43826
+ out.line(`Opened ${import_picocolors10.default.bold(name)}: ${shown.title}`);
43565
43827
  out.line(`${handle(from)}'s ${shown.branch} into ${shown.into}`);
43566
43828
  showStanding(ctx, shown, ahead);
43567
43829
  });
@@ -43637,19 +43899,19 @@ function updateAdvice(request, steps) {
43637
43899
  ];
43638
43900
  }
43639
43901
  function showStanding({ out }, request, { fields }) {
43640
- if (fields.up_to_date === true) out.line(import_picocolors9.default.green("Up to date: it can merge."));
43902
+ if (fields.up_to_date === true) out.line(import_picocolors10.default.green("Up to date: it can merge."));
43641
43903
  if (fields.up_to_date === false) {
43642
- out.line(import_picocolors9.default.yellow(`Out of date: ${outOfDate(request)}`));
43904
+ out.line(import_picocolors10.default.yellow(`Out of date: ${outOfDate(request)}`));
43643
43905
  if (fields.update) for (const line of updateAdvice(request, fields.update)) out.line(line);
43644
43906
  }
43645
43907
  if (fields.up_to_date === null && request.state === "open") {
43646
43908
  out.line(
43647
- import_picocolors9.default.yellow("Patch can't tell whether it's up to date just now. Try again in a moment.")
43909
+ import_picocolors10.default.yellow("Patch can't tell whether it's up to date just now. Try again in a moment.")
43648
43910
  );
43649
43911
  }
43650
43912
  if (request.problems.length) {
43651
43913
  out.line(
43652
- import_picocolors9.default.yellow(
43914
+ import_picocolors10.default.yellow(
43653
43915
  "The latest push to its branch hasn't reached this request, so it stays where it was:"
43654
43916
  )
43655
43917
  );
@@ -43684,20 +43946,20 @@ async function changeRequestList(ctx, ref, options) {
43684
43946
  }
43685
43947
  for (const request of plain(data)) {
43686
43948
  const open3 = request.state === "open";
43687
- const state = request.up_to_date === false ? import_picocolors9.default.yellow("out of date".padEnd(12)) : open3 && request.up_to_date === null ? import_picocolors9.default.yellow("not checked".padEnd(12)) : request.state.padEnd(12);
43949
+ const state = request.up_to_date === false ? import_picocolors10.default.yellow("out of date".padEnd(12)) : open3 && request.up_to_date === null ? import_picocolors10.default.yellow("not checked".padEnd(12)) : request.state.padEnd(12);
43688
43950
  const from = request.from ? `${request.from} ${request.branch}` : request.branch;
43689
43951
  out.line(
43690
- `#${String(request.number).padEnd(4)} ${state} ${request.title} ${import_picocolors9.default.dim(`(${from} \u2192 ${request.into})`)}`
43952
+ `#${String(request.number).padEnd(4)} ${state} ${request.title} ${import_picocolors10.default.dim(`(${from} \u2192 ${request.into})`)}`
43691
43953
  );
43692
43954
  }
43693
43955
  if (data.some((request) => request.up_to_date === false)) {
43694
43956
  out.line(
43695
- import_picocolors9.default.dim(
43957
+ import_picocolors10.default.dim(
43696
43958
  `Out of date ones can't merge yet. pch change-request view ${handle(project2)}#<number> says how to update one.`
43697
43959
  )
43698
43960
  );
43699
43961
  }
43700
- if (cursor) out.line(import_picocolors9.default.dim("There are more; raise --limit."));
43962
+ if (cursor) out.line(import_picocolors10.default.dim("There are more; raise --limit."));
43701
43963
  });
43702
43964
  }
43703
43965
  async function mapInTurn(items, size, run3) {
@@ -43743,7 +44005,7 @@ async function readRequest(ctx, ref) {
43743
44005
  function showHeader(ctx, project2, request, diff, fetch2) {
43744
44006
  const { out } = ctx;
43745
44007
  out.line(
43746
- `${import_picocolors9.default.bold(`${handle(project2)}#${request.number}`)} ${request.title} ${import_picocolors9.default.dim(request.state)}`
44008
+ `${import_picocolors10.default.bold(`${handle(project2)}#${request.number}`)} ${request.title} ${import_picocolors10.default.dim(request.state)}`
43747
44009
  );
43748
44010
  const parts = [
43749
44011
  `${request.from ? `${request.from}'s ` : ""}${request.branch} into ${request.into}`,
@@ -43762,15 +44024,15 @@ function showHeader(ctx, project2, request, diff, fetch2) {
43762
44024
  } else if (request.closed_by) {
43763
44025
  out.line(`${request.state === "declined" ? "Declined" : "Withdrawn"} by ${request.closed_by}`);
43764
44026
  }
43765
- if (fetch2) out.line(import_picocolors9.default.dim(`To get its commits: ${fetch2}`));
44027
+ if (fetch2) out.line(import_picocolors10.default.dim(`To get its commits: ${fetch2}`));
43766
44028
  }
43767
44029
  function showTalk({ out }, talk) {
43768
44030
  if (!talk.length) return;
43769
44031
  out.line();
43770
- out.line(import_picocolors9.default.bold("Talk"));
44032
+ out.line(import_picocolors10.default.bold("Talk"));
43771
44033
  for (const post of talk) {
43772
44034
  const who = `${post.author ?? "someone"}${post.via === "api_key" ? " (with a key)" : ""}`;
43773
- out.line(`${import_picocolors9.default.dim(`${who}, ${post.created_at}`)}`);
44035
+ out.line(`${import_picocolors10.default.dim(`${who}, ${post.created_at}`)}`);
43774
44036
  out.line(post.body);
43775
44037
  }
43776
44038
  }
@@ -43803,18 +44065,18 @@ async function changeRequestView(ctx, ref) {
43803
44065
  function showFiles({ out }, diff) {
43804
44066
  if (tooBigToList(diff)) {
43805
44067
  out.line();
43806
- out.line(import_picocolors9.default.yellow("There's more in this request than can show here."));
44068
+ out.line(import_picocolors10.default.yellow("There's more in this request than can show here."));
43807
44069
  }
43808
44070
  if (!diff.files.length) return;
43809
44071
  out.line();
43810
44072
  for (const file2 of diff.files.slice(0, FILES_SHOWN)) {
43811
44073
  out.line(
43812
- `${file2.status.padEnd(9)} ${file2.path} ${import_picocolors9.default.dim(`+${file2.additions} -${file2.deletions}`)}`
44074
+ `${file2.status.padEnd(9)} ${file2.path} ${import_picocolors10.default.dim(`+${file2.additions} -${file2.deletions}`)}`
43813
44075
  );
43814
44076
  }
43815
44077
  if (diff.files.length > FILES_SHOWN) {
43816
44078
  out.line(
43817
- import_picocolors9.default.dim(
44079
+ import_picocolors10.default.dim(
43818
44080
  `And ${diff.files.length - FILES_SHOWN} more: pch change-request review has them all.`
43819
44081
  )
43820
44082
  );
@@ -43834,7 +44096,7 @@ async function changeRequestReview(ctx, ref) {
43834
44096
  }
43835
44097
  if (shown.diff) showDiff(ctx, shown.diff, shown.fetch);
43836
44098
  else
43837
- out.line(import_picocolors9.default.yellow("Patch couldn't read what it changes just now. Try again in a moment."));
44099
+ out.line(import_picocolors10.default.yellow("Patch couldn't read what it changes just now. Try again in a moment."));
43838
44100
  showTalk(ctx, shown.talk);
43839
44101
  });
43840
44102
  }
@@ -43846,30 +44108,30 @@ var NO_PATCH = {
43846
44108
  };
43847
44109
  function showDiff({ out }, diff, fetch2) {
43848
44110
  out.line();
43849
- out.line(import_picocolors9.default.bold("Commits"));
44111
+ out.line(import_picocolors10.default.bold("Commits"));
43850
44112
  for (const commit of diff.commits) {
43851
- out.line(`${import_picocolors9.default.dim(commit.id.slice(0, 7))} ${commit.subject} ${import_picocolors9.default.dim(commit.author)}`);
44113
+ out.line(`${import_picocolors10.default.dim(commit.id.slice(0, 7))} ${commit.subject} ${import_picocolors10.default.dim(commit.author)}`);
43852
44114
  }
43853
44115
  if (diff.commit_count > diff.commits.length) {
43854
- out.line(import_picocolors9.default.dim(`The newest ${diff.commits.length} of ${diff.commit_count}.`));
44116
+ out.line(import_picocolors10.default.dim(`The newest ${diff.commits.length} of ${diff.commit_count}.`));
43855
44117
  }
43856
44118
  for (const file2 of diff.files) {
43857
44119
  const moved2 = file2.old_path ? `${file2.old_path} \u2192 ` : "";
43858
44120
  out.line();
43859
44121
  out.line(
43860
- `${import_picocolors9.default.bold(`${moved2}${file2.path}`)} ${import_picocolors9.default.dim(`${file2.status}, +${file2.additions} -${file2.deletions}`)}`
44122
+ `${import_picocolors10.default.bold(`${moved2}${file2.path}`)} ${import_picocolors10.default.dim(`${file2.status}, +${file2.additions} -${file2.deletions}`)}`
43861
44123
  );
43862
44124
  if (file2.big_file) {
43863
44125
  const size = (side) => side ? formatSize(side.size) : "nothing";
43864
44126
  out.line(`Big file: ${size(file2.big_file.old)} \u2192 ${size(file2.big_file.new)}`);
43865
44127
  }
43866
44128
  if (file2.patch) out.line(file2.patch.trimEnd());
43867
- else if (file2.no_patch) out.line(import_picocolors9.default.dim(NO_PATCH[file2.no_patch]));
44129
+ else if (file2.no_patch) out.line(import_picocolors10.default.dim(NO_PATCH[file2.no_patch]));
43868
44130
  }
43869
44131
  if (diff.truncated) {
43870
44132
  out.line();
43871
44133
  out.line(
43872
- import_picocolors9.default.yellow(
44134
+ import_picocolors10.default.yellow(
43873
44135
  tooBigToList(diff) ? `None of it can be shown here: there's more in this request than can show.${fetch2 ? ` To read it yourself: ${fetch2}` : ""}` : "Some of it isn't shown: patches stop at 300 files or 2 MiB, and the list at 3,000."
43874
44136
  )
43875
44137
  );
@@ -43911,7 +44173,7 @@ async function changeRequestMerge(ctx, ref, options) {
43911
44173
  const request = merged.data;
43912
44174
  out.result({ project: handle(project2), ...request }, () => {
43913
44175
  const at = request.merge_commit ? ` at ${request.merge_commit.slice(0, 7)}` : "";
43914
- out.line(`Merged ${import_picocolors9.default.bold(`${handle(project2)}#${number4}`)} into ${request.into}${at}.`);
44176
+ out.line(`Merged ${import_picocolors10.default.bold(`${handle(project2)}#${number4}`)} into ${request.into}${at}.`);
43915
44177
  });
43916
44178
  }
43917
44179
  async function cantMergeYet(ctx, project2, number4) {
@@ -43941,7 +44203,7 @@ async function changeRequestDecline(ctx, ref) {
43941
44203
  })
43942
44204
  );
43943
44205
  out.result({ project: handle(project2), ...request }, () => {
43944
- out.line(`Declined ${import_picocolors9.default.bold(`${handle(project2)}#${number4}`)}.`);
44206
+ out.line(`Declined ${import_picocolors10.default.bold(`${handle(project2)}#${number4}`)}.`);
43945
44207
  });
43946
44208
  }
43947
44209
  async function changeRequestWithdraw(ctx, ref) {
@@ -43954,12 +44216,12 @@ async function changeRequestWithdraw(ctx, ref) {
43954
44216
  })
43955
44217
  );
43956
44218
  out.result({ project: handle(project2), ...request }, () => {
43957
- out.line(`Withdrew ${import_picocolors9.default.bold(`${handle(project2)}#${number4}`)}.`);
44219
+ out.line(`Withdrew ${import_picocolors10.default.bold(`${handle(project2)}#${number4}`)}.`);
43958
44220
  });
43959
44221
  }
43960
44222
 
43961
44223
  // src/commands/check.ts
43962
- var import_picocolors10 = __toESM(require_picocolors(), 1);
44224
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
43963
44225
  import { existsSync as existsSync4 } from "node:fs";
43964
44226
  import { mkdir as mkdir6, mkdtemp as mkdtemp2 } from "node:fs/promises";
43965
44227
  import { tmpdir as tmpdir2 } from "node:os";
@@ -45170,10 +45432,10 @@ function gpuName(renderer) {
45170
45432
  init_errors();
45171
45433
  var usage2 = (code, message) => new CliError(EXIT.usage, code, message);
45172
45434
  var SAYS = {
45173
- pass: (text) => import_picocolors10.default.green(text),
45174
- fail: (text) => import_picocolors10.default.red(text),
45175
- warn: (text) => import_picocolors10.default.yellow(text),
45176
- cant_check: (text) => import_picocolors10.default.yellow(text)
45435
+ pass: (text) => import_picocolors11.default.green(text),
45436
+ fail: (text) => import_picocolors11.default.red(text),
45437
+ warn: (text) => import_picocolors11.default.yellow(text),
45438
+ cant_check: (text) => import_picocolors11.default.yellow(text)
45177
45439
  };
45178
45440
  var LABELS = {
45179
45441
  pass: "pass ",
@@ -45181,7 +45443,7 @@ var LABELS = {
45181
45443
  warn: "look ",
45182
45444
  cant_check: "can't"
45183
45445
  };
45184
- var ADVICE = "This is advice: nothing here stops you publishing, and it doesn't judge what kind of game this is. Patch's own test runs in Patch Desktop: pch test start.";
45446
+ var ADVICE = "This is advice: nothing here stops you publishing, and it doesn't judge what kind of game this is. Patch's own test runs in Patch Desktop, as pch publish does it.";
45185
45447
  async function findGame(dir) {
45186
45448
  const root = resolve8(dir);
45187
45449
  if (existsSync4(join12(root, "patch.json"))) {
@@ -45189,7 +45451,7 @@ async function findGame(dir) {
45189
45451
  if (project2.recipe.mode !== "browser") {
45190
45452
  throw usage2(
45191
45453
  "not_a_browser_game",
45192
- `This is a ${project2.kind}, which runs in its game, so a browser can't see it. pch test start tries it on your PC, in Patch Desktop.`
45454
+ `This is a ${project2.kind}, which runs in its game, so a browser can't see it. pch publish tries it on your PC, in Patch Desktop.`
45193
45455
  );
45194
45456
  }
45195
45457
  const launch2 = project2.recipe.launch;
@@ -45292,23 +45554,23 @@ async function check2(ctx, dir, options) {
45292
45554
  },
45293
45555
  () => {
45294
45556
  const on = rendering.software ? "no GPU, in software" : rendering.webgl ? gpuName(rendering.webgl) : "no WebGL";
45295
- out.line(`Checked ${import_picocolors10.default.bold(game.entry)} in ${version2.product} with ${on}.`);
45557
+ out.line(`Checked ${import_picocolors11.default.bold(game.entry)} in ${version2.product} with ${on}.`);
45296
45558
  out.line();
45297
45559
  for (const entry of checks) {
45298
45560
  out.line(` ${SAYS[entry.result](LABELS[entry.result])} ${entry.detail}`);
45299
45561
  }
45300
45562
  if (frames) {
45301
45563
  out.line(
45302
- ` ${import_picocolors10.default.dim("note ")} ${frames.perSecond} frames a second over ${frames.seconds} s, the longest ${frames.longestMs} ms, ${frames.slow} over 100 ms. That's only for you: no frame rate fails anything.`
45564
+ ` ${import_picocolors11.default.dim("note ")} ${frames.perSecond} frames a second over ${frames.seconds} s, the longest ${frames.longestMs} ms, ${frames.slow} over 100 ms. That's only for you: no frame rate fails anything.`
45303
45565
  );
45304
45566
  }
45305
45567
  if (observed.changedWithInput !== null && observed.changedWithInput < 1e-3) {
45306
45568
  out.line(
45307
- ` ${import_picocolors10.default.dim("note ")} The picture didn't change between the first look and the second, with input going in. It may be waiting for something scripted input doesn't do.`
45569
+ ` ${import_picocolors11.default.dim("note ")} The picture didn't change between the first look and the second, with input going in. It may be waiting for something scripted input doesn't do.`
45308
45570
  );
45309
45571
  }
45310
45572
  for (const entry of observed.errors.slice(1, 5)) {
45311
- out.line(import_picocolors10.default.dim(` also: ${entry.message}`));
45573
+ out.line(import_picocolors11.default.dim(` also: ${entry.message}`));
45312
45574
  }
45313
45575
  out.line();
45314
45576
  out.line("Look at these, as a look is what can tell you it's right:");
@@ -45321,7 +45583,7 @@ async function check2(ctx, dir, options) {
45321
45583
  printModels(ctx, models);
45322
45584
  }
45323
45585
  out.line();
45324
- out.line(import_picocolors10.default.dim(ADVICE));
45586
+ out.line(import_picocolors11.default.dim(ADVICE));
45325
45587
  }
45326
45588
  );
45327
45589
  return failed3.length || brokenModels.length ? EXIT.blocked : EXIT.ok;
@@ -45334,8 +45596,8 @@ async function serve(ctx, game) {
45334
45596
  const server = await serveGame(game.root, game.files);
45335
45597
  const url2 = `${server.origin}/${game.entry.split("/").map(encodeURIComponent).join("/")}`;
45336
45598
  ctx.out.result({ url: url2, folder: game.root }, () => {
45337
- ctx.out.line(`Serving ${game.root} as Patch's game host does, at ${import_picocolors10.default.bold(url2)}`);
45338
- ctx.out.line(import_picocolors10.default.dim("Paths have to match exactly here, as they do on Patch. Ctrl+C stops it."));
45599
+ ctx.out.line(`Serving ${game.root} as Patch's game host does, at ${import_picocolors11.default.bold(url2)}`);
45600
+ ctx.out.line(import_picocolors11.default.dim("Paths have to match exactly here, as they do on Patch. Ctrl+C stops it."));
45339
45601
  });
45340
45602
  return EXIT.ok;
45341
45603
  }
@@ -45356,23 +45618,23 @@ function showModels(ctx, models) {
45356
45618
  function printModels(ctx, models) {
45357
45619
  for (const model of models) {
45358
45620
  if (model.error) {
45359
- ctx.out.line(` ${import_picocolors10.default.red("FAIL ")} ${model.file}: ${model.error}`);
45621
+ ctx.out.line(` ${import_picocolors11.default.red("FAIL ")} ${model.file}: ${model.error}`);
45360
45622
  continue;
45361
45623
  }
45362
45624
  const textures = model.textures ?? [];
45363
45625
  const size = textures.reduce((sum, texture) => sum + texture.size, 0);
45364
45626
  ctx.out.line(
45365
- ` ${import_picocolors10.default.green("read ")} ${model.file}: ${formatSize(model.size)}, ${model.triangles} triangles, ${model.meshes} meshes (${model.skinned} skinned), ${model.materials} materials, ${textures.length} textures (${formatSize(size)}), ${model.animations?.length} animations`
45627
+ ` ${import_picocolors11.default.green("read ")} ${model.file}: ${formatSize(model.size)}, ${model.triangles} triangles, ${model.meshes} meshes (${model.skinned} skinned), ${model.materials} materials, ${textures.length} textures (${formatSize(size)}), ${model.animations?.length} animations`
45366
45628
  );
45367
45629
  if (model.needs?.length) {
45368
- ctx.out.line(import_picocolors10.default.dim(` needs a loader with ${model.needs.join(", ")}`));
45630
+ ctx.out.line(import_picocolors11.default.dim(` needs a loader with ${model.needs.join(", ")}`));
45369
45631
  }
45370
45632
  }
45371
- ctx.out.line(import_picocolors10.default.dim(" Read, not drawn: what they look like takes a look in the game."));
45633
+ ctx.out.line(import_picocolors11.default.dim(" Read, not drawn: what they look like takes a look in the game."));
45372
45634
  }
45373
45635
 
45374
45636
  // src/commands/connect.ts
45375
- var import_picocolors14 = __toESM(require_picocolors(), 1);
45637
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
45376
45638
  import { readFile as readFile12, writeFile as writeFile10 } from "node:fs/promises";
45377
45639
  import { join as join15, resolve as resolve11 } from "node:path";
45378
45640
  import { setTimeout as sleep5 } from "node:timers/promises";
@@ -45419,7 +45681,7 @@ function sameName(a, b) {
45419
45681
  }
45420
45682
 
45421
45683
  // src/commands/repos.ts
45422
- var import_picocolors13 = __toESM(require_picocolors(), 1);
45684
+ var import_picocolors14 = __toESM(require_picocolors(), 1);
45423
45685
  import { randomUUID as randomUUID2 } from "node:crypto";
45424
45686
  import { mkdir as mkdir7, readdir as readdir3, readFile as readFile11, rm as rm2, writeFile as writeFile9 } from "node:fs/promises";
45425
45687
  import { join as join14, resolve as resolve10 } from "node:path";
@@ -45489,39 +45751,29 @@ function reviewing(progress) {
45489
45751
  const usual = `usually about ${minutes(progress.usual_minutes)}, ${soFar}`;
45490
45752
  return progress.slow ? `Patch is reviewing it: ${step}. It's taking longer than usual: ${usual}. It finishes by itself.` : `Patch is reviewing it: ${step}. ${capital(usual)}.`;
45491
45753
  }
45492
- function nextStep(release2, status2, test2, cloud) {
45493
- const follow4 = `pch test view ${release2} --wait`;
45494
- const startOne2 = `pch test start ${release2}`;
45754
+ function nextStep(release3, status2, test2, cloud) {
45755
+ const carryOn = `pch publish ${release3}`;
45495
45756
  switch (status2) {
45496
45757
  case "draft":
45497
- return {
45498
- say: "It's a private draft, and it can't be published until it has passed a test on your PC, in Patch Desktop, which someone has to press Start in.",
45499
- command: startOne2
45500
- };
45758
+ return { say: "It's uploaded, and its test comes next.", command: carryOn };
45501
45759
  case "testing":
45502
45760
  switch (test2?.state) {
45503
45761
  case "waiting":
45504
- return {
45505
- say: "Its test is waiting for you: press Start on it in Patch Desktop.",
45506
- command: follow4
45507
- };
45762
+ return { say: "Its test is waiting to start in Patch Desktop.", command: carryOn };
45508
45763
  case "running":
45509
45764
  return {
45510
45765
  say: `Patch Desktop${test2.pc_name ? ` on ${test2.pc_name}` : ""} is playing it.`,
45511
- command: follow4
45766
+ command: carryOn
45512
45767
  };
45513
45768
  case "recorded":
45514
45769
  return {
45515
- say: "It has been played and recorded. Look at the footage, then send your review.",
45516
- command: `pch test footage ${release2}`
45770
+ say: "It's recorded. Look at the footage, then send your review.",
45771
+ command: carryOn
45517
45772
  };
45518
45773
  case "passed":
45519
- return {
45520
- say: "It passed its test, and it's still private. Publish it once you're happy it's ready.",
45521
- command: `pch release submit ${release2}`
45522
- };
45774
+ return { say: "It passed its test.", command: carryOn };
45523
45775
  default:
45524
- return { say: "It needs a test that passes.", command: startOne2 };
45776
+ return { say: "It needs a test that passes.", command: carryOn };
45525
45777
  }
45526
45778
  case "rejected":
45527
45779
  return {
@@ -45530,14 +45782,11 @@ function nextStep(release2, status2, test2, cloud) {
45530
45782
  };
45531
45783
  case "checking":
45532
45784
  case "cloud_review":
45533
- return {
45534
- say: reviewing(cloud) ?? "Patch is reviewing it, which can take several minutes, and nothing tells you when it's done: pch status says.",
45535
- command: `pch status ${release2} --wait 50`
45536
- };
45785
+ return { say: reviewing(cloud) ?? "Patch is reviewing it.", command: carryOn };
45537
45786
  case "in_review":
45538
45787
  return {
45539
45788
  say: "It's waiting for a staff member at Patch, and nothing tells you when that's done: pch status says.",
45540
- command: `pch status ${release2} --wait 50`
45789
+ command: `pch status ${release3} --wait 50`
45541
45790
  };
45542
45791
  case "published":
45543
45792
  return { say: "It's published.", command: null };
@@ -45548,11 +45797,11 @@ function nextStep(release2, status2, test2, cloud) {
45548
45797
  var PLAY_FIRST = "This would be its first public release, and a passed test shows it starts and runs, not that it plays well, so play it yourself first: pch guide says how.";
45549
45798
  async function playFirst(ctx, ref) {
45550
45799
  const { owner, slug: slug2 } = ref;
45551
- const [release2, project2] = await Promise.all([
45800
+ const [release3, project2] = await Promise.all([
45552
45801
  ctx.api.GET("/projects/{owner}/{slug}/releases/{version}", { params: { path: ref } }),
45553
45802
  ctx.api.GET("/projects/{owner}/{slug}", { params: { path: { owner, slug: slug2 } } })
45554
45803
  ]);
45555
- const first = release2.data?.channel === "public" && release2.data.kind !== "block" && project2.data && !project2.data.latest_release;
45804
+ const first = release3.data?.channel === "public" && release3.data.kind !== "block" && project2.data && !project2.data.latest_release;
45556
45805
  return first ? PLAY_FIRST : null;
45557
45806
  }
45558
45807
  function sayNext(next) {
@@ -45597,7 +45846,7 @@ async function versionTaken(ctx, ref, failure2) {
45597
45846
  }
45598
45847
 
45599
45848
  // src/commands/init.ts
45600
- var import_picocolors11 = __toESM(require_picocolors(), 1);
45849
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
45601
45850
  import { readFile as readFile10, stat as stat4, writeFile as writeFile8 } from "node:fs/promises";
45602
45851
  import { basename as basename4, join as join13, resolve as resolve9 } from "node:path";
45603
45852
  init_errors();
@@ -45645,17 +45894,17 @@ async function init(ctx, dir, options) {
45645
45894
  const agentGuide2 = { path: join13(root, guide2.name), written: guide2.written };
45646
45895
  const result = { path: path2, ...contents, on_patch: onPatch2, agent_guide: agentGuide2 };
45647
45896
  ctx.out.result({ ...result, next, look_first: LOOK_FIRST }, () => {
45648
- ctx.out.line(`Wrote ${import_picocolors11.default.bold(path2)} for ${import_picocolors11.default.bold(name)}.`);
45897
+ ctx.out.line(`Wrote ${import_picocolors12.default.bold(path2)} for ${import_picocolors12.default.bold(name)}.`);
45649
45898
  ctx.out.line(
45650
45899
  guide2.written ? `Wrote ${guide2.name}, for coding agents working in this folder.` : `Left ${guide2.name} as it is: it's this folder's guide for agents.`
45651
45900
  );
45652
- ctx.out.line(import_picocolors11.default.dim(BRIEF_WORDS));
45653
- if (files) ctx.out.line(import_picocolors11.default.dim(`The release is what's in ${files}/.`));
45901
+ ctx.out.line(import_picocolors12.default.dim(BRIEF_WORDS));
45902
+ if (files) ctx.out.line(import_picocolors12.default.dim(`The release is what's in ${files}/.`));
45654
45903
  ctx.out.line(LOOK_FIRST);
45655
45904
  ctx.out.line("Then:");
45656
45905
  for (const step of next) ctx.out.line(` ${step}`);
45657
45906
  ctx.out.line(
45658
- import_picocolors11.default.dim(
45907
+ import_picocolors12.default.dim(
45659
45908
  `A browser game whose page is index.html needs no recipe. A test's length and your credits go in one, in patch.json: ${RECIPE_EXAMPLE}. For anything else: pch guide`
45660
45909
  )
45661
45910
  );
@@ -45698,7 +45947,7 @@ async function existsOnPatch(ctx, name) {
45698
45947
  }
45699
45948
 
45700
45949
  // src/commands/scan.ts
45701
- var import_picocolors12 = __toESM(require_picocolors(), 1);
45950
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
45702
45951
  import { setTimeout as sleep3 } from "node:timers/promises";
45703
45952
  init_errors();
45704
45953
  var handle2 = (ref) => `${ref.owner}/${ref.slug}`;
@@ -45761,19 +46010,19 @@ function show(ctx, project2, scan2) {
45761
46010
  out.result({ project: name, ...scan2 }, () => {
45762
46011
  if (scan2.status === "running") {
45763
46012
  out.line(
45764
- `${import_picocolors12.default.bold(name)}: still reading its history. Follow it: pch scan view ${name} --wait`
46013
+ `${import_picocolors13.default.bold(name)}: still reading its history. Follow it: pch scan view ${name} --wait`
45765
46014
  );
45766
46015
  return;
45767
46016
  }
45768
46017
  if (!scan2.up_to_date) {
45769
46018
  out.line(
45770
- import_picocolors12.default.yellow(
45771
- `${import_picocolors12.default.bold(name)}: a push has landed since this scan started, so it says nothing about what the repo holds now. Start a new one: ${start}`
46019
+ import_picocolors13.default.yellow(
46020
+ `${import_picocolors13.default.bold(name)}: a push has landed since this scan started, so it says nothing about what the repo holds now. Start a new one: ${start}`
45772
46021
  )
45773
46022
  );
45774
46023
  }
45775
46024
  if (scan2.status === "failed") {
45776
- out.line(import_picocolors12.default.yellow(`${import_picocolors12.default.bold(name)}: the scan didn't finish, so it can't go open yet.`));
46025
+ out.line(import_picocolors13.default.yellow(`${import_picocolors13.default.bold(name)}: the scan didn't finish, so it can't go open yet.`));
45777
46026
  if (scan2.failure) out.line(plain(scan2.failure));
45778
46027
  out.line(`Start a new one: ${start}`);
45779
46028
  return;
@@ -45790,7 +46039,7 @@ function show(ctx, project2, scan2) {
45790
46039
  function showClean({ out }, name, scan2) {
45791
46040
  const big = scan2.will_become_public?.big_files;
45792
46041
  const where = !big?.count || big.scanned ? "in its whole history" : "in what it read";
45793
- out.line(`${import_picocolors12.default.bold(name)}: ${import_picocolors12.default.green("no secrets found")} ${where}.`);
46042
+ out.line(`${import_picocolors13.default.bold(name)}: ${import_picocolors13.default.green("no secrets found")} ${where}.`);
45794
46043
  if (!scan2.up_to_date) return;
45795
46044
  const open3 = `pch set ${name} --visibility open`;
45796
46045
  const unread = big?.not_read?.count;
@@ -45800,18 +46049,18 @@ function showClean({ out }, name, scan2) {
45800
46049
  }
45801
46050
  function showSecrets({ out }, name, scan2) {
45802
46051
  out.line(
45803
- `${import_picocolors12.default.bold(name)}: ${import_picocolors12.default.red(`${counted2(scan2.secret_count, "secret")} found`)}, which going open would publish.`
46052
+ `${import_picocolors13.default.bold(name)}: ${import_picocolors13.default.red(`${counted2(scan2.secret_count, "secret")} found`)}, which going open would publish.`
45804
46053
  );
45805
46054
  for (const secret of plain(scan2.secrets)) {
45806
46055
  const where = secret.path ? `${secret.path}:${secret.line}` : `its message, line ${secret.line}`;
45807
46056
  const commit = secret.commit ? `${secret.commit.slice(0, 7)} ` : "";
45808
- out.line(` ${import_picocolors12.default.dim(commit)}${where}: ${secret.rule} (${secret.masked})`);
46057
+ out.line(` ${import_picocolors13.default.dim(commit)}${where}: ${secret.rule} (${secret.masked})`);
45809
46058
  }
45810
46059
  if (scan2.secrets.length < scan2.secret_count) {
45811
- out.line(import_picocolors12.default.dim(` The first ${scan2.secrets.length} of them.`));
46060
+ out.line(import_picocolors13.default.dim(` The first ${scan2.secrets.length} of them.`));
45812
46061
  }
45813
46062
  out.line();
45814
- out.line(import_picocolors12.default.bold("How to take them out of its history:"));
46063
+ out.line(import_picocolors13.default.bold("How to take them out of its history:"));
45815
46064
  for (const step of plain(scan2.fix)) {
45816
46065
  out.line(step.step);
45817
46066
  for (const command of step.commands) out.line(` ${command}`);
@@ -45823,7 +46072,7 @@ function showWhatsPublished({ out }, scan2) {
45823
46072
  scan2.will_become_public
45824
46073
  );
45825
46074
  out.line();
45826
- out.line(import_picocolors12.default.bold("Going open publishes:"));
46075
+ out.line(import_picocolors13.default.bold("Going open publishes:"));
45827
46076
  const unread = big_files.not_read?.count;
45828
46077
  const bigFiles = big_files.count ? [
45829
46078
  `${counted2(big_files.count, "big file")} (${formatSize(big_files.total_size)}${unread ? `, ${unread} not read` : ""})`
@@ -45839,14 +46088,14 @@ function showWhatsPublished({ out }, scan2) {
45839
46088
  );
45840
46089
  if (big_files.count) {
45841
46090
  const said2 = big_files.coverage ?? "This answer doesn't say whether they were read for secrets.";
45842
- out.line(unread ? import_picocolors12.default.yellow(said2) : said2);
46091
+ out.line(unread ? import_picocolors13.default.yellow(said2) : said2);
45843
46092
  }
45844
46093
  out.line("Everyone in its history, with the emails git history publishes:");
45845
46094
  for (const person of authors.people.slice(0, PEOPLE_SHOWN)) {
45846
46095
  out.line(` ${person.name} <${person.email}>`);
45847
46096
  }
45848
46097
  if (authors.count > PEOPLE_SHOWN) {
45849
- out.line(import_picocolors12.default.dim(` And ${authors.count - PEOPLE_SHOWN} more: --json has them all.`));
46098
+ out.line(import_picocolors13.default.dim(` And ${authors.count - PEOPLE_SHOWN} more: --json has them all.`));
45850
46099
  }
45851
46100
  }
45852
46101
 
@@ -45937,17 +46186,17 @@ async function newRepo(ctx, ref, dir, options) {
45937
46186
  ...options.visibility === "private" ? { who_can_see: PRIVATE_REPO } : {}
45938
46187
  },
45939
46188
  () => {
45940
- out.line(`Made ${import_picocolors13.default.bold(handle3(project2))} in ${folder}, under the ${licenceName(licence)}.`);
46189
+ out.line(`Made ${import_picocolors14.default.bold(handle3(project2))} in ${folder}, under the ${licenceName(licence)}.`);
45941
46190
  out.line(
45942
46191
  options.visibility === "private" ? `Its repo is private: only your team can find it or read its source. ${PRIVATE_REPO_NOT_LOCKED}` : options.visibility && options.visibility !== "open" ? `Its repo is ${options.visibility}.` : "Its repo opens by itself once that scan is clean: then anyone can read, clone and remix it, with what you push. To keep it to your team: pch set --visibility private."
45943
46192
  );
45944
46193
  if (commitsAs) out.line(saidCommitsAs(commitsAs));
45945
46194
  out.line(`${AGENT_GUIDE_FILE}, in its first commit, tells coding agents how to work here.`);
45946
- out.line(import_picocolors13.default.dim(BRIEF_WORDS));
46195
+ out.line(import_picocolors14.default.dim(BRIEF_WORDS));
45947
46196
  out.line(LOOK_FIRST);
45948
46197
  out.line("Fill in README.md, add your work, then commit and push with plain git.");
45949
46198
  out.line(`To release it: pch validate, then pch publish, in ${folder}.`);
45950
- out.line(import_picocolors13.default.dim(url2));
46199
+ out.line(import_picocolors14.default.dim(url2));
45951
46200
  }
45952
46201
  );
45953
46202
  }
@@ -46018,7 +46267,7 @@ async function remix(ctx, ref, options) {
46018
46267
  ...address ? { address } : {}
46019
46268
  };
46020
46269
  out.result(said2, () => {
46021
- out.line(`Remixed ${import_picocolors13.default.bold(handle3(from))} to ${import_picocolors13.default.bold(name)}.`);
46270
+ out.line(`Remixed ${import_picocolors14.default.bold(handle3(from))} to ${import_picocolors14.default.bold(name)}.`);
46022
46271
  if (address) out.line(address);
46023
46272
  out.line(words2 ?? `Its repo is ${repo.visibility}.`);
46024
46273
  if (ready) out.line(`Clone it with pch clone ${name}`);
@@ -46026,7 +46275,7 @@ async function remix(ctx, ref, options) {
46026
46275
  out.line(
46027
46276
  `A patch.json in it still names ${handle3(from)}: change "project" to ${name} before you publish.`
46028
46277
  );
46029
- out.line(import_picocolors13.default.dim(repo.clone_url));
46278
+ out.line(import_picocolors14.default.dim(repo.clone_url));
46030
46279
  });
46031
46280
  }
46032
46281
  async function yourOrg(ctx, what) {
@@ -46068,23 +46317,23 @@ async function status(ctx, ref, options) {
46068
46317
  const repoAnswer = await api.GET("/projects/{owner}/{slug}/repo", { params: { path: project2 } });
46069
46318
  const repo = repoAnswer.response.ok ? repoAnswer.data : null;
46070
46319
  const latest2 = version2 ?? await latestVersion(ctx, project2, repo);
46071
- const release2 = latest2 ? await releaseStanding(ctx, project2, latest2, options.wait) : null;
46072
- const next = release2 && "next" in release2 ? nextStep(`${handle3(project2)}@${release2.version}`, release2.status, release2.test).command : null;
46073
- const standing2 = repo?.visibility === "open" && release2 && "next" in release2 ? await api.GET("/projects/{owner}/{slug}/releases/{version}", {
46074
- params: { path: { ...project2, version: release2.version } }
46320
+ const release3 = latest2 ? await releaseStanding(ctx, project2, latest2, options.wait) : null;
46321
+ const next = release3 && "next" in release3 ? nextStep(`${handle3(project2)}@${release3.version}`, release3.status, release3.test).command : null;
46322
+ const standing2 = repo?.visibility === "open" && release3 && "next" in release3 ? await api.GET("/projects/{owner}/{slug}/releases/{version}", {
46323
+ params: { path: { ...project2, version: release3.version } }
46075
46324
  }) : null;
46076
46325
  const openCode = standing2?.data ? openCodeWords(handle3(project2), repo, standing2.data) : null;
46077
46326
  const whoCanSee = repo?.visibility === "private" ? { who_can_see: PRIVATE_REPO } : openCode ? { who_can_see: openCode } : {};
46078
- const play2 = release2 && "next" in release2 && release2.status === "testing" && release2.test?.state === "passed" ? await playFirst(ctx, { ...project2, version: release2.version }) : null;
46327
+ const play2 = release3 && "next" in release3 && release3.status === "testing" && release3.test?.state === "passed" ? await playFirst(ctx, { ...project2, version: release3.version }) : null;
46079
46328
  const ask = play2 ? { ask: play2 } : {};
46080
- out.result({ project: handle3(project2), repo, release: release2, next, ...whoCanSee, ...ask }, () => {
46081
- out.line(import_picocolors13.default.bold(handle3(project2)));
46329
+ out.result({ project: handle3(project2), repo, release: release3, next, ...whoCanSee, ...ask }, () => {
46330
+ out.line(import_picocolors14.default.bold(handle3(project2)));
46082
46331
  if (repo) out.line(repoLine(repo));
46083
46332
  if (repo?.visibility === "private") out.line(PRIVATE_REPO);
46084
46333
  if (repo?.home === "github") out.line(homeLine(repo.github));
46085
- if (!release2) out.line("No releases yet. Make one with pch publish.");
46086
- else if ("next" in release2) showReview(ctx, project2, release2);
46087
- else out.line(`Release ${release2.version}: ${import_picocolors13.default.bold(release2.status)}`);
46334
+ if (!release3) out.line("No releases yet. Make one with pch publish.");
46335
+ else if ("next" in release3) showReview(ctx, project2, release3);
46336
+ else out.line(`Release ${release3.version}: ${import_picocolors14.default.bold(release3.status)}`);
46088
46337
  if (openCode) out.line(openCode);
46089
46338
  if (play2) out.line(` ${play2}`);
46090
46339
  if (next) out.line(` Next: ${next}`);
@@ -46163,10 +46412,10 @@ function repoLine(repo) {
46163
46412
  function homeLine(github) {
46164
46413
  if (!github) return "Home: GitHub";
46165
46414
  const seen = github.private ? " (private)" : "";
46166
- return `Home: GitHub, ${plain(github.full_name)}${seen} \xB7 ${import_picocolors13.default.dim(plain(github.html_url))}`;
46415
+ return `Home: GitHub, ${plain(github.full_name)}${seen} \xB7 ${import_picocolors14.default.dim(plain(github.html_url))}`;
46167
46416
  }
46168
46417
  function showReview({ out }, project2, review) {
46169
- out.line(`Release ${review.version}: ${import_picocolors13.default.bold(review.status)}`);
46418
+ out.line(`Release ${review.version}: ${import_picocolors14.default.bold(review.status)}`);
46170
46419
  const ours = ["draft", "testing", "rejected", "checking", "cloud_review", "in_review"].includes(
46171
46420
  review.status
46172
46421
  );
@@ -46185,7 +46434,7 @@ function showReview({ out }, project2, review) {
46185
46434
  for (const finding of run3.findings) {
46186
46435
  const where = finding.path ? ` ${finding.path}:` : "";
46187
46436
  out.line(` ${finding.severity} ${finding.rule_id}${where} ${finding.message}`);
46188
- if (finding.suggestion) out.line(` ${import_picocolors13.default.dim(finding.suggestion)}`);
46437
+ if (finding.suggestion) out.line(` ${import_picocolors14.default.dim(finding.suggestion)}`);
46189
46438
  }
46190
46439
  }
46191
46440
  if (review.test) out.line(` Test: ${review.test.state}`);
@@ -46252,7 +46501,7 @@ async function set2(ctx, ref, options) {
46252
46501
  const whoCanSee = repo.visibility === "private" ? { who_can_see: PRIVATE_REPO } : {};
46253
46502
  const said2 = { project: handle3(project2), ...repo, ...licence ? { licence } : {}, ...whoCanSee };
46254
46503
  out.result(said2, () => {
46255
- out.line(`Updated ${import_picocolors13.default.bold(handle3(project2))}.`);
46504
+ out.line(`Updated ${import_picocolors14.default.bold(handle3(project2))}.`);
46256
46505
  out.line(` Visibility: ${repo.visibility}`);
46257
46506
  if (repo.visibility === "private") out.line(` ${PRIVATE_REPO}`);
46258
46507
  out.line(` Training: ${repo.training_opt_out ? "off" : "on"}`);
@@ -46260,7 +46509,7 @@ async function set2(ctx, ref, options) {
46260
46509
  if (licence) {
46261
46510
  out.line(` Licence: ${licenceName(licence)}`);
46262
46511
  out.line(
46263
- import_picocolors13.default.dim(" Patch lists it now. Commit a LICENSE file to match: that's in your hands.")
46512
+ import_picocolors14.default.dim(" Patch lists it now. Commit a LICENSE file to match: that's in your hands.")
46264
46513
  );
46265
46514
  }
46266
46515
  });
@@ -46328,7 +46577,7 @@ async function connect(ctx, ref, options) {
46328
46577
  out.result(
46329
46578
  { project: handle4(project2), github: shown, already: !posted, patch_json: written, url: url2 },
46330
46579
  () => {
46331
- const name = import_picocolors14.default.bold(handle4(project2));
46580
+ const name = import_picocolors15.default.bold(handle4(project2));
46332
46581
  const repoName = plain(repo.full_name);
46333
46582
  out.line(
46334
46583
  posted ? `Connected ${repoName} on GitHub to ${name}. Its home is GitHub: Patch keeps none of its files until you release one.` : `${repoName} on GitHub is connected to ${name} already.`
@@ -46338,7 +46587,7 @@ async function connect(ctx, ref, options) {
46338
46587
  out.line(
46339
46588
  "Next: pch publish tags a commit on GitHub with your own git login, and releases it."
46340
46589
  );
46341
- out.line(import_picocolors14.default.dim(url2));
46590
+ out.line(import_picocolors15.default.dim(url2));
46342
46591
  }
46343
46592
  );
46344
46593
  }
@@ -46756,7 +47005,7 @@ async function nameInManifest(manifest, project2) {
46756
47005
  }
46757
47006
 
46758
47007
  // src/commands/covers.ts
46759
- var import_picocolors15 = __toESM(require_picocolors(), 1);
47008
+ var import_picocolors16 = __toESM(require_picocolors(), 1);
46760
47009
  import { mkdir as mkdir8, readFile as readFile13, writeFile as writeFile11 } from "node:fs/promises";
46761
47010
  import { basename as basename5, extname as extname3, join as join16, resolve as resolve12 } from "node:path";
46762
47011
  import { setTimeout as sleep7 } from "node:timers/promises";
@@ -46924,7 +47173,7 @@ async function call(ctx, method, path2, body) {
46924
47173
  var coversPath = (project2) => `/projects/${project2.owner}/${project2.slug}/covers`;
46925
47174
  function freeLine(allowance) {
46926
47175
  const left = `${allowance.takes_left} of ${allowance.takes_per_game} free takes and ${allowance.fixes_left} of ${allowance.fixes_per_game} fixes left for this game`;
46927
- return `${import_picocolors15.default.green("Free")}: Patch pays for covers, and they cost you nothing. ${left}.`;
47176
+ return `${import_picocolors16.default.green("Free")}: Patch pays for covers, and they cost you nothing. ${left}.`;
46928
47177
  }
46929
47178
  async function uploadStill(ctx, project2, file2) {
46930
47179
  const absolute = resolve12(file2);
@@ -47003,16 +47252,16 @@ async function save(ctx, job, folder) {
47003
47252
  }
47004
47253
  function show2(ctx, job, saved) {
47005
47254
  ctx.out.result({ ...job, saved }, () => {
47006
- ctx.out.line(`${import_picocolors15.default.bold(job.project)}: ${job.kind === "fix" ? "a fix" : "covers"} (${job.id})`);
47255
+ ctx.out.line(`${import_picocolors16.default.bold(job.project)}: ${job.kind === "fix" ? "a fix" : "covers"} (${job.id})`);
47007
47256
  for (const take of job.takes) {
47008
47257
  const file2 = saved.find((entry) => entry.take === take.id)?.file;
47009
- const state = take.state === "ready" ? import_picocolors15.default.green("ready") : take.state === "failed" ? import_picocolors15.default.red(`failed: ${take.error ?? "no reason given"}`) : import_picocolors15.default.yellow("making");
47010
- const set3 = take.set_as ? import_picocolors15.default.cyan(` set as the ${take.set_as.replace("_", " ")}`) : "";
47258
+ const state = take.state === "ready" ? import_picocolors16.default.green("ready") : take.state === "failed" ? import_picocolors16.default.red(`failed: ${take.error ?? "no reason given"}`) : import_picocolors16.default.yellow("making");
47259
+ const set3 = take.set_as ? import_picocolors16.default.cyan(` set as the ${take.set_as.replace("_", " ")}`) : "";
47011
47260
  ctx.out.line(
47012
- ` ${take.shape.padEnd(4)} ${take.id} ${state}${set3}${file2 ? import_picocolors15.default.dim(` ${file2}`) : ""}`
47261
+ ` ${take.shape.padEnd(4)} ${take.id} ${state}${set3}${file2 ? import_picocolors16.default.dim(` ${file2}`) : ""}`
47013
47262
  );
47014
47263
  }
47015
- if (job.next) ctx.out.line(import_picocolors15.default.dim(job.next));
47264
+ if (job.next) ctx.out.line(import_picocolors16.default.dim(job.next));
47016
47265
  ctx.out.line(freeLine(job.allowance));
47017
47266
  });
47018
47267
  if (job.takes.length && job.takes.every((take) => take.state === "failed")) {
@@ -47087,9 +47336,9 @@ async function coversSet(ctx, ref, take, options) {
47087
47336
  const answer = await call(ctx, "POST", `${coversPath(project2)}/takes/${take}/set`, { as: options.as });
47088
47337
  ctx.out.result(answer, () => {
47089
47338
  ctx.out.line(
47090
- `${import_picocolors15.default.green(`Set the ${answer.media.kind.replace("_", " ")}`)} of ${import_picocolors15.default.bold(ref)}: ${answer.media.status}.`
47339
+ `${import_picocolors16.default.green(`Set the ${answer.media.kind.replace("_", " ")}`)} of ${import_picocolors16.default.bold(ref)}: ${answer.media.status}.`
47091
47340
  );
47092
- ctx.out.line(import_picocolors15.default.dim("It shows on the game's page once it's ready, in a few seconds."));
47341
+ ctx.out.line(import_picocolors16.default.dim("It shows on the game's page once it's ready, in a few seconds."));
47093
47342
  ctx.out.line(freeLine(answer.allowance));
47094
47343
  });
47095
47344
  return EXIT.ok;
@@ -47101,7 +47350,7 @@ function outFolder(out, byDefault) {
47101
47350
  }
47102
47351
 
47103
47352
  // src/commands/feedback.ts
47104
- var import_picocolors16 = __toESM(require_picocolors(), 1);
47353
+ var import_picocolors17 = __toESM(require_picocolors(), 1);
47105
47354
  var FEEDBACK_KINDS = ["failed", "bug", "call_error", "unhelpful", "idea"];
47106
47355
  var FEEDBACK_SEVERITIES = ["blocked", "slowed", "annoyed"];
47107
47356
  async function feedbackSend(ctx, summary, options) {
@@ -47131,7 +47380,7 @@ async function feedbackSend(ctx, summary, options) {
47131
47380
  const shown = { ...sent, next: sent.next.replaceAll("send_feedback", "pch feedback send") };
47132
47381
  ctx.out.result(shown, () => {
47133
47382
  ctx.out.line(
47134
- `${options.addTo ? "Added to your report" : "Told Patch's team"}: ${import_picocolors16.default.bold(plain(sent.summary))}`
47383
+ `${options.addTo ? "Added to your report" : "Told Patch's team"}: ${import_picocolors17.default.bold(plain(sent.summary))}`
47135
47384
  );
47136
47385
  if (sent.known) {
47137
47386
  ctx.out.line(`Patch's team already answered a report like it: ${plain(sent.known.answer)}`);
@@ -47140,12 +47389,12 @@ async function feedbackSend(ctx, summary, options) {
47140
47389
  const flag = sent.follow_up.field.replaceAll("_", "-");
47141
47390
  ctx.out.line(`One more thing would help most: ${plain(sent.follow_up.question)}`);
47142
47391
  ctx.out.line(
47143
- import_picocolors16.default.dim(
47392
+ import_picocolors17.default.dim(
47144
47393
  ` pch feedback send --kind ${sent.kind} --add-to ${sent.id} --${flag} "..." "${plain(sent.summary)}"`
47145
47394
  )
47146
47395
  );
47147
47396
  } else {
47148
- ctx.out.line(import_picocolors16.default.dim(plain(shown.next)));
47397
+ ctx.out.line(import_picocolors17.default.dim(plain(shown.next)));
47149
47398
  }
47150
47399
  });
47151
47400
  }
@@ -47159,24 +47408,24 @@ async function feedbackList(ctx) {
47159
47408
  }
47160
47409
  for (const report of data) {
47161
47410
  ctx.out.line(
47162
- `${import_picocolors16.default.dim(report.created_at.slice(0, 10))} ${statusWord(report.status)} ${plain(report.summary)}`
47411
+ `${import_picocolors17.default.dim(report.created_at.slice(0, 10))} ${statusWord(report.status)} ${plain(report.summary)}`
47163
47412
  );
47164
- if (report.answer) ctx.out.line(` ${import_picocolors16.default.dim("Patch:")} ${plain(report.answer)}`);
47413
+ if (report.answer) ctx.out.line(` ${import_picocolors17.default.dim("Patch:")} ${plain(report.answer)}`);
47165
47414
  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} ...`));
47415
+ ctx.out.line(import_picocolors17.default.dim(` Add to it: pch feedback send --add-to ${report.id} ...`));
47167
47416
  }
47168
47417
  }
47169
47418
  });
47170
47419
  }
47171
47420
  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 ");
47421
+ if (status2 === "fixed") return import_picocolors17.default.green("fixed ");
47422
+ if (status2 === "planned") return import_picocolors17.default.cyan("planned ");
47423
+ if (status2 === "declined") return import_picocolors17.default.dim("declined");
47424
+ return import_picocolors17.default.yellow("open ");
47176
47425
  }
47177
47426
 
47178
47427
  // src/commands/git.ts
47179
- var import_picocolors17 = __toESM(require_picocolors(), 1);
47428
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
47180
47429
  import { stat as stat5 } from "node:fs/promises";
47181
47430
  import { basename as basename6, join as join17, resolve as resolve13 } from "node:path";
47182
47431
 
@@ -47193,7 +47442,7 @@ var PATCH_KEYS = ["PCH_TOKEN", "PCH_GIT_KEY", PUSH_KEY_VARIABLE];
47193
47442
  function run2(dir, args2, signIn = NO_SIGN_IN) {
47194
47443
  const env = { ...process.env };
47195
47444
  if (signIn.own) for (const name of PATCH_KEYS) delete env[name];
47196
- return new Promise((resolve16) => {
47445
+ return new Promise((resolve17) => {
47197
47446
  execFile(
47198
47447
  "git",
47199
47448
  ["-C", dir, ...signIn.args, ...args2],
@@ -47203,7 +47452,7 @@ function run2(dir, args2, signIn = NO_SIGN_IN) {
47203
47452
  },
47204
47453
  (error62, out, err) => {
47205
47454
  const code = error62?.code;
47206
- resolve16({ code: error62 ? typeof code === "number" ? code : -1 : 0, out, err });
47455
+ resolve17({ code: error62 ? typeof code === "number" ? code : -1 : 0, out, err });
47207
47456
  }
47208
47457
  );
47209
47458
  });
@@ -47911,8 +48160,8 @@ async function push(ctx, given) {
47911
48160
  },
47912
48161
  () => {
47913
48162
  const list = sent.map(({ ref, status: status2 }) => `${shortRef(ref)}${status2 === "deleted" ? " (deleted)" : ""}`).join(", ");
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)}.`);
48163
+ if (list) out.line(`${dryRun ? "Would push" : "Pushed"} ${list} to ${import_picocolors18.default.bold(repo)}.`);
48164
+ else if (!quiet) out.line(`Nothing new to push to ${import_picocolors18.default.bold(repo)}.`);
47916
48165
  if (pushed.notice) out.line(pushed.notice);
47917
48166
  }
47918
48167
  );
@@ -47959,10 +48208,10 @@ async function clone2(ctx, ref, dir) {
47959
48208
  brief: brief ? BRIEF_FILE : null
47960
48209
  },
47961
48210
  () => {
47962
- out.line(`Cloned ${import_picocolors17.default.bold(`${project2.owner}/${project2.slug}`)} into ${basename6(into)}`);
48211
+ out.line(`Cloned ${import_picocolors18.default.bold(`${project2.owner}/${project2.slug}`)} into ${basename6(into)}`);
47963
48212
  if (commitsAs) out.line(saidCommitsAs(commitsAs));
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."));
48213
+ out.line(import_picocolors18.default.dim("Commit and push with plain git."));
48214
+ if (!guide2) out.line(import_picocolors18.default.dim("It has no guide for coding agents: pch agent guide adds one."));
47966
48215
  if (brief) {
47967
48216
  out.line(
47968
48217
  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.`
@@ -47973,7 +48222,7 @@ async function clone2(ctx, ref, dir) {
47973
48222
  }
47974
48223
 
47975
48224
  // src/commands/media.ts
47976
- var import_picocolors18 = __toESM(require_picocolors(), 1);
48225
+ var import_picocolors19 = __toESM(require_picocolors(), 1);
47977
48226
  import { readFile as readFile14 } from "node:fs/promises";
47978
48227
  import { basename as basename7, extname as extname4, resolve as resolve14 } from "node:path";
47979
48228
  import { setTimeout as sleep8 } from "node:timers/promises";
@@ -48069,11 +48318,11 @@ function showAdded(ctx, ref, media2) {
48069
48318
  );
48070
48319
  }
48071
48320
  ctx.out.result(media2, () => {
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));
48321
+ ctx.out.line(`${import_picocolors19.default.green(`Added the ${media2.kind}`)} to ${import_picocolors19.default.bold(ref)}: ${media2.status}.`);
48322
+ if (media2.status === "ready") ctx.out.line(import_picocolors19.default.dim(media2.url));
48074
48323
  else {
48075
48324
  ctx.out.line(
48076
- import_picocolors18.default.dim(
48325
+ import_picocolors19.default.dim(
48077
48326
  `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}.`
48078
48327
  )
48079
48328
  );
@@ -48093,7 +48342,7 @@ async function mediaList(ctx, ref) {
48093
48342
  for (const entry of media2.data) {
48094
48343
  const failed3 = entry.status === "failed" && entry.error ? `: ${entry.error}` : "";
48095
48344
  ctx.out.line(
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}`) : ""}`
48345
+ `${entry.id} ${entry.kind.padEnd(10)} ${entry.status.padEnd(10)} ${formatSize(entry.size).padStart(8)}${failed3}${entry.alt ? import_picocolors19.default.dim(` ${entry.alt}`) : ""}`
48097
48346
  );
48098
48347
  }
48099
48348
  });
@@ -48121,11 +48370,466 @@ async function mediaOrder(ctx, ref, ids) {
48121
48370
  }
48122
48371
 
48123
48372
  // src/commands/publish.ts
48124
- var import_picocolors19 = __toESM(require_picocolors(), 1);
48373
+ var import_picocolors22 = __toESM(require_picocolors(), 1);
48125
48374
  var import_semver2 = __toESM(require_semver2(), 1);
48126
48375
  import { open as open2, readFile as readFile15 } from "node:fs/promises";
48127
48376
  init_errors();
48128
48377
  init_guide_words();
48378
+
48379
+ // src/commands/publish-flow.ts
48380
+ var import_picocolors21 = __toESM(require_picocolors(), 1);
48381
+ import { mkdtemp as mkdtemp4 } from "node:fs/promises";
48382
+ import { tmpdir as tmpdir4 } from "node:os";
48383
+ import { join as join19, resolve as resolve16 } from "node:path";
48384
+ init_errors();
48385
+
48386
+ // src/commands/release-test.ts
48387
+ var import_picocolors20 = __toESM(require_picocolors(), 1);
48388
+ import { createHash as createHash3 } from "node:crypto";
48389
+ import { mkdir as mkdir9, mkdtemp as mkdtemp3, writeFile as writeFile12 } from "node:fs/promises";
48390
+ import { tmpdir as tmpdir3 } from "node:os";
48391
+ import { join as join18, resolve as resolve15 } from "node:path";
48392
+ import { setTimeout as sleep9 } from "node:timers/promises";
48393
+ init_errors();
48394
+ var HOLD_SECONDS = 50;
48395
+ var ASK_EVERY_MS2 = 5e3;
48396
+ var OPEN = /* @__PURE__ */ new Set(["waiting", "running", "recorded"]);
48397
+ var GOING = /* @__PURE__ */ new Set(["waiting", "running"]);
48398
+ var REVIEW_ROUTE = "/projects/{owner}/{slug}/releases/{version}/review";
48399
+ var reviewOf = (ctx, release3) => ctx.api.GET(REVIEW_ROUTE, { params: { path: release3 } });
48400
+ async function testStart(ctx, ref, options) {
48401
+ requireToken(ctx.target);
48402
+ const release3 = parseReleaseRef(ref);
48403
+ const wait2 = waitOf(options.wait);
48404
+ const before = await reviewOf(ctx, release3);
48405
+ const open3 = before.data?.test && OPEN.has(before.data.test.state) ? before.data.test : null;
48406
+ const test2 = open3 ?? await startOne(ctx, release3, before.data?.status);
48407
+ return show3(ctx, release3, wait2 === null ? test2 : await follow3(ctx, test2, wait2), {
48408
+ started: !open3,
48409
+ followed: wait2 !== null
48410
+ });
48411
+ }
48412
+ async function startOne(ctx, release3, status2) {
48413
+ const name = releaseName(release3);
48414
+ const answer = await ctx.api.POST("/projects/{owner}/{slug}/releases/{version}/tests", {
48415
+ params: { path: release3 }
48416
+ });
48417
+ if (answer.response.ok) return answer.data;
48418
+ const failure2 = fromResponse(answer.response, answer.error);
48419
+ if (failure2.code === "files_failed") {
48420
+ const { findings } = failure2.details;
48421
+ throw filesFailed(failure2.message, findings);
48422
+ }
48423
+ if (failure2.code === "not_testable") {
48424
+ const next = `pch release submit ${name}`;
48425
+ throw new CliError(
48426
+ failure2.exitCode,
48427
+ failure2.code,
48428
+ `Patch Desktop can't test ${name}, as it's a mod players install some other way, so it skips the test. Send it for review once it's ready: ${next}`,
48429
+ { next }
48430
+ );
48431
+ }
48432
+ if (failure2.code === "not_testing" && status2) {
48433
+ const next = nextStep(name, status2, null);
48434
+ throw new CliError(
48435
+ failure2.exitCode,
48436
+ failure2.code,
48437
+ `${name} is ${status2}, so it can't be tested. ${sayNext(next)}`,
48438
+ { next: next.command }
48439
+ );
48440
+ }
48441
+ throw failure2;
48442
+ }
48443
+ async function testView(ctx, ref, options) {
48444
+ requireToken(ctx.target);
48445
+ const release3 = parseReleaseRef(ref);
48446
+ const wait2 = waitOf(options.wait);
48447
+ const test2 = await newest(ctx, release3);
48448
+ return show3(ctx, release3, wait2 === null ? test2 : await follow3(ctx, test2, wait2), {
48449
+ followed: wait2 !== null
48450
+ });
48451
+ }
48452
+ async function newest(ctx, release3) {
48453
+ const review = await unwrap(reviewOf(ctx, release3));
48454
+ if (review.test) return review.test;
48455
+ const name = releaseName(release3);
48456
+ const next = `pch test start ${name}`;
48457
+ throw new CliError(EXIT.error, "not_found", `${name} has no test yet. Start one: ${next}`, {
48458
+ next
48459
+ });
48460
+ }
48461
+ async function follow3(ctx, test2, seconds2) {
48462
+ const deadline = Date.now() + seconds2 * 1e3;
48463
+ let current = test2;
48464
+ let said2 = "";
48465
+ while (GOING.has(current.state) && Date.now() < deadline) {
48466
+ const now = whatsHappening(current);
48467
+ if (now !== said2) ctx.out.step(now);
48468
+ said2 = now;
48469
+ if (current.needs_person) await sleep9(Math.min(ASK_EVERY_MS2, deadline - Date.now()));
48470
+ const left = Math.ceil((deadline - Date.now()) / 1e3);
48471
+ current = await unwrap(
48472
+ ctx.api.GET("/tests/{id}", {
48473
+ params: {
48474
+ path: { id: current.id },
48475
+ query: { wait: Math.max(0, Math.min(HOLD_SECONDS, left)) }
48476
+ }
48477
+ })
48478
+ );
48479
+ }
48480
+ return current;
48481
+ }
48482
+ function whatsHappening(test2) {
48483
+ if (test2.state === "running") {
48484
+ const where = test2.pc_name ? ` on ${plain(test2.pc_name)}` : "";
48485
+ const phase = test2.progress?.phase;
48486
+ return phase && phase !== "playing" ? `Patch Desktop${where} is ${plain(phase)} it` : `Patch Desktop${where} is playing it`;
48487
+ }
48488
+ if (test2.needs_person) return plain(test2.needs_person.ask);
48489
+ return "Waiting for you to press Start in Patch Desktop";
48490
+ }
48491
+ async function show3(ctx, release3, test2, { followed, ...extra } = {}) {
48492
+ const { out } = ctx;
48493
+ const name = releaseName(release3);
48494
+ const next = nextStep(name, test2.state === "failed" ? "rejected" : "testing", test2);
48495
+ const command = next.command;
48496
+ const play2 = test2.state === "passed" ? await playFirst(ctx, release3) : null;
48497
+ const ask = test2.state === "waiting" ? askThePerson(test2) : play2;
48498
+ const noteAsk = test2.note_ask?.replaceAll("suggest_note", "pch note suggest");
48499
+ const feedbackAsk = test2.feedback_ask ? {
48500
+ ...test2.feedback_ask,
48501
+ ask: test2.feedback_ask.ask.replaceAll("send_feedback", "pch feedback send")
48502
+ } : void 0;
48503
+ const shown = {
48504
+ ...test2,
48505
+ ...extra,
48506
+ next: command,
48507
+ ...noteAsk ? { note_ask: noteAsk } : {},
48508
+ ...feedbackAsk ? { feedback_ask: feedbackAsk } : {}
48509
+ };
48510
+ out.result({ ...shown, ...ask ? { ask } : {} }, () => {
48511
+ const bold = import_picocolors20.default.bold(name);
48512
+ if (test2.state === "waiting") {
48513
+ out.line(
48514
+ !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.`
48515
+ );
48516
+ if (test2.needs_person) {
48517
+ out.line(plain(test2.needs_person.why));
48518
+ out.line(plain(test2.needs_person.ask));
48519
+ }
48520
+ } else if (test2.state === "running") {
48521
+ out.line(`${bold}: ${whatsHappening(test2)}.`);
48522
+ } else if (test2.state === "recorded") {
48523
+ out.line(
48524
+ `${bold}: it has been played and recorded. Look at the footage, then send your review.`
48525
+ );
48526
+ } else if (test2.state === "passed") {
48527
+ out.line(`${bold}: ${import_picocolors20.default.green("its test passed.")} It's still private.`);
48528
+ if (test2.review) out.line(plain(test2.review.summary));
48529
+ if (play2) out.line(play2);
48530
+ out.line(
48531
+ "Publish it once you're happy it's ready: that's your yes, and nothing does it for you."
48532
+ );
48533
+ } else if (test2.state === "failed") {
48534
+ out.line(`${bold}: ${import_picocolors20.default.red("its test failed")}, so this version can't be published.`);
48535
+ showWhatFailed(ctx, test2);
48536
+ out.line(`pch status ${name} says how to fix it.`);
48537
+ out.line("Then move the version on in patch.json and publish that.");
48538
+ } else if (test2.state === "couldnt_run") {
48539
+ out.line(`${bold}: ${import_picocolors20.default.yellow("its test couldn't run.")}`);
48540
+ if (test2.outcome) out.line(plain(test2.outcome.reason));
48541
+ } else {
48542
+ out.line(`${bold}: its test was cancelled.`);
48543
+ }
48544
+ if (noteAsk) out.line(plain(noteAsk));
48545
+ if (feedbackAsk) out.line(plain(feedbackAsk.ask));
48546
+ if (followed && GOING.has(test2.state)) {
48547
+ out.line("It isn't over, and nothing's wrong: that's as long as --wait follows it here.");
48548
+ }
48549
+ if (command) out.line(`Next: ${command}`);
48550
+ if (OPEN.has(test2.state) && !extra.started) {
48551
+ out.line(import_picocolors20.default.dim(`To start over: pch test cancel ${name}, then pch test start ${name}`));
48552
+ }
48553
+ });
48554
+ if (test2.state === "failed") return EXIT.blocked;
48555
+ if (test2.state === "couldnt_run" || test2.state === "cancelled") return EXIT.error;
48556
+ return EXIT.ok;
48557
+ }
48558
+ function askThePerson(test2) {
48559
+ return test2.needs_person?.ask ?? null;
48560
+ }
48561
+ function showWhatFailed({ out }, test2) {
48562
+ for (const run3 of test2.results) {
48563
+ for (const check3 of run3.checks) {
48564
+ if (check3.result !== "fail") continue;
48565
+ const detail = check3.detail ? ` (${plain(check3.detail)})` : "";
48566
+ out.line(` Run ${run3.run}: ${plain(check3.name)}${detail}`);
48567
+ }
48568
+ }
48569
+ if (test2.review) out.line(` Its review: ${plain(test2.review.summary)}`);
48570
+ }
48571
+ async function testFootage(ctx, ref, options) {
48572
+ requireToken(ctx.target);
48573
+ const { out } = ctx;
48574
+ const release3 = parseReleaseRef(ref);
48575
+ const name = releaseName(release3);
48576
+ const run3 = options.run === void 0 ? void 0 : positive(options.run, "--run");
48577
+ const test2 = await newest(ctx, release3);
48578
+ const { data } = await unwrap(
48579
+ ctx.api.GET("/tests/{id}/footage", { params: { path: { id: test2.id }, query: { run: run3 } } })
48580
+ );
48581
+ const sheets = data.length ? await saveSheets(
48582
+ data,
48583
+ options.out ? resolve15(options.out) : await mkdtemp3(join18(tmpdir3(), "pch-footage-"))
48584
+ ) : [];
48585
+ const next = data.length ? reviewCommand(name) : nextStep(name, "testing", test2).command;
48586
+ out.result({ test: test2.id, state: test2.state, data: sheets, next }, () => {
48587
+ if (!sheets.length) {
48588
+ out.line(`${import_picocolors20.default.bold(name)} has no footage yet: its test is ${test2.state.replace("_", " ")}.`);
48589
+ for (const result of test2.results) {
48590
+ if (result.footage_error) out.line(plain(result.footage_error));
48591
+ }
48592
+ }
48593
+ for (const sheet of sheets) {
48594
+ out.line(`Run ${sheet.run}, ${sheet.from} to ${sheet.to}: ${sheet.path}`);
48595
+ }
48596
+ if (next) {
48597
+ if (sheets.length) out.line("Look at every one, then send your review, with what you saw:");
48598
+ out.line(`Next: ${next}`);
48599
+ }
48600
+ });
48601
+ }
48602
+ var reviewCommand = (name) => `pch test review ${name} --verdict <pass|fail> --plays <yes|no> --harmful <yes|no> --summary "<what you saw>" --moment "<run> <mm:ss> <what you saw>" --moment "<run> <mm:ss> <what you saw>"`;
48603
+ async function saveSheets(sheets, folder) {
48604
+ await mkdir9(folder, { recursive: true });
48605
+ const saved = [];
48606
+ for (const sheet of sheets) {
48607
+ const answer = await fetch(sheet.url).catch(() => null);
48608
+ const bytes = answer?.ok ? Buffer.from(await answer.arrayBuffer()) : null;
48609
+ if (!bytes || createHash3("sha256").update(bytes).digest("hex") !== sheet.sha256) {
48610
+ throw new CliError(
48611
+ EXIT.error,
48612
+ "download_failed",
48613
+ `Run ${sheet.run}'s sheet ${sheet.index} didn't download intact. Try again.`
48614
+ );
48615
+ }
48616
+ const path2 = join18(folder, `run-${sheet.run}-sheet-${sheet.index}.jpg`);
48617
+ await writeFile12(path2, bytes);
48618
+ const { url: _url2, ...kept } = sheet;
48619
+ saved.push({ ...kept, path: path2 });
48620
+ }
48621
+ return saved;
48622
+ }
48623
+ async function testReview(ctx, ref, options) {
48624
+ requireToken(ctx.target);
48625
+ const release3 = parseReleaseRef(ref);
48626
+ const moments = options.moment.map(parseMoment);
48627
+ const test2 = await newest(ctx, release3);
48628
+ const answer = await ctx.api.POST("/tests/{id}/review", {
48629
+ params: { path: { id: test2.id } },
48630
+ body: {
48631
+ verdict: options.verdict,
48632
+ plays: options.plays === "yes",
48633
+ harmful: options.harmful === "yes",
48634
+ summary: options.summary,
48635
+ moments
48636
+ }
48637
+ });
48638
+ if (answer.response.ok) return show3(ctx, release3, answer.data);
48639
+ throw withNext(fromResponse(answer.response, answer.error), release3, test2);
48640
+ }
48641
+ function parseMoment(text) {
48642
+ const match = /^(\d+)\s+(\d{1,3}:\d{2})\s+(\S.*)$/s.exec(text.trim());
48643
+ if (!match) {
48644
+ throw new CliError(
48645
+ EXIT.usage,
48646
+ "bad_moment",
48647
+ `"${text}" isn't a moment. Write its run, the time on a frame and what you saw, such as "1 00:04 the title screen draws".`
48648
+ );
48649
+ }
48650
+ return { run: Number(match[1]), at: match[2], saw: match[3] };
48651
+ }
48652
+ async function testCancel(ctx, ref) {
48653
+ requireToken(ctx.target);
48654
+ const release3 = parseReleaseRef(ref);
48655
+ const name = releaseName(release3);
48656
+ const test2 = await newest(ctx, release3);
48657
+ const answer = await ctx.api.POST("/tests/{id}/cancel", { params: { path: { id: test2.id } } });
48658
+ if (!answer.response.ok) {
48659
+ throw withNext(fromResponse(answer.response, answer.error), release3, test2);
48660
+ }
48661
+ const next = `pch test start ${name}`;
48662
+ ctx.out.result({ ...answer.data, next }, () => {
48663
+ ctx.out.line(`Cancelled ${import_picocolors20.default.bold(name)}'s test. Start another: ${next}`);
48664
+ });
48665
+ }
48666
+ function withNext(failure2, release3, test2) {
48667
+ if (failure2.code !== "test_state") return failure2;
48668
+ const next = nextStep(releaseName(release3), "testing", test2);
48669
+ return new CliError(failure2.exitCode, failure2.code, `${failure2.message}. ${sayNext(next)}`, {
48670
+ next: next.command
48671
+ });
48672
+ }
48673
+ function positive(value, flag) {
48674
+ if (/^[1-9]\d*$/.test(value)) return Number(value);
48675
+ throw new CliError(EXIT.usage, "bad_usage", `${flag} takes a whole number, not "${value}".`);
48676
+ }
48677
+ async function testAutostart(ctx, state) {
48678
+ requireToken(ctx.target);
48679
+ if (state !== "on" && state !== "off") {
48680
+ throw new CliError(EXIT.usage, "bad_value", `Say on or off, not "${state}"`);
48681
+ }
48682
+ const setting = await unwrap(
48683
+ ctx.api.PUT("/me/test-settings", { body: { starts_own_tests: state === "on", by: "agent" } })
48684
+ );
48685
+ ctx.out.result(setting, () => {
48686
+ ctx.out.line(
48687
+ setting.starts_own_tests ? "Your tests of browser games and blocks now start in Patch Desktop by themselves, with a notice each time that lets the person stop them. Patch Desktop tells them you turned it on. A mod's test still waits for Start." : "Your tests wait for the person to press Start in Patch Desktop again."
48688
+ );
48689
+ });
48690
+ }
48691
+
48692
+ // src/commands/publish-flow.ts
48693
+ var ENDS = /* @__PURE__ */ new Set(["your_review", "ready", "live", "stopped", "uploading"]);
48694
+ var PERSON_EVERY_MS = 5e3;
48695
+ function followFor(wait2) {
48696
+ if (wait2 === void 0) {
48697
+ return process.stdout.isTTY ? Number.POSITIVE_INFINITY : UNATTENDED_WAIT_SECONDS;
48698
+ }
48699
+ if (/^\d+$/.test(wait2)) return Number(wait2);
48700
+ throw new CliError(EXIT.usage, "bad_wait", `--wait takes a number of seconds, not "${wait2}".`);
48701
+ }
48702
+ function reviewOf2(flags) {
48703
+ if (flags.review === void 0) return void 0;
48704
+ if (flags.review !== "pass" && flags.review !== "fail") {
48705
+ throw new CliError(EXIT.usage, "bad_value", `--review is pass or fail, not "${flags.review}".`);
48706
+ }
48707
+ const moments = (flags.moment ?? []).map(parseMoment);
48708
+ if (!flags.summary?.trim() || moments.length < 2) {
48709
+ throw new CliError(
48710
+ EXIT.usage,
48711
+ "review_incomplete",
48712
+ `A review needs --summary and at least two --moment, each a run, the time on a frame and what you saw: ${reviewCommand2("<owner/slug@version>")}`
48713
+ );
48714
+ }
48715
+ return {
48716
+ verdict: flags.review,
48717
+ plays: flags.review === "pass",
48718
+ harmful: Boolean(flags.harmful),
48719
+ summary: flags.summary,
48720
+ moments
48721
+ };
48722
+ }
48723
+ var reviewCommand2 = (name) => `pch publish ${name} --review <pass|fail> --summary "<what you saw>" --moment "<run> <mm:ss> <what you saw>" --moment "<run> <mm:ss> <what you saw>"`;
48724
+ function lineOf(step) {
48725
+ if (step.stage === "testing" && step.needs_person) return "Testing on your PC: waiting for you";
48726
+ return step.line;
48727
+ }
48728
+ var sleep10 = (ms) => new Promise((done) => setTimeout(done, ms));
48729
+ async function followRelease(ctx, ref, options = {}) {
48730
+ requireToken(ctx.target);
48731
+ const { out, api } = ctx;
48732
+ const review = reviewOf2(options);
48733
+ const wait2 = options.wait ?? followFor(void 0);
48734
+ const deadline = (options.since ?? Date.now()) + wait2 * 1e3;
48735
+ let body = {
48736
+ ...review ? { review } : {},
48737
+ ...options.testAgain ? { test_again: true } : {}
48738
+ };
48739
+ let said2 = "";
48740
+ let asked = "";
48741
+ let step;
48742
+ for (; ; ) {
48743
+ const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
48744
+ const answer = await api.POST("/projects/{owner}/{slug}/releases/{version}/publish", {
48745
+ params: { path: ref, query: { wait: Math.min(50, left) } },
48746
+ body
48747
+ });
48748
+ if (!answer.response.ok) throw fromResponse(answer.response, answer.error);
48749
+ step = answer.data;
48750
+ body = void 0;
48751
+ const stops = ENDS.has(step.stage) || step.status === "in_review";
48752
+ const line = lineOf(step);
48753
+ if (line !== said2 && !stops) out.progress(line, { stage: step.stage });
48754
+ said2 = line;
48755
+ if (step.needs_person && step.needs_person.ask !== asked) {
48756
+ asked = step.needs_person.ask;
48757
+ out.step(step.needs_person.ask);
48758
+ }
48759
+ if (stops || Date.now() >= deadline) break;
48760
+ if (step.needs_person) await sleep10(PERSON_EVERY_MS);
48761
+ }
48762
+ return finish(ctx, ref, step, options);
48763
+ }
48764
+ async function finish(ctx, ref, step, options) {
48765
+ const { out } = ctx;
48766
+ const name = releaseName(ref);
48767
+ const url2 = step.urls.play ?? step.urls.page;
48768
+ const ended = ENDS.has(step.stage) && step.stage !== "stopped" && step.stage !== "uploading";
48769
+ const { extra = {}, notes = [] } = (ended || step.stage === "testing" || step.stage === "in_review" ? await options.after?.(step) : void 0) ?? {};
48770
+ const base = { release: { ...ref }, stage: step.stage, line: step.line, url: url2, ...extra };
48771
+ const say = (...lines) => {
48772
+ for (const line of [...notes, ...lines]) out.line(line);
48773
+ };
48774
+ switch (step.stage) {
48775
+ case "live":
48776
+ out.result({ ...base, next: null }, () => say(import_picocolors21.default.green(`Live: ${url2}`)));
48777
+ return;
48778
+ case "ready":
48779
+ out.result(
48780
+ { ...base, next: null },
48781
+ () => say("Passed. Press publish in the chat to send it.")
48782
+ );
48783
+ return;
48784
+ case "your_review": {
48785
+ const sheets = await saveSheets(
48786
+ step.footage ?? [],
48787
+ options.out ? resolve16(options.out) : await mkdtemp4(join19(tmpdir4(), "pch-footage-"))
48788
+ );
48789
+ const next = reviewCommand2(name);
48790
+ out.result({ ...base, test: step.test_id, footage: sheets, next }, () => {
48791
+ say(`Recorded. Look at every sheet, the bursts too:`);
48792
+ for (const sheet of sheets) out.line(` ${sheet.path} (${sheet.from} to ${sheet.to})`);
48793
+ out.line(`Then send your review (add --harmful if it does anything harmful):`);
48794
+ out.line(`Next: ${next}`);
48795
+ });
48796
+ return;
48797
+ }
48798
+ case "stopped":
48799
+ case "uploading": {
48800
+ const code = step.code ?? "stopped";
48801
+ const exit = code === "test_failed" || code === "rejected" || code === "files_failed" ? EXIT.blocked : EXIT.error;
48802
+ throw new CliError(exit, code, `${name}: ${step.next}`, {
48803
+ findings: step.findings,
48804
+ issues: step.findings.map((finding) => ({
48805
+ path: finding.path ?? "",
48806
+ message: `${finding.rule_id}: ${finding.message}${finding.suggestion ? `. ${finding.suggestion}` : ""}`
48807
+ })),
48808
+ next: code === "couldnt_run" ? `pch publish ${name} --test-again` : "pch publish"
48809
+ });
48810
+ }
48811
+ default: {
48812
+ const staff = step.status === "in_review";
48813
+ const next = staff ? `pch status ${name}` : `pch publish ${name}`;
48814
+ out.result({ ...base, needs_person: step.needs_person, next }, () => {
48815
+ if (staff) say("Waiting for a staff member at Patch.");
48816
+ else say(`Still ${lineOf(step).replace(/^./, (first) => first.toLowerCase())}.`);
48817
+ out.line(`Next: ${next}`);
48818
+ });
48819
+ }
48820
+ }
48821
+ }
48822
+ async function firstTime(ctx, project2, what) {
48823
+ const key = `${ctx.target.origin} ${project2}`;
48824
+ ctx.config.told ??= {};
48825
+ const told = ctx.config.told[key] ?? [];
48826
+ if (told.includes(what)) return false;
48827
+ ctx.config.told[key] = [...told, what];
48828
+ await writeConfig(ctx.config).catch(() => void 0);
48829
+ return true;
48830
+ }
48831
+
48832
+ // src/commands/publish.ts
48129
48833
  var MB = 1024 * 1024;
48130
48834
  var FILES_AT_ONCE = 16;
48131
48835
  var BIG_FILE = 16 * MB;
@@ -48141,8 +48845,23 @@ function forkNamesParent(named, clone3) {
48141
48845
  { named, clone: clone3 }
48142
48846
  );
48143
48847
  }
48144
- async function publish(ctx, dir, options) {
48848
+ async function publish(ctx, target, options) {
48145
48849
  requireToken(ctx.target);
48850
+ const started = Date.now();
48851
+ const follow4 = {
48852
+ ...options,
48853
+ wait: followFor(options.wait),
48854
+ since: started
48855
+ };
48856
+ if (target.includes("@")) return followRelease(ctx, parseReleaseRef(target), follow4);
48857
+ if (options.review !== void 0 || options.testAgain) {
48858
+ throw new CliError(
48859
+ EXIT.usage,
48860
+ "release_required",
48861
+ "Say which release: pch publish <owner/slug@version> --review \u2026, as pch publish printed it."
48862
+ );
48863
+ }
48864
+ const dir = target;
48146
48865
  const { out, api } = ctx;
48147
48866
  const project2 = await loadProject(dir);
48148
48867
  if (project2.issues.length) {
@@ -48201,53 +48920,41 @@ async function publish(ctx, dir, options) {
48201
48920
  } else {
48202
48921
  out.step("Patch already has every file");
48203
48922
  }
48204
- out.step("Submitting");
48205
- const name = releaseName({ ...ref, version: version2 });
48206
- const submitted = await submit(ctx, { ...ref, version: version2 }).catch(draftUntilTested);
48207
- const release2 = await unwrap(
48208
- api.GET("/projects/{owner}/{slug}/releases/{version}", { params: { path: path2 } })
48209
- );
48210
- const url2 = projectUrl(ctx.target.origin, ref);
48211
48923
  const branch = await source?.bringBranch();
48212
- const branchSaid = branch ? { branch } : {};
48213
- const next = nextStep(name, submitted?.status ?? "draft", null).command;
48214
- const openCode = await openCodeOf(ctx, ref, release2);
48215
- const whoCanSee = whoCanSeeIt(
48216
- // A draft is private whichever channel it's for, as the words for a person say.
48217
- !submitted || release2.channel === "private" ? privateWords(release2.kind, !submitted) : null,
48218
- openCode
48219
- );
48220
- const result = {
48221
- release: release2,
48222
- uploaded: draft.uploads.length,
48223
- url: url2,
48224
- next,
48225
- ...branchSaid,
48226
- ...whoCanSee
48227
- };
48228
- out.result(result, () => {
48229
- if (!submitted) {
48230
- out.line(
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.`
48232
- );
48233
- out.line(privateWords(release2.kind, true));
48234
- if (openCode) out.line(openCode);
48235
- if (source) out.line(fromCommit(source));
48236
- if (branch?.say) out.line(branch.say);
48237
- out.line(`Next: ${next}`);
48238
- return;
48239
- }
48240
- showSubmitted(ctx, { ...ref, version: version2 }, submitted.status, release2);
48241
- if (openCode) out.line(openCode);
48242
- if (source) out.line(fromCommit(source));
48243
- if (branch?.say) out.line(branch.say);
48244
- for (const entry of release2.files) {
48245
- out.line(
48246
- ` ${entry.path.padEnd(40)} ${formatSize(entry.size).padStart(8)} sha256 ${entry.sha256}`
48247
- );
48924
+ await followRelease(
48925
+ ctx,
48926
+ { ...ref, version: version2 },
48927
+ {
48928
+ ...follow4,
48929
+ // Who can see it, as it is once it stops. It's the same each time, so it's said once a
48930
+ // project, and --json always has it.
48931
+ after: async () => {
48932
+ const release3 = await unwrap(
48933
+ api.GET("/projects/{owner}/{slug}/releases/{version}", { params: { path: path2 } })
48934
+ );
48935
+ const openCode = await openCodeOf(ctx, ref, release3);
48936
+ const privately = release3.channel === "private" ? privateWords(release3.kind, release3.status !== "published") : null;
48937
+ const notes = [source ? fromCommit(source).trim() : null, branch?.say ?? null].filter(
48938
+ (note2) => Boolean(note2)
48939
+ );
48940
+ for (const [what, words2] of [
48941
+ ["private", privately],
48942
+ ["open_code", openCode]
48943
+ ]) {
48944
+ if (words2 && await firstTime(ctx, file2.project, what)) notes.push(words2);
48945
+ }
48946
+ return {
48947
+ extra: {
48948
+ release: release3,
48949
+ uploaded: draft.uploads.length,
48950
+ ...branch ? { branch } : {},
48951
+ ...whoCanSeeIt(privately, openCode)
48952
+ },
48953
+ notes
48954
+ };
48955
+ }
48248
48956
  }
48249
- out.line(import_picocolors19.default.dim(url2));
48250
- });
48957
+ );
48251
48958
  }
48252
48959
  function fromCommit(source) {
48253
48960
  const on = source.on === "GitHub" ? " on GitHub" : "";
@@ -48255,36 +48962,36 @@ function fromCommit(source) {
48255
48962
  }
48256
48963
  async function releaseSubmit(ctx, ref) {
48257
48964
  requireToken(ctx.target);
48258
- const release2 = parseReleaseRef(ref);
48259
- const submitted = await submit(ctx, release2);
48260
- const next = nextStep(releaseName(release2), submitted.status, null).command;
48261
- const openCode = await openCodeOf(ctx, release2, submitted);
48965
+ const release3 = parseReleaseRef(ref);
48966
+ const submitted = await submit(ctx, release3);
48967
+ const next = nextStep(releaseName(release3), submitted.status, null).command;
48968
+ const openCode = await openCodeOf(ctx, release3, submitted);
48262
48969
  const whoCanSee = whoCanSeeIt(
48263
48970
  submitted.channel === "private" && submitted.status === "published" ? privateWords(submitted.kind, false) : null,
48264
48971
  openCode
48265
48972
  );
48266
48973
  ctx.out.result({ ...submitted, next, ...whoCanSee }, () => {
48267
- showSubmitted(ctx, release2, submitted.status, submitted);
48974
+ showSubmitted(ctx, release3, submitted.status, submitted);
48268
48975
  if (openCode) ctx.out.line(openCode);
48269
- ctx.out.line(import_picocolors19.default.dim(projectUrl(ctx.target.origin, release2)));
48976
+ ctx.out.line(import_picocolors22.default.dim(projectUrl(ctx.target.origin, release3)));
48270
48977
  });
48271
48978
  }
48272
48979
  async function releasePromote(ctx, ref, options) {
48273
48980
  requireToken(ctx.target);
48274
48981
  const from = parseReleaseRef(ref);
48275
48982
  const project2 = { owner: from.owner, slug: from.slug };
48276
- const release2 = await unwrap(
48983
+ const release3 = await unwrap(
48277
48984
  ctx.api.GET("/projects/{owner}/{slug}/releases/{version}", { params: { path: from } })
48278
48985
  );
48279
- if (release2.status !== "published") {
48280
- const next = nextStep(ref, release2.status, null);
48986
+ if (release3.status !== "published") {
48987
+ const next = nextStep(ref, release3.status, null);
48281
48988
  throw new CliError(
48282
48989
  EXIT.conflict,
48283
48990
  "not_published",
48284
- `${ref} is ${release2.status}, and only a published release can move to another channel. ${next.say}${next.command ? ` Next: ${next.command}` : ""} To release it elsewhere instead, set "channel" in patch.json, move the version on, and run pch publish.`
48991
+ `${ref} is ${release3.status}, and only a published release can move to another channel. ${next.say}${next.command ? ` Next: ${next.command}` : ""} To release it elsewhere instead, set "channel" in patch.json, move the version on, and run pch publish.`
48285
48992
  );
48286
48993
  }
48287
- if (release2.channel === options.channel) {
48994
+ if (release3.channel === options.channel) {
48288
48995
  throw new CliError(
48289
48996
  EXIT.conflict,
48290
48997
  "already_on_channel",
@@ -48298,10 +49005,10 @@ async function releasePromote(ctx, ref, options) {
48298
49005
  body: {
48299
49006
  version: version2,
48300
49007
  channel: options.channel,
48301
- changelog: options.changelog ?? release2.changelog ?? void 0,
48302
- ...release2.recipe ? { recipe: release2.recipe } : {},
48303
- ...release2.commit ? { commit: release2.commit } : {},
48304
- files: release2.files.map(({ path: path2, sha256, size }) => ({ path: path2, sha256, size }))
49008
+ changelog: options.changelog ?? release3.changelog ?? void 0,
49009
+ ...release3.recipe ? { recipe: release3.recipe } : {},
49010
+ ...release3.commit ? { commit: release3.commit } : {},
49011
+ files: release3.files.map(({ path: path2, sha256, size }) => ({ path: path2, sha256, size }))
48305
49012
  }
48306
49013
  })
48307
49014
  );
@@ -48320,7 +49027,7 @@ async function releasePromote(ctx, ref, options) {
48320
49027
  const next = nextStep(name, "draft", null).command;
48321
49028
  ctx.out.result({ from: ref, release: { version: version2, status: "draft" }, url: url2, next }, () => {
48322
49029
  ctx.out.line(
48323
- `Made ${import_picocolors19.default.bold(name)} from ${ref}, as a draft for ${options.channel}. It needs a passed test of its own.`
49030
+ `Made ${import_picocolors22.default.bold(name)} from ${ref}, as a draft for ${options.channel}. It needs a passed test of its own.`
48324
49031
  );
48325
49032
  ctx.out.line(`Next: ${next}`);
48326
49033
  });
@@ -48328,7 +49035,7 @@ async function releasePromote(ctx, ref, options) {
48328
49035
  }
48329
49036
  ctx.out.result({ from: ref, release: submitted, url: url2 }, () => {
48330
49037
  showSubmitted(ctx, to, submitted.status, submitted);
48331
- ctx.out.line(import_picocolors19.default.dim(url2));
49038
+ ctx.out.line(import_picocolors22.default.dim(url2));
48332
49039
  });
48333
49040
  }
48334
49041
  function draftUntilTested(error62) {
@@ -48344,7 +49051,7 @@ async function nextVersion(ctx, project2) {
48344
49051
  params: { path: project2, query: { limit: 100, cursor } }
48345
49052
  })
48346
49053
  );
48347
- versions.push(...page.data.map((release2) => release2.version));
49054
+ versions.push(...page.data.map((release3) => release3.version));
48348
49055
  cursor = page.next_cursor ?? void 0;
48349
49056
  } while (cursor);
48350
49057
  const newest2 = import_semver2.default.rsort(versions.filter((version2) => import_semver2.default.valid(version2)))[0];
@@ -48354,9 +49061,9 @@ function privateWords(kind, draft) {
48354
49061
  if (!onGameHost(kind)) return "Only your team can find it.";
48355
49062
  return draft ? PRIVATE_DRAFT : PRIVATE_GAME;
48356
49063
  }
48357
- async function openCodeOf(ctx, { owner, slug: slug2 }, release2) {
49064
+ async function openCodeOf(ctx, { owner, slug: slug2 }, release3) {
48358
49065
  const repo = await ctx.api.GET("/projects/{owner}/{slug}/repo", { params: { path: { owner, slug: slug2 } } }).catch(() => null);
48359
- return openCodeWords(`${owner}/${slug2}`, repo?.data, release2);
49066
+ return openCodeWords(`${owner}/${slug2}`, repo?.data, release3);
48360
49067
  }
48361
49068
  function whoCanSeeIt(...words2) {
48362
49069
  const said2 = words2.filter(Boolean).join(" ");
@@ -48365,7 +49072,7 @@ function whoCanSeeIt(...words2) {
48365
49072
  function showSubmitted(ctx, ref, status2, { channel, kind }) {
48366
49073
  const name = releaseName(ref);
48367
49074
  if (status2 === "published") {
48368
- ctx.out.line(import_picocolors19.default.green(`Published ${name} to ${channel}`));
49075
+ ctx.out.line(import_picocolors22.default.green(`Published ${name} to ${channel}`));
48369
49076
  if (channel === "private") {
48370
49077
  ctx.out.line(privateWords(kind, false));
48371
49078
  ctx.out.line(`pch status ${name} says whether your team can play it yet, and where.`);
@@ -48518,314 +49225,8 @@ async function uploadFailed(file2, upload, response) {
48518
49225
  );
48519
49226
  }
48520
49227
 
48521
- // src/commands/release-test.ts
48522
- var import_picocolors20 = __toESM(require_picocolors(), 1);
48523
- import { createHash as createHash3 } from "node:crypto";
48524
- import { mkdir as mkdir9, mkdtemp as mkdtemp3, writeFile as writeFile12 } from "node:fs/promises";
48525
- import { tmpdir as tmpdir3 } from "node:os";
48526
- import { join as join18, resolve as resolve15 } from "node:path";
48527
- import { setTimeout as sleep9 } from "node:timers/promises";
48528
- init_errors();
48529
- var HOLD_SECONDS = 50;
48530
- var ASK_EVERY_MS2 = 5e3;
48531
- var OPEN = /* @__PURE__ */ new Set(["waiting", "running", "recorded"]);
48532
- var GOING = /* @__PURE__ */ new Set(["waiting", "running"]);
48533
- var REVIEW_ROUTE = "/projects/{owner}/{slug}/releases/{version}/review";
48534
- var reviewOf = (ctx, release2) => ctx.api.GET(REVIEW_ROUTE, { params: { path: release2 } });
48535
- async function testStart(ctx, ref, options) {
48536
- requireToken(ctx.target);
48537
- const release2 = parseReleaseRef(ref);
48538
- const wait2 = waitOf(options.wait);
48539
- const before = await reviewOf(ctx, release2);
48540
- const open3 = before.data?.test && OPEN.has(before.data.test.state) ? before.data.test : null;
48541
- const test2 = open3 ?? await startOne(ctx, release2, before.data?.status);
48542
- return show3(ctx, release2, wait2 === null ? test2 : await follow3(ctx, test2, wait2), {
48543
- started: !open3,
48544
- followed: wait2 !== null
48545
- });
48546
- }
48547
- async function startOne(ctx, release2, status2) {
48548
- const name = releaseName(release2);
48549
- const answer = await ctx.api.POST("/projects/{owner}/{slug}/releases/{version}/tests", {
48550
- params: { path: release2 }
48551
- });
48552
- if (answer.response.ok) return answer.data;
48553
- const failure2 = fromResponse(answer.response, answer.error);
48554
- if (failure2.code === "files_failed") {
48555
- const { findings } = failure2.details;
48556
- throw filesFailed(failure2.message, findings);
48557
- }
48558
- if (failure2.code === "not_testable") {
48559
- const next = `pch release submit ${name}`;
48560
- throw new CliError(
48561
- failure2.exitCode,
48562
- failure2.code,
48563
- `Patch Desktop can't test ${name}, as it's a mod players install some other way, so it skips the test. Send it for review once it's ready: ${next}`,
48564
- { next }
48565
- );
48566
- }
48567
- if (failure2.code === "not_testing" && status2) {
48568
- const next = nextStep(name, status2, null);
48569
- throw new CliError(
48570
- failure2.exitCode,
48571
- failure2.code,
48572
- `${name} is ${status2}, so it can't be tested. ${sayNext(next)}`,
48573
- { next: next.command }
48574
- );
48575
- }
48576
- throw failure2;
48577
- }
48578
- async function testView(ctx, ref, options) {
48579
- requireToken(ctx.target);
48580
- const release2 = parseReleaseRef(ref);
48581
- const wait2 = waitOf(options.wait);
48582
- const test2 = await newest(ctx, release2);
48583
- return show3(ctx, release2, wait2 === null ? test2 : await follow3(ctx, test2, wait2), {
48584
- followed: wait2 !== null
48585
- });
48586
- }
48587
- async function newest(ctx, release2) {
48588
- const review = await unwrap(reviewOf(ctx, release2));
48589
- if (review.test) return review.test;
48590
- const name = releaseName(release2);
48591
- const next = `pch test start ${name}`;
48592
- throw new CliError(EXIT.error, "not_found", `${name} has no test yet. Start one: ${next}`, {
48593
- next
48594
- });
48595
- }
48596
- async function follow3(ctx, test2, seconds2) {
48597
- const deadline = Date.now() + seconds2 * 1e3;
48598
- let current = test2;
48599
- let said2 = "";
48600
- while (GOING.has(current.state) && Date.now() < deadline) {
48601
- const now = whatsHappening(current);
48602
- if (now !== said2) ctx.out.step(now);
48603
- said2 = now;
48604
- if (current.needs_person) await sleep9(Math.min(ASK_EVERY_MS2, deadline - Date.now()));
48605
- const left = Math.ceil((deadline - Date.now()) / 1e3);
48606
- current = await unwrap(
48607
- ctx.api.GET("/tests/{id}", {
48608
- params: {
48609
- path: { id: current.id },
48610
- query: { wait: Math.max(0, Math.min(HOLD_SECONDS, left)) }
48611
- }
48612
- })
48613
- );
48614
- }
48615
- return current;
48616
- }
48617
- function whatsHappening(test2) {
48618
- if (test2.state === "running") {
48619
- const where = test2.pc_name ? ` on ${plain(test2.pc_name)}` : "";
48620
- const phase = test2.progress?.phase;
48621
- return phase && phase !== "playing" ? `Patch Desktop${where} is ${plain(phase)} it` : `Patch Desktop${where} is playing it`;
48622
- }
48623
- if (test2.needs_person) return plain(test2.needs_person.ask);
48624
- return "Waiting for you to press Start in Patch Desktop";
48625
- }
48626
- async function show3(ctx, release2, test2, { followed, ...extra } = {}) {
48627
- const { out } = ctx;
48628
- const name = releaseName(release2);
48629
- const next = nextStep(name, test2.state === "failed" ? "rejected" : "testing", test2);
48630
- const command = next.command;
48631
- const play2 = test2.state === "passed" ? await playFirst(ctx, release2) : null;
48632
- const ask = test2.state === "waiting" ? askThePerson(test2) : play2;
48633
- const noteAsk = test2.note_ask?.replaceAll("suggest_note", "pch note suggest");
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
- };
48645
- out.result({ ...shown, ...ask ? { ask } : {} }, () => {
48646
- const bold = import_picocolors20.default.bold(name);
48647
- if (test2.state === "waiting") {
48648
- out.line(
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.`
48650
- );
48651
- if (test2.needs_person) {
48652
- out.line(plain(test2.needs_person.why));
48653
- out.line(plain(test2.needs_person.ask));
48654
- }
48655
- } else if (test2.state === "running") {
48656
- out.line(`${bold}: ${whatsHappening(test2)}.`);
48657
- } else if (test2.state === "recorded") {
48658
- out.line(
48659
- `${bold}: it has been played and recorded. Look at the footage, then send your review.`
48660
- );
48661
- } else if (test2.state === "passed") {
48662
- out.line(`${bold}: ${import_picocolors20.default.green("its test passed.")} It's still private.`);
48663
- if (test2.review) out.line(plain(test2.review.summary));
48664
- if (play2) out.line(play2);
48665
- out.line(
48666
- "Publish it once you're happy it's ready: that's your yes, and nothing does it for you."
48667
- );
48668
- } else if (test2.state === "failed") {
48669
- out.line(`${bold}: ${import_picocolors20.default.red("its test failed")}, so this version can't be published.`);
48670
- showWhatFailed(ctx, test2);
48671
- out.line(`pch status ${name} says how to fix it.`);
48672
- out.line("Then move the version on in patch.json and publish that.");
48673
- } else if (test2.state === "couldnt_run") {
48674
- out.line(`${bold}: ${import_picocolors20.default.yellow("its test couldn't run.")}`);
48675
- if (test2.outcome) out.line(plain(test2.outcome.reason));
48676
- } else {
48677
- out.line(`${bold}: its test was cancelled.`);
48678
- }
48679
- if (noteAsk) out.line(plain(noteAsk));
48680
- if (feedbackAsk) out.line(plain(feedbackAsk.ask));
48681
- if (followed && GOING.has(test2.state)) {
48682
- out.line("It isn't over, and nothing's wrong: that's as long as --wait follows it here.");
48683
- }
48684
- if (command) out.line(`Next: ${command}`);
48685
- if (OPEN.has(test2.state) && !extra.started) {
48686
- out.line(import_picocolors20.default.dim(`To start over: pch test cancel ${name}, then pch test start ${name}`));
48687
- }
48688
- });
48689
- if (test2.state === "failed") return EXIT.blocked;
48690
- if (test2.state === "couldnt_run" || test2.state === "cancelled") return EXIT.error;
48691
- return EXIT.ok;
48692
- }
48693
- function askThePerson(test2) {
48694
- return test2.needs_person?.ask ?? null;
48695
- }
48696
- function showWhatFailed({ out }, test2) {
48697
- for (const run3 of test2.results) {
48698
- for (const check3 of run3.checks) {
48699
- if (check3.result !== "fail") continue;
48700
- const detail = check3.detail ? ` (${plain(check3.detail)})` : "";
48701
- out.line(` Run ${run3.run}: ${plain(check3.name)}${detail}`);
48702
- }
48703
- }
48704
- if (test2.review) out.line(` Its review: ${plain(test2.review.summary)}`);
48705
- }
48706
- async function testFootage(ctx, ref, options) {
48707
- requireToken(ctx.target);
48708
- const { out } = ctx;
48709
- const release2 = parseReleaseRef(ref);
48710
- const name = releaseName(release2);
48711
- const run3 = options.run === void 0 ? void 0 : positive(options.run, "--run");
48712
- const test2 = await newest(ctx, release2);
48713
- const { data } = await unwrap(
48714
- ctx.api.GET("/tests/{id}/footage", { params: { path: { id: test2.id }, query: { run: run3 } } })
48715
- );
48716
- const sheets = data.length ? await saveSheets(
48717
- data,
48718
- options.out ? resolve15(options.out) : await mkdtemp3(join18(tmpdir3(), "pch-footage-"))
48719
- ) : [];
48720
- const next = data.length ? reviewCommand(name) : nextStep(name, "testing", test2).command;
48721
- out.result({ test: test2.id, state: test2.state, data: sheets, next }, () => {
48722
- if (!sheets.length) {
48723
- out.line(`${import_picocolors20.default.bold(name)} has no footage yet: its test is ${test2.state.replace("_", " ")}.`);
48724
- for (const result of test2.results) {
48725
- if (result.footage_error) out.line(plain(result.footage_error));
48726
- }
48727
- }
48728
- for (const sheet of sheets) {
48729
- out.line(`Run ${sheet.run}, ${sheet.from} to ${sheet.to}: ${sheet.path}`);
48730
- }
48731
- if (next) {
48732
- if (sheets.length) out.line("Look at every one, then send your review, with what you saw:");
48733
- out.line(`Next: ${next}`);
48734
- }
48735
- });
48736
- }
48737
- var reviewCommand = (name) => `pch test review ${name} --verdict <pass|fail> --plays <yes|no> --harmful <yes|no> --summary "<what you saw>" --moment "<run> <mm:ss> <what you saw>" --moment "<run> <mm:ss> <what you saw>"`;
48738
- async function saveSheets(sheets, folder) {
48739
- await mkdir9(folder, { recursive: true });
48740
- const saved = [];
48741
- for (const sheet of sheets) {
48742
- const answer = await fetch(sheet.url).catch(() => null);
48743
- const bytes = answer?.ok ? Buffer.from(await answer.arrayBuffer()) : null;
48744
- if (!bytes || createHash3("sha256").update(bytes).digest("hex") !== sheet.sha256) {
48745
- throw new CliError(
48746
- EXIT.error,
48747
- "download_failed",
48748
- `Run ${sheet.run}'s sheet ${sheet.index} didn't download intact. Try again.`
48749
- );
48750
- }
48751
- const path2 = join18(folder, `run-${sheet.run}-sheet-${sheet.index}.jpg`);
48752
- await writeFile12(path2, bytes);
48753
- const { url: _url2, ...kept } = sheet;
48754
- saved.push({ ...kept, path: path2 });
48755
- }
48756
- return saved;
48757
- }
48758
- async function testReview(ctx, ref, options) {
48759
- requireToken(ctx.target);
48760
- const release2 = parseReleaseRef(ref);
48761
- const moments = options.moment.map(parseMoment);
48762
- const test2 = await newest(ctx, release2);
48763
- const answer = await ctx.api.POST("/tests/{id}/review", {
48764
- params: { path: { id: test2.id } },
48765
- body: {
48766
- verdict: options.verdict,
48767
- plays: options.plays === "yes",
48768
- harmful: options.harmful === "yes",
48769
- summary: options.summary,
48770
- moments
48771
- }
48772
- });
48773
- if (answer.response.ok) return show3(ctx, release2, answer.data);
48774
- throw withNext(fromResponse(answer.response, answer.error), release2, test2);
48775
- }
48776
- function parseMoment(text) {
48777
- const match = /^(\d+)\s+(\d{1,3}:\d{2})\s+(\S.*)$/s.exec(text.trim());
48778
- if (!match) {
48779
- throw new CliError(
48780
- EXIT.usage,
48781
- "bad_moment",
48782
- `"${text}" isn't a moment. Write its run, the time on a frame and what you saw, such as "1 00:04 the title screen draws".`
48783
- );
48784
- }
48785
- return { run: Number(match[1]), at: match[2], saw: match[3] };
48786
- }
48787
- async function testCancel(ctx, ref) {
48788
- requireToken(ctx.target);
48789
- const release2 = parseReleaseRef(ref);
48790
- const name = releaseName(release2);
48791
- const test2 = await newest(ctx, release2);
48792
- const answer = await ctx.api.POST("/tests/{id}/cancel", { params: { path: { id: test2.id } } });
48793
- if (!answer.response.ok) {
48794
- throw withNext(fromResponse(answer.response, answer.error), release2, test2);
48795
- }
48796
- const next = `pch test start ${name}`;
48797
- ctx.out.result({ ...answer.data, next }, () => {
48798
- ctx.out.line(`Cancelled ${import_picocolors20.default.bold(name)}'s test. Start another: ${next}`);
48799
- });
48800
- }
48801
- function withNext(failure2, release2, test2) {
48802
- if (failure2.code !== "test_state") return failure2;
48803
- const next = nextStep(releaseName(release2), "testing", test2);
48804
- return new CliError(failure2.exitCode, failure2.code, `${failure2.message}. ${sayNext(next)}`, {
48805
- next: next.command
48806
- });
48807
- }
48808
- function positive(value, flag) {
48809
- if (/^[1-9]\d*$/.test(value)) return Number(value);
48810
- throw new CliError(EXIT.usage, "bad_usage", `${flag} takes a whole number, not "${value}".`);
48811
- }
48812
- async function testAutostart(ctx, state) {
48813
- requireToken(ctx.target);
48814
- if (state !== "on" && state !== "off") {
48815
- throw new CliError(EXIT.usage, "bad_value", `Say on or off, not "${state}"`);
48816
- }
48817
- const setting = await unwrap(
48818
- ctx.api.PUT("/me/test-settings", { body: { starts_own_tests: state === "on", by: "agent" } })
48819
- );
48820
- ctx.out.result(setting, () => {
48821
- ctx.out.line(
48822
- setting.starts_own_tests ? "Your tests of browser games and blocks now start in Patch Desktop by themselves, with a notice each time that lets the person stop them. Patch Desktop tells them you turned it on. A mod's test still waits for Start." : "Your tests wait for the person to press Start in Patch Desktop again."
48823
- );
48824
- });
48825
- }
48826
-
48827
49228
  // src/commands/search.ts
48828
- var import_picocolors21 = __toESM(require_picocolors(), 1);
49229
+ var import_picocolors23 = __toESM(require_picocolors(), 1);
48829
49230
  init_guide_words();
48830
49231
  var filter = (values) => values?.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean).join(",") || void 0;
48831
49232
  var licences = (values) => values.flatMap((value) => value.split(",")).map((value) => licenceId(value.trim()) ?? value.trim());
@@ -48863,13 +49264,13 @@ async function search(ctx, q, options) {
48863
49264
  out.result({ ...answer, ...aboutBlocks }, () => {
48864
49265
  if (!data.length) out.line("Nothing found.");
48865
49266
  for (const result of plain(data)) {
48866
- out.line(`${import_picocolors21.default.bold(result.handle)} ${import_picocolors21.default.dim(facts(result))}`);
49267
+ out.line(`${import_picocolors23.default.bold(result.handle)} ${import_picocolors23.default.dim(facts(result))}`);
48867
49268
  if (result.summary) out.line(` ${result.summary}`);
48868
49269
  if (result.demo) out.line(` Live demo: ${ctx.target.origin}/${result.owner}/${result.slug}`);
48869
- out.line(import_picocolors21.default.dim(` ${result.clone_url}`));
49270
+ out.line(import_picocolors23.default.dim(` ${result.clone_url}`));
48870
49271
  }
48871
49272
  if (blocks) out.line(BLOCK_WORDS);
48872
- if (cursor) out.line(import_picocolors21.default.dim("There are more; raise --limit."));
49273
+ if (cursor) out.line(import_picocolors23.default.dim("There are more; raise --limit."));
48873
49274
  });
48874
49275
  }
48875
49276
  function facts(result) {
@@ -48894,21 +49295,21 @@ async function categories(ctx, options) {
48894
49295
  const width = Math.max(0, ...data.map((category) => category.id.length));
48895
49296
  for (const category of data) {
48896
49297
  ctx.out.line(
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`}`)}`
49298
+ `${import_picocolors23.default.bold(category.id.padEnd(width))} ${category.name} ${import_picocolors23.default.dim(`\xB7 ${category.for.join(", ")} \xB7 ${category.projects === 1 ? "1 project" : `${category.projects} projects`}`)}`
48898
49299
  );
48899
49300
  }
48900
49301
  });
48901
49302
  }
48902
49303
 
48903
49304
  // src/commands/update.ts
48904
- var import_picocolors22 = __toESM(require_picocolors(), 1);
49305
+ var import_picocolors24 = __toESM(require_picocolors(), 1);
48905
49306
  var import_semver3 = __toESM(require_semver2(), 1);
48906
- import { spawnSync as spawnSync2 } from "node:child_process";
49307
+ import { spawnSync as spawnSync3 } from "node:child_process";
48907
49308
  import { createHash as createHash4 } from "node:crypto";
48908
49309
  import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
48909
49310
  import { chmod as chmod3, rename as rename2, rm as rm3, writeFile as writeFile13 } from "node:fs/promises";
48910
- import { basename as basename8, dirname as dirname7, join as join19 } from "node:path";
48911
- import { setTimeout as sleep10 } from "node:timers/promises";
49311
+ import { basename as basename8, dirname as dirname7, join as join20 } from "node:path";
49312
+ import { setTimeout as sleep11 } from "node:timers/promises";
48912
49313
  import { gunzipSync } from "node:zlib";
48913
49314
  init_errors();
48914
49315
  init_version();
@@ -49016,7 +49417,7 @@ async function update(ctx) {
49016
49417
  const fresh = `${target}.new`;
49017
49418
  await writeFile13(fresh, gunzipSync(packed), { mode: 493 });
49018
49419
  await chmod3(fresh, 493);
49019
- const said2 = spawnSync2(fresh, ["--version"], { encoding: "utf8", timeout: 6e4 });
49420
+ const said2 = spawnSync3(fresh, ["--version"], { encoding: "utf8", timeout: 6e4 });
49020
49421
  if (said2.status !== 0 || said2.stdout.trim() !== latest2) {
49021
49422
  await whenFree(() => rm3(fresh, { force: true })).catch(() => void 0);
49022
49423
  throw new CliError(
@@ -49051,7 +49452,7 @@ async function whenFree(step, wait2 = 200) {
49051
49452
  } catch (error62) {
49052
49453
  const code = error62.code ?? "";
49053
49454
  if (attempt >= 10 || !["EBUSY", "EPERM", "EACCES"].includes(code)) throw error62;
49054
- await sleep10(attempt * wait2);
49455
+ await sleep11(attempt * wait2);
49055
49456
  }
49056
49457
  }
49057
49458
  }
@@ -49061,7 +49462,7 @@ function removeOldExecutables(target = process.execPath) {
49061
49462
  const old = new RegExp(`^${basename8(target).replace(/\./g, "\\.")}\\.\\d+\\.old$`, "i");
49062
49463
  try {
49063
49464
  for (const name of readdirSync(folder)) {
49064
- if (old.test(name)) rmSync(join19(folder, name), { force: true });
49465
+ if (old.test(name)) rmSync(join20(folder, name), { force: true });
49065
49466
  }
49066
49467
  } catch {
49067
49468
  }
@@ -49071,7 +49472,7 @@ function startUpdateNotice(ctx, command, now = Date.now()) {
49071
49472
  const releases = releasesUrl(ctx.target.env);
49072
49473
  if (!how || !releases || ctx.out.json || QUIET.has(command)) return null;
49073
49474
  if (!process.stderr.isTTY || process.env.CI) return null;
49074
- const stamp = join19(dirname7(configPath()), "update-check.json");
49475
+ const stamp = join20(dirname7(configPath()), "update-check.json");
49075
49476
  try {
49076
49477
  const { checked_at } = JSON.parse(readFileSync(stamp, "utf8"));
49077
49478
  if (checked_at && now - Date.parse(checked_at) < DAY) return null;
@@ -49100,7 +49501,7 @@ function startUpdateNotice(ctx, command, now = Date.now()) {
49100
49501
  if (version2 && import_semver3.default.gt(version2, VERSION)) {
49101
49502
  process.stderr.write(
49102
49503
  `
49103
- ${import_picocolors22.default.dim("\u203A")} pch ${version2} is out (you have ${VERSION}). To update, run: ${how}
49504
+ ${import_picocolors24.default.dim("\u203A")} pch ${version2} is out (you have ${VERSION}). To update, run: ${how}
49104
49505
  `
49105
49506
  );
49106
49507
  }
@@ -49109,7 +49510,7 @@ ${import_picocolors22.default.dim("\u203A")} pch ${version2} is out (you have ${
49109
49510
  }
49110
49511
 
49111
49512
  // src/commands/validate.ts
49112
- var import_picocolors23 = __toESM(require_picocolors(), 1);
49513
+ var import_picocolors25 = __toESM(require_picocolors(), 1);
49113
49514
  init_errors();
49114
49515
  async function validate2(ctx, dir) {
49115
49516
  const project2 = await loadProject(dir);
@@ -49168,12 +49569,12 @@ async function validate2(ctx, dir) {
49168
49569
  notes
49169
49570
  };
49170
49571
  ctx.out.result(result, () => {
49171
- const mark = result.ok ? import_picocolors23.default.green("\u2713") : import_picocolors23.default.red("\u2717");
49572
+ const mark = result.ok ? import_picocolors25.default.green("\u2713") : import_picocolors25.default.red("\u2717");
49172
49573
  ctx.out.line(
49173
49574
  `${mark} ${file2.project}@${file2.version} \xB7 ${project2.kind} \xB7 ${result.trust_tier} \xB7 ${project2.files.length} files, ${formatSize(total)}`
49174
49575
  );
49175
- for (const issue2 of issues) ctx.out.line(` ${import_picocolors23.default.red(issue2.path || "\xB7")} ${issue2.message}`);
49176
- for (const note2 of notes) ctx.out.line(` ${import_picocolors23.default.dim(note2)}`);
49576
+ for (const issue2 of issues) ctx.out.line(` ${import_picocolors25.default.red(issue2.path || "\xB7")} ${issue2.message}`);
49577
+ for (const note2 of notes) ctx.out.line(` ${import_picocolors25.default.dim(note2)}`);
49177
49578
  });
49178
49579
  return result.ok ? EXIT.ok : EXIT.usage;
49179
49580
  }
@@ -49208,8 +49609,7 @@ Start here:
49208
49609
  pch search "grappling hook" find a block or game to start from, then pch remix or pch clone it, and change it
49209
49610
  pch assets search "crate" --type model find a model, sound or texture, then pch assets add it
49210
49611
  pch new <you>/<game> --licence MIT make a repo with a patch.json, or pch init in a folder you have
49211
- pch validate, then pch publish
49212
- pch test start <you>/<game>@<version> a game, mod or block goes live only after passing a test in Patch Desktop
49612
+ pch validate, then pch publish upload, test on your PC, your review of the footage, then live
49213
49613
  pch media add, pch project update the cover and the listing
49214
49614
 
49215
49615
  Add --json to any command for results an agent can read. Exit codes: 0 fine, 1 error, 2 bad usage,
@@ -49463,17 +49863,29 @@ program2.command("check [dir]").description(
49463
49863
  (ctx, dir, options) => check2(ctx, dir ?? ".", options)
49464
49864
  )
49465
49865
  );
49466
- program2.command("publish [dir]").description(
49467
- "Hash, upload missing files and submit a release. A browser game or a mod Patch installs stays a private draft until pch test has passed it. If it stops partway, run it again: what's up already isn't sent again"
49866
+ program2.command("publish [target]").description(
49867
+ "Publish from upload to live: uploads what Patch doesn't have, tests it on your PC, saves the footage for your review, then follows Patch's review until it's live. Running it is your yes. Give owner/slug@version instead of a folder to carry a release on, with --review once you've looked at its footage"
49468
49868
  ).addOption(
49469
49869
  new Option("--channel <channel>", `overrides patch.json. ${NOT_LOCKED_HELP}`).choices([
49470
49870
  "private",
49471
49871
  "unlisted",
49472
49872
  "public"
49473
49873
  ])
49474
- ).option("--changelog <text>", "what changed, shown as the update notes").action(
49874
+ ).option("--changelog <text>", "what changed, shown as the update notes").addOption(
49875
+ new Option("--review <verdict>", "your review of the footage: pass or fail").choices([
49876
+ "pass",
49877
+ "fail"
49878
+ ])
49879
+ ).option("--summary <text>", "with --review: what you saw, in a few lines").option(
49880
+ "--moment <moment>",
49881
+ 'with --review: "<run> <mm:ss> <what you saw>", at least two, one per run (repeatable)',
49882
+ collect
49883
+ ).option("--harmful", "with --review: it does something harmful (fails it)").option("--test-again", "test it again after a test that couldn't run").option("--out <dir>", "where to save the footage").option(
49884
+ "--wait <seconds>",
49885
+ "how long to follow it (default: until it stops, at a terminal; 100 s otherwise)"
49886
+ ).action(
49475
49887
  action(
49476
- (ctx, dir, options) => publish(ctx, dir ?? ".", options)
49888
+ (ctx, target, options) => publish(ctx, target ?? ".", options)
49477
49889
  )
49478
49890
  );
49479
49891
  var project = program2.command("project").description("Projects, addressed owner/slug");
@@ -49617,10 +50029,10 @@ assets.command("add <ids...>").description(
49617
50029
  (ctx, ids, options) => assetsAdd(ctx, ids, options)
49618
50030
  )
49619
50031
  );
49620
- var release = program2.command("release").description("A project's releases");
49621
- release.command("list <project>").action(action((ctx, ref) => releaseList(ctx, ref)));
49622
- release.command("view <release>").description("owner/slug@version").action(action((ctx, ref) => releaseView(ctx, ref)));
49623
- release.command("submit <release>").description(
50032
+ var release2 = program2.command("release").description("A project's releases");
50033
+ release2.command("list <project>").action(action((ctx, ref) => releaseList(ctx, ref)));
50034
+ release2.command("view <release>").description("owner/slug@version").action(action((ctx, ref) => releaseView(ctx, ref)));
50035
+ release2.command("submit <release>").description(
49624
50036
  "Send a release on to be published, owner/slug@version: this is your yes. A browser game or a mod Patch installs has to have passed a test"
49625
50037
  ).action(action((ctx, ref) => releaseSubmit(ctx, ref)));
49626
50038
  var test = program2.command("test").description(
@@ -49667,7 +50079,7 @@ test.command("review <release>").description("Send your review of a test's foota
49667
50079
  )
49668
50080
  );
49669
50081
  test.command("cancel <release>").description("Cancel a release's test, if it hasn't been decided").action(action((ctx, ref) => testCancel(ctx, ref)));
49670
- release.command("promote <release>").description(
50082
+ release2.command("promote <release>").description(
49671
50083
  "Put owner/slug@version on another channel: a new version with the same files, which uploads nothing"
49672
50084
  ).addOption(
49673
50085
  new Option("--channel <channel>", `where it goes. ${NOT_LOCKED_HELP}`).choices(["private", "unlisted", "public"]).default("public")