@patchgg/cli 0.2.0 → 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 +19 -29
  2. package/dist/cli.js +580 -429
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -187,7 +187,7 @@ var VERSION;
187
187
  var init_version = __esm({
188
188
  "src/version.ts"() {
189
189
  "use strict";
190
- VERSION = true ? "0.2.0" : 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;
191
191
  }
192
192
  });
193
193
 
@@ -247,13 +247,13 @@ git add -A && git commit -m "..."
247
247
  ${pch} publish
248
248
  \`\`\`
249
249
 
250
- 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:
251
251
 
252
252
  \`\`\`sh
253
- ${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>"
254
254
  \`\`\`
255
255
 
256
- 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.
257
257
 
258
258
  ## 4. Its listing
259
259
 
@@ -304,6 +304,7 @@ var init_guide_words = __esm({
304
304
  ["search_assets, asset_facets, get_asset", "assets search, assets facets, assets view"],
305
305
  ["get_asset_files", "assets add"],
306
306
  ["validate_recipe", "validate"],
307
+ ["publish", "publish, then publish <release> --review \u2026"],
307
308
  ["create_release, complete_upload", "publish, from a folder with a patch.json"],
308
309
  ["submit_release", "publish, or release submit"],
309
310
  ["list_releases, release_status", "release list, status"],
@@ -6847,7 +6848,7 @@ function isRef(value) {
6847
6848
  function cloneIssues(issues) {
6848
6849
  return issues.map((iss) => iss.path ? { ...iss, path: iss.path.slice() } : { ...iss });
6849
6850
  }
6850
- function isRecursive(inst, stack, resolve16) {
6851
+ function isRecursive(inst, stack, resolve17) {
6851
6852
  const cached2 = recursive.get(inst);
6852
6853
  if (cached2 !== void 0)
6853
6854
  return cached2 ? PROVEN : NONE;
@@ -6857,7 +6858,7 @@ function isRecursive(inst, stack, resolve16) {
6857
6858
  let result = NONE;
6858
6859
  const check3 = (child) => {
6859
6860
  if (result !== PROVEN && child?._zod) {
6860
- const answer = isRecursive(child, stack, resolve16);
6861
+ const answer = isRecursive(child, stack, resolve17);
6861
6862
  if (answer > result)
6862
6863
  result = answer;
6863
6864
  }
@@ -6868,7 +6869,7 @@ function isRecursive(inst, stack, resolve16) {
6868
6869
  const desc = Object.getOwnPropertyDescriptor(sh, key);
6869
6870
  if (spread && !desc.enumerable)
6870
6871
  continue;
6871
- 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;
6872
6873
  if (child > answer)
6873
6874
  answer = child;
6874
6875
  }
@@ -6932,7 +6933,7 @@ function isRecursive(inst, stack, resolve16) {
6932
6933
  break;
6933
6934
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
6934
6935
  case "lazy": {
6935
- const inner = def._cachedInner ?? (resolve16 ? inst._zod.innerType : void 0);
6936
+ const inner = def._cachedInner ?? (resolve17 ? inst._zod.innerType : void 0);
6936
6937
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
6937
6938
  break;
6938
6939
  }
@@ -25300,7 +25301,7 @@ var init_protocol = __esm({
25300
25301
  return;
25301
25302
  }
25302
25303
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
25303
- await new Promise((resolve16) => setTimeout(resolve16, pollInterval));
25304
+ await new Promise((resolve17) => setTimeout(resolve17, pollInterval));
25304
25305
  options?.signal?.throwIfAborted();
25305
25306
  }
25306
25307
  } catch (error62) {
@@ -25317,7 +25318,7 @@ var init_protocol = __esm({
25317
25318
  */
25318
25319
  request(request, resultSchema, options) {
25319
25320
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
25320
- return new Promise((resolve16, reject) => {
25321
+ return new Promise((resolve17, reject) => {
25321
25322
  const earlyReject = (error62) => {
25322
25323
  reject(error62);
25323
25324
  };
@@ -25395,7 +25396,7 @@ var init_protocol = __esm({
25395
25396
  if (!parseResult.success) {
25396
25397
  reject(parseResult.error);
25397
25398
  } else {
25398
- resolve16(parseResult.data);
25399
+ resolve17(parseResult.data);
25399
25400
  }
25400
25401
  } catch (error62) {
25401
25402
  reject(error62);
@@ -25657,12 +25658,12 @@ var init_protocol = __esm({
25657
25658
  }
25658
25659
  } catch {
25659
25660
  }
25660
- return new Promise((resolve16, reject) => {
25661
+ return new Promise((resolve17, reject) => {
25661
25662
  if (signal.aborted) {
25662
25663
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
25663
25664
  return;
25664
25665
  }
25665
- const timeoutId = setTimeout(resolve16, interval);
25666
+ const timeoutId = setTimeout(resolve17, interval);
25666
25667
  signal.addEventListener("abort", () => {
25667
25668
  clearTimeout(timeoutId);
25668
25669
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -28689,7 +28690,7 @@ var require_compile = __commonJS({
28689
28690
  const schOrFunc = root.refs[ref];
28690
28691
  if (schOrFunc)
28691
28692
  return schOrFunc;
28692
- let _sch = resolve16.call(this, root, ref);
28693
+ let _sch = resolve17.call(this, root, ref);
28693
28694
  if (_sch === void 0) {
28694
28695
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
28695
28696
  const { schemaId } = this.opts;
@@ -28716,7 +28717,7 @@ var require_compile = __commonJS({
28716
28717
  function sameSchemaEnv(s1, s2) {
28717
28718
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
28718
28719
  }
28719
- function resolve16(root, ref) {
28720
+ function resolve17(root, ref) {
28720
28721
  let sch;
28721
28722
  while (typeof (sch = this.refs[ref]) == "string")
28722
28723
  ref = sch;
@@ -29580,7 +29581,7 @@ var require_fast_uri = __commonJS({
29580
29581
  }
29581
29582
  return uri;
29582
29583
  }
29583
- function resolve16(baseURI, relativeURI, options) {
29584
+ function resolve17(baseURI, relativeURI, options) {
29584
29585
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
29585
29586
  const {
29586
29587
  parsed: baseParsed,
@@ -29971,7 +29972,7 @@ var require_fast_uri = __commonJS({
29971
29972
  var fastUri = {
29972
29973
  SCHEMES,
29973
29974
  normalize,
29974
- resolve: resolve16,
29975
+ resolve: resolve17,
29975
29976
  resolveComponent,
29976
29977
  equal,
29977
29978
  serialize,
@@ -35644,12 +35645,12 @@ var init_stdio2 = __esm({
35644
35645
  this.onclose?.();
35645
35646
  }
35646
35647
  send(message) {
35647
- return new Promise((resolve16) => {
35648
+ return new Promise((resolve17) => {
35648
35649
  const json2 = serializeMessage(message);
35649
35650
  if (this._stdout.write(json2)) {
35650
- resolve16();
35651
+ resolve17();
35651
35652
  } else {
35652
- this._stdout.once("drain", resolve16);
35653
+ this._stdout.once("drain", resolve17);
35653
35654
  }
35654
35655
  });
35655
35656
  }
@@ -35668,8 +35669,8 @@ async function mcp(ctx, streams = {}) {
35668
35669
  const remote = patchServer(ctx, "start pch mcp again");
35669
35670
  const stdin = streams.stdin ?? process.stdin;
35670
35671
  const local = new StdioServerTransport(stdin, streams.stdout ?? takeStdout());
35671
- const hungUp = new Promise((resolve16) => {
35672
- local.onclose = resolve16;
35672
+ const hungUp = new Promise((resolve17) => {
35673
+ local.onclose = resolve17;
35673
35674
  });
35674
35675
  stdin.once("end", () => void local.close());
35675
35676
  let landed = Promise.resolve();
@@ -42631,8 +42632,8 @@ This project is on [Patch](https://patch.gg), where people publish and play brow
42631
42632
 
42632
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.
42633
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.
42634
- - **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>\`.
42635
- - **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.
42636
42637
  - **Push it.** Plain \`git push\` works. If git says HTTP 413, that's over 100 MB: run \`pch push\`, which takes any size.
42637
42638
  - **The rest.** \`pch guide\` has everything else.
42638
42639
  `;
@@ -45442,7 +45443,7 @@ var LABELS = {
45442
45443
  warn: "look ",
45443
45444
  cant_check: "can't"
45444
45445
  };
45445
- 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.";
45446
45447
  async function findGame(dir) {
45447
45448
  const root = resolve8(dir);
45448
45449
  if (existsSync4(join12(root, "patch.json"))) {
@@ -45450,7 +45451,7 @@ async function findGame(dir) {
45450
45451
  if (project2.recipe.mode !== "browser") {
45451
45452
  throw usage2(
45452
45453
  "not_a_browser_game",
45453
- `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.`
45454
45455
  );
45455
45456
  }
45456
45457
  const launch2 = project2.recipe.launch;
@@ -45751,38 +45752,28 @@ function reviewing(progress) {
45751
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)}.`;
45752
45753
  }
45753
45754
  function nextStep(release3, status2, test2, cloud) {
45754
- const follow4 = `pch test view ${release3} --wait`;
45755
- const startOne2 = `pch test start ${release3}`;
45755
+ const carryOn = `pch publish ${release3}`;
45756
45756
  switch (status2) {
45757
45757
  case "draft":
45758
- return {
45759
- 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.",
45760
- command: startOne2
45761
- };
45758
+ return { say: "It's uploaded, and its test comes next.", command: carryOn };
45762
45759
  case "testing":
45763
45760
  switch (test2?.state) {
45764
45761
  case "waiting":
45765
- return {
45766
- say: "Its test is waiting for you: press Start on it in Patch Desktop.",
45767
- command: follow4
45768
- };
45762
+ return { say: "Its test is waiting to start in Patch Desktop.", command: carryOn };
45769
45763
  case "running":
45770
45764
  return {
45771
45765
  say: `Patch Desktop${test2.pc_name ? ` on ${test2.pc_name}` : ""} is playing it.`,
45772
- command: follow4
45766
+ command: carryOn
45773
45767
  };
45774
45768
  case "recorded":
45775
45769
  return {
45776
- say: "It has been played and recorded. Look at the footage, then send your review.",
45777
- command: `pch test footage ${release3}`
45770
+ say: "It's recorded. Look at the footage, then send your review.",
45771
+ command: carryOn
45778
45772
  };
45779
45773
  case "passed":
45780
- return {
45781
- say: "It passed its test, and it's still private. Publish it once you're happy it's ready.",
45782
- command: `pch release submit ${release3}`
45783
- };
45774
+ return { say: "It passed its test.", command: carryOn };
45784
45775
  default:
45785
- return { say: "It needs a test that passes.", command: startOne2 };
45776
+ return { say: "It needs a test that passes.", command: carryOn };
45786
45777
  }
45787
45778
  case "rejected":
45788
45779
  return {
@@ -45791,10 +45782,7 @@ function nextStep(release3, status2, test2, cloud) {
45791
45782
  };
45792
45783
  case "checking":
45793
45784
  case "cloud_review":
45794
- return {
45795
- say: reviewing(cloud) ?? "Patch is reviewing it, which can take several minutes, and nothing tells you when it's done: pch status says.",
45796
- command: `pch status ${release3} --wait 50`
45797
- };
45785
+ return { say: reviewing(cloud) ?? "Patch is reviewing it.", command: carryOn };
45798
45786
  case "in_review":
45799
45787
  return {
45800
45788
  say: "It's waiting for a staff member at Patch, and nothing tells you when that's done: pch status says.",
@@ -47454,7 +47442,7 @@ var PATCH_KEYS = ["PCH_TOKEN", "PCH_GIT_KEY", PUSH_KEY_VARIABLE];
47454
47442
  function run2(dir, args2, signIn = NO_SIGN_IN) {
47455
47443
  const env = { ...process.env };
47456
47444
  if (signIn.own) for (const name of PATCH_KEYS) delete env[name];
47457
- return new Promise((resolve16) => {
47445
+ return new Promise((resolve17) => {
47458
47446
  execFile(
47459
47447
  "git",
47460
47448
  ["-C", dir, ...signIn.args, ...args2],
@@ -47464,7 +47452,7 @@ function run2(dir, args2, signIn = NO_SIGN_IN) {
47464
47452
  },
47465
47453
  (error62, out, err) => {
47466
47454
  const code = error62?.code;
47467
- resolve16({ code: error62 ? typeof code === "number" ? code : -1 : 0, out, err });
47455
+ resolve17({ code: error62 ? typeof code === "number" ? code : -1 : 0, out, err });
47468
47456
  }
47469
47457
  );
47470
47458
  });
@@ -48382,11 +48370,466 @@ async function mediaOrder(ctx, ref, ids) {
48382
48370
  }
48383
48371
 
48384
48372
  // src/commands/publish.ts
48385
- var import_picocolors20 = __toESM(require_picocolors(), 1);
48373
+ var import_picocolors22 = __toESM(require_picocolors(), 1);
48386
48374
  var import_semver2 = __toESM(require_semver2(), 1);
48387
48375
  import { open as open2, readFile as readFile15 } from "node:fs/promises";
48388
48376
  init_errors();
48389
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
48390
48833
  var MB = 1024 * 1024;
48391
48834
  var FILES_AT_ONCE = 16;
48392
48835
  var BIG_FILE = 16 * MB;
@@ -48402,8 +48845,23 @@ function forkNamesParent(named, clone3) {
48402
48845
  { named, clone: clone3 }
48403
48846
  );
48404
48847
  }
48405
- async function publish(ctx, dir, options) {
48848
+ async function publish(ctx, target, options) {
48406
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;
48407
48865
  const { out, api } = ctx;
48408
48866
  const project2 = await loadProject(dir);
48409
48867
  if (project2.issues.length) {
@@ -48462,53 +48920,41 @@ async function publish(ctx, dir, options) {
48462
48920
  } else {
48463
48921
  out.step("Patch already has every file");
48464
48922
  }
48465
- out.step("Submitting");
48466
- const name = releaseName({ ...ref, version: version2 });
48467
- const submitted = await submit(ctx, { ...ref, version: version2 }).catch(draftUntilTested);
48468
- const release3 = await unwrap(
48469
- api.GET("/projects/{owner}/{slug}/releases/{version}", { params: { path: path2 } })
48470
- );
48471
- const url2 = projectUrl(ctx.target.origin, ref);
48472
48923
  const branch = await source?.bringBranch();
48473
- const branchSaid = branch ? { branch } : {};
48474
- const next = nextStep(name, submitted?.status ?? "draft", null).command;
48475
- const openCode = await openCodeOf(ctx, ref, release3);
48476
- const whoCanSee = whoCanSeeIt(
48477
- // A draft is private whichever channel it's for, as the words for a person say.
48478
- !submitted || release3.channel === "private" ? privateWords(release3.kind, !submitted) : null,
48479
- openCode
48480
- );
48481
- const result = {
48482
- release: release3,
48483
- uploaded: draft.uploads.length,
48484
- url: url2,
48485
- next,
48486
- ...branchSaid,
48487
- ...whoCanSee
48488
- };
48489
- out.result(result, () => {
48490
- if (!submitted) {
48491
- out.line(
48492
- `Uploaded ${import_picocolors20.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.`
48493
- );
48494
- out.line(privateWords(release3.kind, true));
48495
- if (openCode) out.line(openCode);
48496
- if (source) out.line(fromCommit(source));
48497
- if (branch?.say) out.line(branch.say);
48498
- out.line(`Next: ${next}`);
48499
- return;
48500
- }
48501
- showSubmitted(ctx, { ...ref, version: version2 }, submitted.status, release3);
48502
- if (openCode) out.line(openCode);
48503
- if (source) out.line(fromCommit(source));
48504
- if (branch?.say) out.line(branch.say);
48505
- for (const entry of release3.files) {
48506
- out.line(
48507
- ` ${entry.path.padEnd(40)} ${formatSize(entry.size).padStart(8)} sha256 ${entry.sha256}`
48508
- );
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
+ }
48509
48956
  }
48510
- out.line(import_picocolors20.default.dim(url2));
48511
- });
48957
+ );
48512
48958
  }
48513
48959
  function fromCommit(source) {
48514
48960
  const on = source.on === "GitHub" ? " on GitHub" : "";
@@ -48527,7 +48973,7 @@ async function releaseSubmit(ctx, ref) {
48527
48973
  ctx.out.result({ ...submitted, next, ...whoCanSee }, () => {
48528
48974
  showSubmitted(ctx, release3, submitted.status, submitted);
48529
48975
  if (openCode) ctx.out.line(openCode);
48530
- ctx.out.line(import_picocolors20.default.dim(projectUrl(ctx.target.origin, release3)));
48976
+ ctx.out.line(import_picocolors22.default.dim(projectUrl(ctx.target.origin, release3)));
48531
48977
  });
48532
48978
  }
48533
48979
  async function releasePromote(ctx, ref, options) {
@@ -48581,7 +49027,7 @@ async function releasePromote(ctx, ref, options) {
48581
49027
  const next = nextStep(name, "draft", null).command;
48582
49028
  ctx.out.result({ from: ref, release: { version: version2, status: "draft" }, url: url2, next }, () => {
48583
49029
  ctx.out.line(
48584
- `Made ${import_picocolors20.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.`
48585
49031
  );
48586
49032
  ctx.out.line(`Next: ${next}`);
48587
49033
  });
@@ -48589,7 +49035,7 @@ async function releasePromote(ctx, ref, options) {
48589
49035
  }
48590
49036
  ctx.out.result({ from: ref, release: submitted, url: url2 }, () => {
48591
49037
  showSubmitted(ctx, to, submitted.status, submitted);
48592
- ctx.out.line(import_picocolors20.default.dim(url2));
49038
+ ctx.out.line(import_picocolors22.default.dim(url2));
48593
49039
  });
48594
49040
  }
48595
49041
  function draftUntilTested(error62) {
@@ -48626,7 +49072,7 @@ function whoCanSeeIt(...words2) {
48626
49072
  function showSubmitted(ctx, ref, status2, { channel, kind }) {
48627
49073
  const name = releaseName(ref);
48628
49074
  if (status2 === "published") {
48629
- ctx.out.line(import_picocolors20.default.green(`Published ${name} to ${channel}`));
49075
+ ctx.out.line(import_picocolors22.default.green(`Published ${name} to ${channel}`));
48630
49076
  if (channel === "private") {
48631
49077
  ctx.out.line(privateWords(kind, false));
48632
49078
  ctx.out.line(`pch status ${name} says whether your team can play it yet, and where.`);
@@ -48779,314 +49225,8 @@ async function uploadFailed(file2, upload, response) {
48779
49225
  );
48780
49226
  }
48781
49227
 
48782
- // src/commands/release-test.ts
48783
- var import_picocolors21 = __toESM(require_picocolors(), 1);
48784
- import { createHash as createHash3 } from "node:crypto";
48785
- import { mkdir as mkdir9, mkdtemp as mkdtemp3, writeFile as writeFile12 } from "node:fs/promises";
48786
- import { tmpdir as tmpdir3 } from "node:os";
48787
- import { join as join18, resolve as resolve15 } from "node:path";
48788
- import { setTimeout as sleep9 } from "node:timers/promises";
48789
- init_errors();
48790
- var HOLD_SECONDS = 50;
48791
- var ASK_EVERY_MS2 = 5e3;
48792
- var OPEN = /* @__PURE__ */ new Set(["waiting", "running", "recorded"]);
48793
- var GOING = /* @__PURE__ */ new Set(["waiting", "running"]);
48794
- var REVIEW_ROUTE = "/projects/{owner}/{slug}/releases/{version}/review";
48795
- var reviewOf = (ctx, release3) => ctx.api.GET(REVIEW_ROUTE, { params: { path: release3 } });
48796
- async function testStart(ctx, ref, options) {
48797
- requireToken(ctx.target);
48798
- const release3 = parseReleaseRef(ref);
48799
- const wait2 = waitOf(options.wait);
48800
- const before = await reviewOf(ctx, release3);
48801
- const open3 = before.data?.test && OPEN.has(before.data.test.state) ? before.data.test : null;
48802
- const test2 = open3 ?? await startOne(ctx, release3, before.data?.status);
48803
- return show3(ctx, release3, wait2 === null ? test2 : await follow3(ctx, test2, wait2), {
48804
- started: !open3,
48805
- followed: wait2 !== null
48806
- });
48807
- }
48808
- async function startOne(ctx, release3, status2) {
48809
- const name = releaseName(release3);
48810
- const answer = await ctx.api.POST("/projects/{owner}/{slug}/releases/{version}/tests", {
48811
- params: { path: release3 }
48812
- });
48813
- if (answer.response.ok) return answer.data;
48814
- const failure2 = fromResponse(answer.response, answer.error);
48815
- if (failure2.code === "files_failed") {
48816
- const { findings } = failure2.details;
48817
- throw filesFailed(failure2.message, findings);
48818
- }
48819
- if (failure2.code === "not_testable") {
48820
- const next = `pch release submit ${name}`;
48821
- throw new CliError(
48822
- failure2.exitCode,
48823
- failure2.code,
48824
- `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}`,
48825
- { next }
48826
- );
48827
- }
48828
- if (failure2.code === "not_testing" && status2) {
48829
- const next = nextStep(name, status2, null);
48830
- throw new CliError(
48831
- failure2.exitCode,
48832
- failure2.code,
48833
- `${name} is ${status2}, so it can't be tested. ${sayNext(next)}`,
48834
- { next: next.command }
48835
- );
48836
- }
48837
- throw failure2;
48838
- }
48839
- async function testView(ctx, ref, options) {
48840
- requireToken(ctx.target);
48841
- const release3 = parseReleaseRef(ref);
48842
- const wait2 = waitOf(options.wait);
48843
- const test2 = await newest(ctx, release3);
48844
- return show3(ctx, release3, wait2 === null ? test2 : await follow3(ctx, test2, wait2), {
48845
- followed: wait2 !== null
48846
- });
48847
- }
48848
- async function newest(ctx, release3) {
48849
- const review = await unwrap(reviewOf(ctx, release3));
48850
- if (review.test) return review.test;
48851
- const name = releaseName(release3);
48852
- const next = `pch test start ${name}`;
48853
- throw new CliError(EXIT.error, "not_found", `${name} has no test yet. Start one: ${next}`, {
48854
- next
48855
- });
48856
- }
48857
- async function follow3(ctx, test2, seconds2) {
48858
- const deadline = Date.now() + seconds2 * 1e3;
48859
- let current = test2;
48860
- let said2 = "";
48861
- while (GOING.has(current.state) && Date.now() < deadline) {
48862
- const now = whatsHappening(current);
48863
- if (now !== said2) ctx.out.step(now);
48864
- said2 = now;
48865
- if (current.needs_person) await sleep9(Math.min(ASK_EVERY_MS2, deadline - Date.now()));
48866
- const left = Math.ceil((deadline - Date.now()) / 1e3);
48867
- current = await unwrap(
48868
- ctx.api.GET("/tests/{id}", {
48869
- params: {
48870
- path: { id: current.id },
48871
- query: { wait: Math.max(0, Math.min(HOLD_SECONDS, left)) }
48872
- }
48873
- })
48874
- );
48875
- }
48876
- return current;
48877
- }
48878
- function whatsHappening(test2) {
48879
- if (test2.state === "running") {
48880
- const where = test2.pc_name ? ` on ${plain(test2.pc_name)}` : "";
48881
- const phase = test2.progress?.phase;
48882
- return phase && phase !== "playing" ? `Patch Desktop${where} is ${plain(phase)} it` : `Patch Desktop${where} is playing it`;
48883
- }
48884
- if (test2.needs_person) return plain(test2.needs_person.ask);
48885
- return "Waiting for you to press Start in Patch Desktop";
48886
- }
48887
- async function show3(ctx, release3, test2, { followed, ...extra } = {}) {
48888
- const { out } = ctx;
48889
- const name = releaseName(release3);
48890
- const next = nextStep(name, test2.state === "failed" ? "rejected" : "testing", test2);
48891
- const command = next.command;
48892
- const play2 = test2.state === "passed" ? await playFirst(ctx, release3) : null;
48893
- const ask = test2.state === "waiting" ? askThePerson(test2) : play2;
48894
- const noteAsk = test2.note_ask?.replaceAll("suggest_note", "pch note suggest");
48895
- const feedbackAsk = test2.feedback_ask ? {
48896
- ...test2.feedback_ask,
48897
- ask: test2.feedback_ask.ask.replaceAll("send_feedback", "pch feedback send")
48898
- } : void 0;
48899
- const shown = {
48900
- ...test2,
48901
- ...extra,
48902
- next: command,
48903
- ...noteAsk ? { note_ask: noteAsk } : {},
48904
- ...feedbackAsk ? { feedback_ask: feedbackAsk } : {}
48905
- };
48906
- out.result({ ...shown, ...ask ? { ask } : {} }, () => {
48907
- const bold = import_picocolors21.default.bold(name);
48908
- if (test2.state === "waiting") {
48909
- out.line(
48910
- !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.`
48911
- );
48912
- if (test2.needs_person) {
48913
- out.line(plain(test2.needs_person.why));
48914
- out.line(plain(test2.needs_person.ask));
48915
- }
48916
- } else if (test2.state === "running") {
48917
- out.line(`${bold}: ${whatsHappening(test2)}.`);
48918
- } else if (test2.state === "recorded") {
48919
- out.line(
48920
- `${bold}: it has been played and recorded. Look at the footage, then send your review.`
48921
- );
48922
- } else if (test2.state === "passed") {
48923
- out.line(`${bold}: ${import_picocolors21.default.green("its test passed.")} It's still private.`);
48924
- if (test2.review) out.line(plain(test2.review.summary));
48925
- if (play2) out.line(play2);
48926
- out.line(
48927
- "Publish it once you're happy it's ready: that's your yes, and nothing does it for you."
48928
- );
48929
- } else if (test2.state === "failed") {
48930
- out.line(`${bold}: ${import_picocolors21.default.red("its test failed")}, so this version can't be published.`);
48931
- showWhatFailed(ctx, test2);
48932
- out.line(`pch status ${name} says how to fix it.`);
48933
- out.line("Then move the version on in patch.json and publish that.");
48934
- } else if (test2.state === "couldnt_run") {
48935
- out.line(`${bold}: ${import_picocolors21.default.yellow("its test couldn't run.")}`);
48936
- if (test2.outcome) out.line(plain(test2.outcome.reason));
48937
- } else {
48938
- out.line(`${bold}: its test was cancelled.`);
48939
- }
48940
- if (noteAsk) out.line(plain(noteAsk));
48941
- if (feedbackAsk) out.line(plain(feedbackAsk.ask));
48942
- if (followed && GOING.has(test2.state)) {
48943
- out.line("It isn't over, and nothing's wrong: that's as long as --wait follows it here.");
48944
- }
48945
- if (command) out.line(`Next: ${command}`);
48946
- if (OPEN.has(test2.state) && !extra.started) {
48947
- out.line(import_picocolors21.default.dim(`To start over: pch test cancel ${name}, then pch test start ${name}`));
48948
- }
48949
- });
48950
- if (test2.state === "failed") return EXIT.blocked;
48951
- if (test2.state === "couldnt_run" || test2.state === "cancelled") return EXIT.error;
48952
- return EXIT.ok;
48953
- }
48954
- function askThePerson(test2) {
48955
- return test2.needs_person?.ask ?? null;
48956
- }
48957
- function showWhatFailed({ out }, test2) {
48958
- for (const run3 of test2.results) {
48959
- for (const check3 of run3.checks) {
48960
- if (check3.result !== "fail") continue;
48961
- const detail = check3.detail ? ` (${plain(check3.detail)})` : "";
48962
- out.line(` Run ${run3.run}: ${plain(check3.name)}${detail}`);
48963
- }
48964
- }
48965
- if (test2.review) out.line(` Its review: ${plain(test2.review.summary)}`);
48966
- }
48967
- async function testFootage(ctx, ref, options) {
48968
- requireToken(ctx.target);
48969
- const { out } = ctx;
48970
- const release3 = parseReleaseRef(ref);
48971
- const name = releaseName(release3);
48972
- const run3 = options.run === void 0 ? void 0 : positive(options.run, "--run");
48973
- const test2 = await newest(ctx, release3);
48974
- const { data } = await unwrap(
48975
- ctx.api.GET("/tests/{id}/footage", { params: { path: { id: test2.id }, query: { run: run3 } } })
48976
- );
48977
- const sheets = data.length ? await saveSheets(
48978
- data,
48979
- options.out ? resolve15(options.out) : await mkdtemp3(join18(tmpdir3(), "pch-footage-"))
48980
- ) : [];
48981
- const next = data.length ? reviewCommand(name) : nextStep(name, "testing", test2).command;
48982
- out.result({ test: test2.id, state: test2.state, data: sheets, next }, () => {
48983
- if (!sheets.length) {
48984
- out.line(`${import_picocolors21.default.bold(name)} has no footage yet: its test is ${test2.state.replace("_", " ")}.`);
48985
- for (const result of test2.results) {
48986
- if (result.footage_error) out.line(plain(result.footage_error));
48987
- }
48988
- }
48989
- for (const sheet of sheets) {
48990
- out.line(`Run ${sheet.run}, ${sheet.from} to ${sheet.to}: ${sheet.path}`);
48991
- }
48992
- if (next) {
48993
- if (sheets.length) out.line("Look at every one, then send your review, with what you saw:");
48994
- out.line(`Next: ${next}`);
48995
- }
48996
- });
48997
- }
48998
- 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>"`;
48999
- async function saveSheets(sheets, folder) {
49000
- await mkdir9(folder, { recursive: true });
49001
- const saved = [];
49002
- for (const sheet of sheets) {
49003
- const answer = await fetch(sheet.url).catch(() => null);
49004
- const bytes = answer?.ok ? Buffer.from(await answer.arrayBuffer()) : null;
49005
- if (!bytes || createHash3("sha256").update(bytes).digest("hex") !== sheet.sha256) {
49006
- throw new CliError(
49007
- EXIT.error,
49008
- "download_failed",
49009
- `Run ${sheet.run}'s sheet ${sheet.index} didn't download intact. Try again.`
49010
- );
49011
- }
49012
- const path2 = join18(folder, `run-${sheet.run}-sheet-${sheet.index}.jpg`);
49013
- await writeFile12(path2, bytes);
49014
- const { url: _url2, ...kept } = sheet;
49015
- saved.push({ ...kept, path: path2 });
49016
- }
49017
- return saved;
49018
- }
49019
- async function testReview(ctx, ref, options) {
49020
- requireToken(ctx.target);
49021
- const release3 = parseReleaseRef(ref);
49022
- const moments = options.moment.map(parseMoment);
49023
- const test2 = await newest(ctx, release3);
49024
- const answer = await ctx.api.POST("/tests/{id}/review", {
49025
- params: { path: { id: test2.id } },
49026
- body: {
49027
- verdict: options.verdict,
49028
- plays: options.plays === "yes",
49029
- harmful: options.harmful === "yes",
49030
- summary: options.summary,
49031
- moments
49032
- }
49033
- });
49034
- if (answer.response.ok) return show3(ctx, release3, answer.data);
49035
- throw withNext(fromResponse(answer.response, answer.error), release3, test2);
49036
- }
49037
- function parseMoment(text) {
49038
- const match = /^(\d+)\s+(\d{1,3}:\d{2})\s+(\S.*)$/s.exec(text.trim());
49039
- if (!match) {
49040
- throw new CliError(
49041
- EXIT.usage,
49042
- "bad_moment",
49043
- `"${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".`
49044
- );
49045
- }
49046
- return { run: Number(match[1]), at: match[2], saw: match[3] };
49047
- }
49048
- async function testCancel(ctx, ref) {
49049
- requireToken(ctx.target);
49050
- const release3 = parseReleaseRef(ref);
49051
- const name = releaseName(release3);
49052
- const test2 = await newest(ctx, release3);
49053
- const answer = await ctx.api.POST("/tests/{id}/cancel", { params: { path: { id: test2.id } } });
49054
- if (!answer.response.ok) {
49055
- throw withNext(fromResponse(answer.response, answer.error), release3, test2);
49056
- }
49057
- const next = `pch test start ${name}`;
49058
- ctx.out.result({ ...answer.data, next }, () => {
49059
- ctx.out.line(`Cancelled ${import_picocolors21.default.bold(name)}'s test. Start another: ${next}`);
49060
- });
49061
- }
49062
- function withNext(failure2, release3, test2) {
49063
- if (failure2.code !== "test_state") return failure2;
49064
- const next = nextStep(releaseName(release3), "testing", test2);
49065
- return new CliError(failure2.exitCode, failure2.code, `${failure2.message}. ${sayNext(next)}`, {
49066
- next: next.command
49067
- });
49068
- }
49069
- function positive(value, flag) {
49070
- if (/^[1-9]\d*$/.test(value)) return Number(value);
49071
- throw new CliError(EXIT.usage, "bad_usage", `${flag} takes a whole number, not "${value}".`);
49072
- }
49073
- async function testAutostart(ctx, state) {
49074
- requireToken(ctx.target);
49075
- if (state !== "on" && state !== "off") {
49076
- throw new CliError(EXIT.usage, "bad_value", `Say on or off, not "${state}"`);
49077
- }
49078
- const setting = await unwrap(
49079
- ctx.api.PUT("/me/test-settings", { body: { starts_own_tests: state === "on", by: "agent" } })
49080
- );
49081
- ctx.out.result(setting, () => {
49082
- ctx.out.line(
49083
- 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."
49084
- );
49085
- });
49086
- }
49087
-
49088
49228
  // src/commands/search.ts
49089
- var import_picocolors22 = __toESM(require_picocolors(), 1);
49229
+ var import_picocolors23 = __toESM(require_picocolors(), 1);
49090
49230
  init_guide_words();
49091
49231
  var filter = (values) => values?.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean).join(",") || void 0;
49092
49232
  var licences = (values) => values.flatMap((value) => value.split(",")).map((value) => licenceId(value.trim()) ?? value.trim());
@@ -49124,13 +49264,13 @@ async function search(ctx, q, options) {
49124
49264
  out.result({ ...answer, ...aboutBlocks }, () => {
49125
49265
  if (!data.length) out.line("Nothing found.");
49126
49266
  for (const result of plain(data)) {
49127
- out.line(`${import_picocolors22.default.bold(result.handle)} ${import_picocolors22.default.dim(facts(result))}`);
49267
+ out.line(`${import_picocolors23.default.bold(result.handle)} ${import_picocolors23.default.dim(facts(result))}`);
49128
49268
  if (result.summary) out.line(` ${result.summary}`);
49129
49269
  if (result.demo) out.line(` Live demo: ${ctx.target.origin}/${result.owner}/${result.slug}`);
49130
- out.line(import_picocolors22.default.dim(` ${result.clone_url}`));
49270
+ out.line(import_picocolors23.default.dim(` ${result.clone_url}`));
49131
49271
  }
49132
49272
  if (blocks) out.line(BLOCK_WORDS);
49133
- if (cursor) out.line(import_picocolors22.default.dim("There are more; raise --limit."));
49273
+ if (cursor) out.line(import_picocolors23.default.dim("There are more; raise --limit."));
49134
49274
  });
49135
49275
  }
49136
49276
  function facts(result) {
@@ -49155,21 +49295,21 @@ async function categories(ctx, options) {
49155
49295
  const width = Math.max(0, ...data.map((category) => category.id.length));
49156
49296
  for (const category of data) {
49157
49297
  ctx.out.line(
49158
- `${import_picocolors22.default.bold(category.id.padEnd(width))} ${category.name} ${import_picocolors22.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`}`)}`
49159
49299
  );
49160
49300
  }
49161
49301
  });
49162
49302
  }
49163
49303
 
49164
49304
  // src/commands/update.ts
49165
- var import_picocolors23 = __toESM(require_picocolors(), 1);
49305
+ var import_picocolors24 = __toESM(require_picocolors(), 1);
49166
49306
  var import_semver3 = __toESM(require_semver2(), 1);
49167
49307
  import { spawnSync as spawnSync3 } from "node:child_process";
49168
49308
  import { createHash as createHash4 } from "node:crypto";
49169
49309
  import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
49170
49310
  import { chmod as chmod3, rename as rename2, rm as rm3, writeFile as writeFile13 } from "node:fs/promises";
49171
- import { basename as basename8, dirname as dirname7, join as join19 } from "node:path";
49172
- 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";
49173
49313
  import { gunzipSync } from "node:zlib";
49174
49314
  init_errors();
49175
49315
  init_version();
@@ -49312,7 +49452,7 @@ async function whenFree(step, wait2 = 200) {
49312
49452
  } catch (error62) {
49313
49453
  const code = error62.code ?? "";
49314
49454
  if (attempt >= 10 || !["EBUSY", "EPERM", "EACCES"].includes(code)) throw error62;
49315
- await sleep10(attempt * wait2);
49455
+ await sleep11(attempt * wait2);
49316
49456
  }
49317
49457
  }
49318
49458
  }
@@ -49322,7 +49462,7 @@ function removeOldExecutables(target = process.execPath) {
49322
49462
  const old = new RegExp(`^${basename8(target).replace(/\./g, "\\.")}\\.\\d+\\.old$`, "i");
49323
49463
  try {
49324
49464
  for (const name of readdirSync(folder)) {
49325
- if (old.test(name)) rmSync(join19(folder, name), { force: true });
49465
+ if (old.test(name)) rmSync(join20(folder, name), { force: true });
49326
49466
  }
49327
49467
  } catch {
49328
49468
  }
@@ -49332,7 +49472,7 @@ function startUpdateNotice(ctx, command, now = Date.now()) {
49332
49472
  const releases = releasesUrl(ctx.target.env);
49333
49473
  if (!how || !releases || ctx.out.json || QUIET.has(command)) return null;
49334
49474
  if (!process.stderr.isTTY || process.env.CI) return null;
49335
- const stamp = join19(dirname7(configPath()), "update-check.json");
49475
+ const stamp = join20(dirname7(configPath()), "update-check.json");
49336
49476
  try {
49337
49477
  const { checked_at } = JSON.parse(readFileSync(stamp, "utf8"));
49338
49478
  if (checked_at && now - Date.parse(checked_at) < DAY) return null;
@@ -49361,7 +49501,7 @@ function startUpdateNotice(ctx, command, now = Date.now()) {
49361
49501
  if (version2 && import_semver3.default.gt(version2, VERSION)) {
49362
49502
  process.stderr.write(
49363
49503
  `
49364
- ${import_picocolors23.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}
49365
49505
  `
49366
49506
  );
49367
49507
  }
@@ -49370,7 +49510,7 @@ ${import_picocolors23.default.dim("\u203A")} pch ${version2} is out (you have ${
49370
49510
  }
49371
49511
 
49372
49512
  // src/commands/validate.ts
49373
- var import_picocolors24 = __toESM(require_picocolors(), 1);
49513
+ var import_picocolors25 = __toESM(require_picocolors(), 1);
49374
49514
  init_errors();
49375
49515
  async function validate2(ctx, dir) {
49376
49516
  const project2 = await loadProject(dir);
@@ -49429,12 +49569,12 @@ async function validate2(ctx, dir) {
49429
49569
  notes
49430
49570
  };
49431
49571
  ctx.out.result(result, () => {
49432
- const mark = result.ok ? import_picocolors24.default.green("\u2713") : import_picocolors24.default.red("\u2717");
49572
+ const mark = result.ok ? import_picocolors25.default.green("\u2713") : import_picocolors25.default.red("\u2717");
49433
49573
  ctx.out.line(
49434
49574
  `${mark} ${file2.project}@${file2.version} \xB7 ${project2.kind} \xB7 ${result.trust_tier} \xB7 ${project2.files.length} files, ${formatSize(total)}`
49435
49575
  );
49436
- for (const issue2 of issues) ctx.out.line(` ${import_picocolors24.default.red(issue2.path || "\xB7")} ${issue2.message}`);
49437
- for (const note2 of notes) ctx.out.line(` ${import_picocolors24.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)}`);
49438
49578
  });
49439
49579
  return result.ok ? EXIT.ok : EXIT.usage;
49440
49580
  }
@@ -49469,8 +49609,7 @@ Start here:
49469
49609
  pch search "grappling hook" find a block or game to start from, then pch remix or pch clone it, and change it
49470
49610
  pch assets search "crate" --type model find a model, sound or texture, then pch assets add it
49471
49611
  pch new <you>/<game> --licence MIT make a repo with a patch.json, or pch init in a folder you have
49472
- pch validate, then pch publish
49473
- 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
49474
49613
  pch media add, pch project update the cover and the listing
49475
49614
 
49476
49615
  Add --json to any command for results an agent can read. Exit codes: 0 fine, 1 error, 2 bad usage,
@@ -49724,17 +49863,29 @@ program2.command("check [dir]").description(
49724
49863
  (ctx, dir, options) => check2(ctx, dir ?? ".", options)
49725
49864
  )
49726
49865
  );
49727
- program2.command("publish [dir]").description(
49728
- "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"
49729
49868
  ).addOption(
49730
49869
  new Option("--channel <channel>", `overrides patch.json. ${NOT_LOCKED_HELP}`).choices([
49731
49870
  "private",
49732
49871
  "unlisted",
49733
49872
  "public"
49734
49873
  ])
49735
- ).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(
49736
49887
  action(
49737
- (ctx, dir, options) => publish(ctx, dir ?? ".", options)
49888
+ (ctx, target, options) => publish(ctx, target ?? ".", options)
49738
49889
  )
49739
49890
  );
49740
49891
  var project = program2.command("project").description("Projects, addressed owner/slug");