@patchgg/cli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -29
- package/dist/cli.js +620 -442
- 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.
|
|
190
|
+
VERSION = true ? "0.3.1" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
191
191
|
}
|
|
192
192
|
});
|
|
193
193
|
|
|
@@ -229,7 +229,7 @@ ${BLOCK_WORDS} Its credit goes in your recipe's \`credits\`. If nothing fits, ma
|
|
|
229
229
|
## 2. Start
|
|
230
230
|
|
|
231
231
|
\`\`\`sh
|
|
232
|
-
${pch} login
|
|
232
|
+
${pch} login # agents: run it yourself, in the background, and pass on its link
|
|
233
233
|
${pch} agent install # gives your coding agent Patch's tools
|
|
234
234
|
${pch} new <you>/<game> --licence MIT # a repo with a patch.json, open unless you add --visibility private
|
|
235
235
|
${pch} init # or this, in a folder you already have
|
|
@@ -247,13 +247,13 @@ git add -A && git commit -m "..."
|
|
|
247
247
|
${pch} publish
|
|
248
248
|
\`\`\`
|
|
249
249
|
|
|
250
|
-
|
|
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}
|
|
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
|
-
|
|
256
|
+
That sends it to Patch's review and follows it until it's live. Run it yourself, wherever you run: in WSL, on Linux or a Mac, or on a server, the person's Patch Desktop takes the test through Patch, and pch says if they need to open it or press Start. 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,
|
|
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,
|
|
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,
|
|
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 ?? (
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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((
|
|
35648
|
+
return new Promise((resolve17) => {
|
|
35648
35649
|
const json2 = serializeMessage(message);
|
|
35649
35650
|
if (this._stdout.write(json2)) {
|
|
35650
|
-
|
|
35651
|
+
resolve17();
|
|
35651
35652
|
} else {
|
|
35652
|
-
this._stdout.once("drain",
|
|
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((
|
|
35672
|
-
local.onclose =
|
|
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();
|
|
@@ -35770,6 +35771,13 @@ if ((!process.stdout.isTTY || process.env.TERM === "dumb") && !process.env.FORCE
|
|
|
35770
35771
|
process.env.NO_COLOR = "1";
|
|
35771
35772
|
}
|
|
35772
35773
|
|
|
35774
|
+
// src/closed-pipes.ts
|
|
35775
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
35776
|
+
stream.on("error", (error62) => {
|
|
35777
|
+
if (error62.code !== "EPIPE") throw error62;
|
|
35778
|
+
});
|
|
35779
|
+
}
|
|
35780
|
+
|
|
35773
35781
|
// node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
|
|
35774
35782
|
var CommanderError = class extends Error {
|
|
35775
35783
|
/**
|
|
@@ -40330,7 +40338,8 @@ async function login(ctx, options) {
|
|
|
40330
40338
|
scopes: key.scopes,
|
|
40331
40339
|
git,
|
|
40332
40340
|
git_identity: gitIdentity,
|
|
40333
|
-
look_first: LOOK_FIRST
|
|
40341
|
+
look_first: LOOK_FIRST,
|
|
40342
|
+
next: `${pchOn(target.env)} agent install`
|
|
40334
40343
|
},
|
|
40335
40344
|
() => {
|
|
40336
40345
|
if (people) {
|
|
@@ -40352,9 +40361,7 @@ async function login(ctx, options) {
|
|
|
40352
40361
|
if (git.helper) out.line(`Git is signed in too: Patch repos at ${git.url} just work.`);
|
|
40353
40362
|
if (gitIdentity) out.line(gitIdentity);
|
|
40354
40363
|
out.line(LOOK_FIRST);
|
|
40355
|
-
out.line(
|
|
40356
|
-
"pch agent install gives your coding agent Patch's tools, and pch guide says how to publish."
|
|
40357
|
-
);
|
|
40364
|
+
out.line(agentNext(pchOn(target.env)));
|
|
40358
40365
|
}
|
|
40359
40366
|
);
|
|
40360
40367
|
}
|
|
@@ -40392,6 +40399,7 @@ async function loginWithKey(ctx) {
|
|
|
40392
40399
|
}
|
|
40393
40400
|
);
|
|
40394
40401
|
}
|
|
40402
|
+
var agentNext = (pch) => `Next: run ${pch} agent install now, which gives you Patch's MCP tools, one-step publish among them, then ask the person to restart you to load them. ${pch} guide says how to make and publish a game.`;
|
|
40395
40403
|
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.";
|
|
40396
40404
|
function signedIn(ctx, headline, { git, notes }) {
|
|
40397
40405
|
const { out, target } = ctx;
|
|
@@ -40456,14 +40464,14 @@ ${drawn.map((line) => ` ${line}`).join("\n")}
|
|
|
40456
40464
|
block.write(` Opening ${page} in your browser: check the code matches, then approve.`);
|
|
40457
40465
|
block.write(import_picocolors3.default.dim(` Didn't open? Go to ${device.verification_uri} and enter the code.`));
|
|
40458
40466
|
} else {
|
|
40459
|
-
block.write(` Go to ${import_picocolors3.default.bold(device.
|
|
40467
|
+
block.write(` Go to ${import_picocolors3.default.bold(device.verification_uri_complete)} and approve the code.`);
|
|
40460
40468
|
}
|
|
40461
40469
|
block.write();
|
|
40462
40470
|
} else {
|
|
40463
40471
|
process.stderr.write(
|
|
40464
40472
|
`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.
|
|
40465
40473
|
|
|
40466
|
-
To sign in,
|
|
40474
|
+
To sign in, the person opens this link, which has the code in it, checks the code is ${import_picocolors3.default.bold(code)} and approves it: ${import_picocolors3.default.bold(device.verification_uri_complete)}
|
|
40467
40475
|
|
|
40468
40476
|
`
|
|
40469
40477
|
);
|
|
@@ -41693,7 +41701,12 @@ var recipeSchema = external_exports.object({
|
|
|
41693
41701
|
).max(4).default([]).describe("Config files and the values to set in them."),
|
|
41694
41702
|
test: external_exports.object({
|
|
41695
41703
|
args: external_exports.array(shortLine(400)).max(16).optional(),
|
|
41696
|
-
checks: external_exports.array(
|
|
41704
|
+
checks: external_exports.array(
|
|
41705
|
+
external_exports.string().regex(
|
|
41706
|
+
checkName,
|
|
41707
|
+
"must be lowercase letters, digits and dashes, such as player-moves"
|
|
41708
|
+
)
|
|
41709
|
+
).max(12).optional().describe(
|
|
41697
41710
|
`Checks the release reports itself while it's tested: a browser game opened with ?patch-test=1 logs "pass <name>" or "fail <name>: why".`
|
|
41698
41711
|
),
|
|
41699
41712
|
minutes: external_exports.number().int().min(1).max(15).optional(),
|
|
@@ -42631,8 +42644,8 @@ This project is on [Patch](https://patch.gg), where people publish and play brow
|
|
|
42631
42644
|
|
|
42632
42645
|
- **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
42646
|
- **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
|
|
42635
|
-
- **Publish it.**
|
|
42647
|
+
- **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.
|
|
42648
|
+
- **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
42649
|
- **Push it.** Plain \`git push\` works. If git says HTTP 413, that's over 100 MB: run \`pch push\`, which takes any size.
|
|
42637
42650
|
- **The rest.** \`pch guide\` has everything else.
|
|
42638
42651
|
`;
|
|
@@ -43048,13 +43061,15 @@ function mcpServer({ target }, options = {}, found = {}) {
|
|
|
43048
43061
|
];
|
|
43049
43062
|
const name = target.env === "production" ? "patch" : `patch-${target.env}`;
|
|
43050
43063
|
const platform = found.platform ?? process.platform;
|
|
43051
|
-
if (found.
|
|
43052
|
-
|
|
43064
|
+
if (found.executable ?? isExecutable) return { name, command: process.execPath, args: flags };
|
|
43065
|
+
const pch = found.pchPath === void 0 ? whereOnPath("pch") : found.pchPath;
|
|
43066
|
+
if (pch) {
|
|
43067
|
+
return platform === "win32" ? { name, command: "cmd", args: ["/c", "pch", ...flags] } : { name, command: pch, args: flags };
|
|
43053
43068
|
}
|
|
43054
43069
|
return {
|
|
43055
43070
|
name,
|
|
43056
43071
|
command: process.execPath,
|
|
43057
|
-
args:
|
|
43072
|
+
args: [...process.execArgv, resolve5(process.argv[1]), ...flags]
|
|
43058
43073
|
};
|
|
43059
43074
|
}
|
|
43060
43075
|
function commandLine(server) {
|
|
@@ -43083,8 +43098,18 @@ function codexHas(server) {
|
|
|
43083
43098
|
return { status: listed.stdout?.includes(server.name) ? 0 : 1 };
|
|
43084
43099
|
}
|
|
43085
43100
|
function onPath(bin) {
|
|
43101
|
+
return whereOnPath(bin) !== null;
|
|
43102
|
+
}
|
|
43103
|
+
function whereOnPath(bin) {
|
|
43086
43104
|
const names = process.platform === "win32" ? [`${bin}.cmd`, `${bin}.exe`, `${bin}.bat`, bin] : [bin];
|
|
43087
|
-
|
|
43105
|
+
for (const folder of (process.env.PATH ?? "").split(delimiter)) {
|
|
43106
|
+
if (!folder) continue;
|
|
43107
|
+
for (const name of names) {
|
|
43108
|
+
const path2 = resolve5(folder, name);
|
|
43109
|
+
if (existsSync2(path2)) return path2;
|
|
43110
|
+
}
|
|
43111
|
+
}
|
|
43112
|
+
return null;
|
|
43088
43113
|
}
|
|
43089
43114
|
function run(bin, args2) {
|
|
43090
43115
|
const shell = process.platform === "win32";
|
|
@@ -45442,7 +45467,7 @@ var LABELS = {
|
|
|
45442
45467
|
warn: "look ",
|
|
45443
45468
|
cant_check: "can't"
|
|
45444
45469
|
};
|
|
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
|
|
45470
|
+
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
45471
|
async function findGame(dir) {
|
|
45447
45472
|
const root = resolve8(dir);
|
|
45448
45473
|
if (existsSync4(join12(root, "patch.json"))) {
|
|
@@ -45450,7 +45475,7 @@ async function findGame(dir) {
|
|
|
45450
45475
|
if (project2.recipe.mode !== "browser") {
|
|
45451
45476
|
throw usage2(
|
|
45452
45477
|
"not_a_browser_game",
|
|
45453
|
-
`This is a ${project2.kind}, which runs in its game, so a browser can't see it. pch
|
|
45478
|
+
`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
45479
|
);
|
|
45455
45480
|
}
|
|
45456
45481
|
const launch2 = project2.recipe.launch;
|
|
@@ -45751,38 +45776,28 @@ function reviewing(progress) {
|
|
|
45751
45776
|
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
45777
|
}
|
|
45753
45778
|
function nextStep(release3, status2, test2, cloud) {
|
|
45754
|
-
const
|
|
45755
|
-
const startOne2 = `pch test start ${release3}`;
|
|
45779
|
+
const carryOn = `pch publish ${release3}`;
|
|
45756
45780
|
switch (status2) {
|
|
45757
45781
|
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
|
-
};
|
|
45782
|
+
return { say: "It's uploaded, and its test comes next.", command: carryOn };
|
|
45762
45783
|
case "testing":
|
|
45763
45784
|
switch (test2?.state) {
|
|
45764
45785
|
case "waiting":
|
|
45765
|
-
return {
|
|
45766
|
-
say: "Its test is waiting for you: press Start on it in Patch Desktop.",
|
|
45767
|
-
command: follow4
|
|
45768
|
-
};
|
|
45786
|
+
return { say: "Its test is waiting to start in Patch Desktop.", command: carryOn };
|
|
45769
45787
|
case "running":
|
|
45770
45788
|
return {
|
|
45771
45789
|
say: `Patch Desktop${test2.pc_name ? ` on ${test2.pc_name}` : ""} is playing it.`,
|
|
45772
|
-
command:
|
|
45790
|
+
command: carryOn
|
|
45773
45791
|
};
|
|
45774
45792
|
case "recorded":
|
|
45775
45793
|
return {
|
|
45776
|
-
say: "It
|
|
45777
|
-
command:
|
|
45794
|
+
say: "It's recorded. Look at the footage, then send your review.",
|
|
45795
|
+
command: carryOn
|
|
45778
45796
|
};
|
|
45779
45797
|
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
|
-
};
|
|
45798
|
+
return { say: "It passed its test.", command: carryOn };
|
|
45784
45799
|
default:
|
|
45785
|
-
return { say: "It needs a test that passes.", command:
|
|
45800
|
+
return { say: "It needs a test that passes.", command: carryOn };
|
|
45786
45801
|
}
|
|
45787
45802
|
case "rejected":
|
|
45788
45803
|
return {
|
|
@@ -45791,10 +45806,7 @@ function nextStep(release3, status2, test2, cloud) {
|
|
|
45791
45806
|
};
|
|
45792
45807
|
case "checking":
|
|
45793
45808
|
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
|
-
};
|
|
45809
|
+
return { say: reviewing(cloud) ?? "Patch is reviewing it.", command: carryOn };
|
|
45798
45810
|
case "in_review":
|
|
45799
45811
|
return {
|
|
45800
45812
|
say: "It's waiting for a staff member at Patch, and nothing tells you when that's done: pch status says.",
|
|
@@ -47454,7 +47466,7 @@ var PATCH_KEYS = ["PCH_TOKEN", "PCH_GIT_KEY", PUSH_KEY_VARIABLE];
|
|
|
47454
47466
|
function run2(dir, args2, signIn = NO_SIGN_IN) {
|
|
47455
47467
|
const env = { ...process.env };
|
|
47456
47468
|
if (signIn.own) for (const name of PATCH_KEYS) delete env[name];
|
|
47457
|
-
return new Promise((
|
|
47469
|
+
return new Promise((resolve17) => {
|
|
47458
47470
|
execFile(
|
|
47459
47471
|
"git",
|
|
47460
47472
|
["-C", dir, ...signIn.args, ...args2],
|
|
@@ -47464,7 +47476,7 @@ function run2(dir, args2, signIn = NO_SIGN_IN) {
|
|
|
47464
47476
|
},
|
|
47465
47477
|
(error62, out, err) => {
|
|
47466
47478
|
const code = error62?.code;
|
|
47467
|
-
|
|
47479
|
+
resolve17({ code: error62 ? typeof code === "number" ? code : -1 : 0, out, err });
|
|
47468
47480
|
}
|
|
47469
47481
|
);
|
|
47470
47482
|
});
|
|
@@ -48382,11 +48394,469 @@ async function mediaOrder(ctx, ref, ids) {
|
|
|
48382
48394
|
}
|
|
48383
48395
|
|
|
48384
48396
|
// src/commands/publish.ts
|
|
48385
|
-
var
|
|
48397
|
+
var import_picocolors22 = __toESM(require_picocolors(), 1);
|
|
48386
48398
|
var import_semver2 = __toESM(require_semver2(), 1);
|
|
48387
48399
|
import { open as open2, readFile as readFile15 } from "node:fs/promises";
|
|
48388
48400
|
init_errors();
|
|
48389
48401
|
init_guide_words();
|
|
48402
|
+
|
|
48403
|
+
// src/commands/publish-flow.ts
|
|
48404
|
+
var import_picocolors21 = __toESM(require_picocolors(), 1);
|
|
48405
|
+
import { mkdtemp as mkdtemp4 } from "node:fs/promises";
|
|
48406
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
48407
|
+
import { join as join19, resolve as resolve16 } from "node:path";
|
|
48408
|
+
init_errors();
|
|
48409
|
+
|
|
48410
|
+
// src/commands/release-test.ts
|
|
48411
|
+
var import_picocolors20 = __toESM(require_picocolors(), 1);
|
|
48412
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
48413
|
+
import { mkdir as mkdir9, mkdtemp as mkdtemp3, writeFile as writeFile12 } from "node:fs/promises";
|
|
48414
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
48415
|
+
import { join as join18, resolve as resolve15 } from "node:path";
|
|
48416
|
+
import { setTimeout as sleep9 } from "node:timers/promises";
|
|
48417
|
+
init_errors();
|
|
48418
|
+
var HOLD_SECONDS = 50;
|
|
48419
|
+
var ASK_EVERY_MS2 = 5e3;
|
|
48420
|
+
var OPEN = /* @__PURE__ */ new Set(["waiting", "running", "recorded"]);
|
|
48421
|
+
var GOING = /* @__PURE__ */ new Set(["waiting", "running"]);
|
|
48422
|
+
var REVIEW_ROUTE = "/projects/{owner}/{slug}/releases/{version}/review";
|
|
48423
|
+
var reviewOf = (ctx, release3) => ctx.api.GET(REVIEW_ROUTE, { params: { path: release3 } });
|
|
48424
|
+
async function testStart(ctx, ref, options) {
|
|
48425
|
+
requireToken(ctx.target);
|
|
48426
|
+
const release3 = parseReleaseRef(ref);
|
|
48427
|
+
const wait2 = waitOf(options.wait);
|
|
48428
|
+
const before = await reviewOf(ctx, release3);
|
|
48429
|
+
const open3 = before.data?.test && OPEN.has(before.data.test.state) ? before.data.test : null;
|
|
48430
|
+
const test2 = open3 ?? await startOne(ctx, release3, before.data?.status);
|
|
48431
|
+
return show3(ctx, release3, wait2 === null ? test2 : await follow3(ctx, test2, wait2), {
|
|
48432
|
+
started: !open3,
|
|
48433
|
+
followed: wait2 !== null
|
|
48434
|
+
});
|
|
48435
|
+
}
|
|
48436
|
+
async function startOne(ctx, release3, status2) {
|
|
48437
|
+
const name = releaseName(release3);
|
|
48438
|
+
const answer = await ctx.api.POST("/projects/{owner}/{slug}/releases/{version}/tests", {
|
|
48439
|
+
params: { path: release3 }
|
|
48440
|
+
});
|
|
48441
|
+
if (answer.response.ok) return answer.data;
|
|
48442
|
+
const failure2 = fromResponse(answer.response, answer.error);
|
|
48443
|
+
if (failure2.code === "files_failed") {
|
|
48444
|
+
const { findings } = failure2.details;
|
|
48445
|
+
throw filesFailed(failure2.message, findings);
|
|
48446
|
+
}
|
|
48447
|
+
if (failure2.code === "not_testable") {
|
|
48448
|
+
const next = `pch release submit ${name}`;
|
|
48449
|
+
throw new CliError(
|
|
48450
|
+
failure2.exitCode,
|
|
48451
|
+
failure2.code,
|
|
48452
|
+
`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}`,
|
|
48453
|
+
{ next }
|
|
48454
|
+
);
|
|
48455
|
+
}
|
|
48456
|
+
if (failure2.code === "not_testing" && status2) {
|
|
48457
|
+
const next = nextStep(name, status2, null);
|
|
48458
|
+
throw new CliError(
|
|
48459
|
+
failure2.exitCode,
|
|
48460
|
+
failure2.code,
|
|
48461
|
+
`${name} is ${status2}, so it can't be tested. ${sayNext(next)}`,
|
|
48462
|
+
{ next: next.command }
|
|
48463
|
+
);
|
|
48464
|
+
}
|
|
48465
|
+
throw failure2;
|
|
48466
|
+
}
|
|
48467
|
+
async function testView(ctx, ref, options) {
|
|
48468
|
+
requireToken(ctx.target);
|
|
48469
|
+
const release3 = parseReleaseRef(ref);
|
|
48470
|
+
const wait2 = waitOf(options.wait);
|
|
48471
|
+
const test2 = await newest(ctx, release3);
|
|
48472
|
+
return show3(ctx, release3, wait2 === null ? test2 : await follow3(ctx, test2, wait2), {
|
|
48473
|
+
followed: wait2 !== null
|
|
48474
|
+
});
|
|
48475
|
+
}
|
|
48476
|
+
async function newest(ctx, release3) {
|
|
48477
|
+
const review = await unwrap(reviewOf(ctx, release3));
|
|
48478
|
+
if (review.test) return review.test;
|
|
48479
|
+
const name = releaseName(release3);
|
|
48480
|
+
const next = `pch test start ${name}`;
|
|
48481
|
+
throw new CliError(EXIT.error, "not_found", `${name} has no test yet. Start one: ${next}`, {
|
|
48482
|
+
next
|
|
48483
|
+
});
|
|
48484
|
+
}
|
|
48485
|
+
async function follow3(ctx, test2, seconds2) {
|
|
48486
|
+
const deadline = Date.now() + seconds2 * 1e3;
|
|
48487
|
+
let current = test2;
|
|
48488
|
+
let said2 = "";
|
|
48489
|
+
while (GOING.has(current.state) && Date.now() < deadline) {
|
|
48490
|
+
const now = whatsHappening(current);
|
|
48491
|
+
if (now !== said2) ctx.out.step(now);
|
|
48492
|
+
said2 = now;
|
|
48493
|
+
if (current.needs_person) await sleep9(Math.min(ASK_EVERY_MS2, deadline - Date.now()));
|
|
48494
|
+
const left = Math.ceil((deadline - Date.now()) / 1e3);
|
|
48495
|
+
current = await unwrap(
|
|
48496
|
+
ctx.api.GET("/tests/{id}", {
|
|
48497
|
+
params: {
|
|
48498
|
+
path: { id: current.id },
|
|
48499
|
+
query: { wait: Math.max(0, Math.min(HOLD_SECONDS, left)) }
|
|
48500
|
+
}
|
|
48501
|
+
})
|
|
48502
|
+
);
|
|
48503
|
+
}
|
|
48504
|
+
return current;
|
|
48505
|
+
}
|
|
48506
|
+
function whatsHappening(test2) {
|
|
48507
|
+
if (test2.state === "running") {
|
|
48508
|
+
const where = test2.pc_name ? ` on ${plain(test2.pc_name)}` : "";
|
|
48509
|
+
const phase = test2.progress?.phase;
|
|
48510
|
+
return phase && phase !== "playing" ? `Patch Desktop${where} is ${plain(phase)} it` : `Patch Desktop${where} is playing it`;
|
|
48511
|
+
}
|
|
48512
|
+
if (test2.needs_person) return plain(test2.needs_person.ask);
|
|
48513
|
+
return "Waiting for you to press Start in Patch Desktop";
|
|
48514
|
+
}
|
|
48515
|
+
async function show3(ctx, release3, test2, { followed, ...extra } = {}) {
|
|
48516
|
+
const { out } = ctx;
|
|
48517
|
+
const name = releaseName(release3);
|
|
48518
|
+
const next = nextStep(name, test2.state === "failed" ? "rejected" : "testing", test2);
|
|
48519
|
+
const command = next.command;
|
|
48520
|
+
const play2 = test2.state === "passed" ? await playFirst(ctx, release3) : null;
|
|
48521
|
+
const ask = test2.state === "waiting" ? askThePerson(test2) : play2;
|
|
48522
|
+
const noteAsk = test2.note_ask?.replaceAll("suggest_note", "pch note suggest");
|
|
48523
|
+
const feedbackAsk = test2.feedback_ask ? {
|
|
48524
|
+
...test2.feedback_ask,
|
|
48525
|
+
ask: test2.feedback_ask.ask.replaceAll("send_feedback", "pch feedback send")
|
|
48526
|
+
} : void 0;
|
|
48527
|
+
const shown = {
|
|
48528
|
+
...test2,
|
|
48529
|
+
...extra,
|
|
48530
|
+
next: command,
|
|
48531
|
+
...noteAsk ? { note_ask: noteAsk } : {},
|
|
48532
|
+
...feedbackAsk ? { feedback_ask: feedbackAsk } : {}
|
|
48533
|
+
};
|
|
48534
|
+
out.result({ ...shown, ...ask ? { ask } : {} }, () => {
|
|
48535
|
+
const bold = import_picocolors20.default.bold(name);
|
|
48536
|
+
if (test2.state === "waiting") {
|
|
48537
|
+
out.line(
|
|
48538
|
+
!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.`
|
|
48539
|
+
);
|
|
48540
|
+
if (test2.needs_person) {
|
|
48541
|
+
out.line(plain(test2.needs_person.why));
|
|
48542
|
+
out.line(plain(test2.needs_person.ask));
|
|
48543
|
+
}
|
|
48544
|
+
} else if (test2.state === "running") {
|
|
48545
|
+
out.line(`${bold}: ${whatsHappening(test2)}.`);
|
|
48546
|
+
} else if (test2.state === "recorded") {
|
|
48547
|
+
out.line(
|
|
48548
|
+
`${bold}: it has been played and recorded. Look at the footage, then send your review.`
|
|
48549
|
+
);
|
|
48550
|
+
} else if (test2.state === "passed") {
|
|
48551
|
+
out.line(`${bold}: ${import_picocolors20.default.green("its test passed.")} It's still private.`);
|
|
48552
|
+
if (test2.review) out.line(plain(test2.review.summary));
|
|
48553
|
+
if (play2) out.line(play2);
|
|
48554
|
+
out.line(
|
|
48555
|
+
"Publish it once you're happy it's ready: that's your yes, and nothing does it for you."
|
|
48556
|
+
);
|
|
48557
|
+
} else if (test2.state === "failed") {
|
|
48558
|
+
out.line(`${bold}: ${import_picocolors20.default.red("its test failed")}, so this version can't be published.`);
|
|
48559
|
+
showWhatFailed(ctx, test2);
|
|
48560
|
+
out.line(`pch status ${name} says how to fix it.`);
|
|
48561
|
+
out.line("Then move the version on in patch.json and publish that.");
|
|
48562
|
+
} else if (test2.state === "couldnt_run") {
|
|
48563
|
+
out.line(`${bold}: ${import_picocolors20.default.yellow("its test couldn't run.")}`);
|
|
48564
|
+
if (test2.outcome) out.line(plain(test2.outcome.reason));
|
|
48565
|
+
} else {
|
|
48566
|
+
out.line(`${bold}: its test was cancelled.`);
|
|
48567
|
+
}
|
|
48568
|
+
if (noteAsk) out.line(plain(noteAsk));
|
|
48569
|
+
if (feedbackAsk) out.line(plain(feedbackAsk.ask));
|
|
48570
|
+
if (followed && GOING.has(test2.state)) {
|
|
48571
|
+
out.line("It isn't over, and nothing's wrong: that's as long as --wait follows it here.");
|
|
48572
|
+
}
|
|
48573
|
+
if (command) out.line(`Next: ${command}`);
|
|
48574
|
+
if (OPEN.has(test2.state) && !extra.started) {
|
|
48575
|
+
out.line(import_picocolors20.default.dim(`To start over: pch test cancel ${name}, then pch test start ${name}`));
|
|
48576
|
+
}
|
|
48577
|
+
});
|
|
48578
|
+
if (test2.state === "failed") return EXIT.blocked;
|
|
48579
|
+
if (test2.state === "couldnt_run" || test2.state === "cancelled") return EXIT.error;
|
|
48580
|
+
return EXIT.ok;
|
|
48581
|
+
}
|
|
48582
|
+
function askThePerson(test2) {
|
|
48583
|
+
return test2.needs_person?.ask ?? null;
|
|
48584
|
+
}
|
|
48585
|
+
function showWhatFailed({ out }, test2) {
|
|
48586
|
+
for (const run3 of test2.results) {
|
|
48587
|
+
for (const check3 of run3.checks) {
|
|
48588
|
+
if (check3.result !== "fail") continue;
|
|
48589
|
+
const detail = check3.detail ? ` (${plain(check3.detail)})` : "";
|
|
48590
|
+
out.line(` Run ${run3.run}: ${plain(check3.name)}${detail}`);
|
|
48591
|
+
}
|
|
48592
|
+
}
|
|
48593
|
+
if (test2.review) out.line(` Its review: ${plain(test2.review.summary)}`);
|
|
48594
|
+
}
|
|
48595
|
+
async function testFootage(ctx, ref, options) {
|
|
48596
|
+
requireToken(ctx.target);
|
|
48597
|
+
const { out } = ctx;
|
|
48598
|
+
const release3 = parseReleaseRef(ref);
|
|
48599
|
+
const name = releaseName(release3);
|
|
48600
|
+
const run3 = options.run === void 0 ? void 0 : positive(options.run, "--run");
|
|
48601
|
+
const test2 = await newest(ctx, release3);
|
|
48602
|
+
const { data } = await unwrap(
|
|
48603
|
+
ctx.api.GET("/tests/{id}/footage", { params: { path: { id: test2.id }, query: { run: run3 } } })
|
|
48604
|
+
);
|
|
48605
|
+
const sheets = data.length ? await saveSheets(
|
|
48606
|
+
data,
|
|
48607
|
+
options.out ? resolve15(options.out) : await mkdtemp3(join18(tmpdir3(), "pch-footage-"))
|
|
48608
|
+
) : [];
|
|
48609
|
+
const next = data.length ? reviewCommand(name) : nextStep(name, "testing", test2).command;
|
|
48610
|
+
out.result({ test: test2.id, state: test2.state, data: sheets, next }, () => {
|
|
48611
|
+
if (!sheets.length) {
|
|
48612
|
+
out.line(`${import_picocolors20.default.bold(name)} has no footage yet: its test is ${test2.state.replace("_", " ")}.`);
|
|
48613
|
+
for (const result of test2.results) {
|
|
48614
|
+
if (result.footage_error) out.line(plain(result.footage_error));
|
|
48615
|
+
}
|
|
48616
|
+
}
|
|
48617
|
+
for (const sheet of sheets) {
|
|
48618
|
+
out.line(`Run ${sheet.run}, ${sheet.from} to ${sheet.to}: ${sheet.path}`);
|
|
48619
|
+
}
|
|
48620
|
+
if (next) {
|
|
48621
|
+
if (sheets.length) out.line("Look at every one, then send your review, with what you saw:");
|
|
48622
|
+
out.line(`Next: ${next}`);
|
|
48623
|
+
}
|
|
48624
|
+
});
|
|
48625
|
+
}
|
|
48626
|
+
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>"`;
|
|
48627
|
+
async function saveSheets(sheets, folder) {
|
|
48628
|
+
await mkdir9(folder, { recursive: true });
|
|
48629
|
+
const saved = [];
|
|
48630
|
+
for (const sheet of sheets) {
|
|
48631
|
+
const answer = await fetch(sheet.url).catch(() => null);
|
|
48632
|
+
const bytes = answer?.ok ? Buffer.from(await answer.arrayBuffer()) : null;
|
|
48633
|
+
if (!bytes || createHash3("sha256").update(bytes).digest("hex") !== sheet.sha256) {
|
|
48634
|
+
throw new CliError(
|
|
48635
|
+
EXIT.error,
|
|
48636
|
+
"download_failed",
|
|
48637
|
+
`Run ${sheet.run}'s sheet ${sheet.index} didn't download intact. Try again.`
|
|
48638
|
+
);
|
|
48639
|
+
}
|
|
48640
|
+
const path2 = join18(folder, `run-${sheet.run}-sheet-${sheet.index}.jpg`);
|
|
48641
|
+
await writeFile12(path2, bytes);
|
|
48642
|
+
const { url: _url2, ...kept } = sheet;
|
|
48643
|
+
saved.push({ ...kept, path: path2 });
|
|
48644
|
+
}
|
|
48645
|
+
return saved;
|
|
48646
|
+
}
|
|
48647
|
+
async function testReview(ctx, ref, options) {
|
|
48648
|
+
requireToken(ctx.target);
|
|
48649
|
+
const release3 = parseReleaseRef(ref);
|
|
48650
|
+
const moments = options.moment.map(parseMoment);
|
|
48651
|
+
const test2 = await newest(ctx, release3);
|
|
48652
|
+
const answer = await ctx.api.POST("/tests/{id}/review", {
|
|
48653
|
+
params: { path: { id: test2.id } },
|
|
48654
|
+
body: {
|
|
48655
|
+
verdict: options.verdict,
|
|
48656
|
+
plays: options.plays === "yes",
|
|
48657
|
+
harmful: options.harmful === "yes",
|
|
48658
|
+
summary: options.summary,
|
|
48659
|
+
moments
|
|
48660
|
+
}
|
|
48661
|
+
});
|
|
48662
|
+
if (answer.response.ok) return show3(ctx, release3, answer.data);
|
|
48663
|
+
throw withNext(fromResponse(answer.response, answer.error), release3, test2);
|
|
48664
|
+
}
|
|
48665
|
+
function parseMoment(text) {
|
|
48666
|
+
const match = /^(\d+)\s+(\d{1,3}:\d{2})\s+(\S.*)$/s.exec(text.trim());
|
|
48667
|
+
if (!match) {
|
|
48668
|
+
throw new CliError(
|
|
48669
|
+
EXIT.usage,
|
|
48670
|
+
"bad_moment",
|
|
48671
|
+
`"${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".`
|
|
48672
|
+
);
|
|
48673
|
+
}
|
|
48674
|
+
return { run: Number(match[1]), at: match[2], saw: match[3] };
|
|
48675
|
+
}
|
|
48676
|
+
async function testCancel(ctx, ref) {
|
|
48677
|
+
requireToken(ctx.target);
|
|
48678
|
+
const release3 = parseReleaseRef(ref);
|
|
48679
|
+
const name = releaseName(release3);
|
|
48680
|
+
const test2 = await newest(ctx, release3);
|
|
48681
|
+
const answer = await ctx.api.POST("/tests/{id}/cancel", { params: { path: { id: test2.id } } });
|
|
48682
|
+
if (!answer.response.ok) {
|
|
48683
|
+
throw withNext(fromResponse(answer.response, answer.error), release3, test2);
|
|
48684
|
+
}
|
|
48685
|
+
const next = `pch test start ${name}`;
|
|
48686
|
+
ctx.out.result({ ...answer.data, next }, () => {
|
|
48687
|
+
ctx.out.line(`Cancelled ${import_picocolors20.default.bold(name)}'s test. Start another: ${next}`);
|
|
48688
|
+
});
|
|
48689
|
+
}
|
|
48690
|
+
function withNext(failure2, release3, test2) {
|
|
48691
|
+
if (failure2.code !== "test_state") return failure2;
|
|
48692
|
+
const next = nextStep(releaseName(release3), "testing", test2);
|
|
48693
|
+
return new CliError(failure2.exitCode, failure2.code, `${failure2.message}. ${sayNext(next)}`, {
|
|
48694
|
+
next: next.command
|
|
48695
|
+
});
|
|
48696
|
+
}
|
|
48697
|
+
function positive(value, flag) {
|
|
48698
|
+
if (/^[1-9]\d*$/.test(value)) return Number(value);
|
|
48699
|
+
throw new CliError(EXIT.usage, "bad_usage", `${flag} takes a whole number, not "${value}".`);
|
|
48700
|
+
}
|
|
48701
|
+
async function testAutostart(ctx, state) {
|
|
48702
|
+
requireToken(ctx.target);
|
|
48703
|
+
if (state !== "on" && state !== "off") {
|
|
48704
|
+
throw new CliError(EXIT.usage, "bad_value", `Say on or off, not "${state}"`);
|
|
48705
|
+
}
|
|
48706
|
+
const setting = await unwrap(
|
|
48707
|
+
ctx.api.PUT("/me/test-settings", { body: { starts_own_tests: state === "on", by: "agent" } })
|
|
48708
|
+
);
|
|
48709
|
+
ctx.out.result(setting, () => {
|
|
48710
|
+
ctx.out.line(
|
|
48711
|
+
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."
|
|
48712
|
+
);
|
|
48713
|
+
});
|
|
48714
|
+
}
|
|
48715
|
+
|
|
48716
|
+
// src/commands/publish-flow.ts
|
|
48717
|
+
var ENDS = /* @__PURE__ */ new Set(["your_review", "ready", "live", "stopped", "uploading"]);
|
|
48718
|
+
var PERSON_EVERY_MS = 5e3;
|
|
48719
|
+
function followFor(wait2) {
|
|
48720
|
+
if (wait2 === void 0) {
|
|
48721
|
+
return process.stdout.isTTY ? Number.POSITIVE_INFINITY : UNATTENDED_WAIT_SECONDS;
|
|
48722
|
+
}
|
|
48723
|
+
if (/^\d+$/.test(wait2)) return Number(wait2);
|
|
48724
|
+
throw new CliError(EXIT.usage, "bad_wait", `--wait takes a number of seconds, not "${wait2}".`);
|
|
48725
|
+
}
|
|
48726
|
+
function reviewOf2(flags) {
|
|
48727
|
+
if (flags.review === void 0) return void 0;
|
|
48728
|
+
if (flags.review !== "pass" && flags.review !== "fail") {
|
|
48729
|
+
throw new CliError(EXIT.usage, "bad_value", `--review is pass or fail, not "${flags.review}".`);
|
|
48730
|
+
}
|
|
48731
|
+
const moments = (flags.moment ?? []).map(parseMoment);
|
|
48732
|
+
if (!flags.summary?.trim() || moments.length < 2) {
|
|
48733
|
+
throw new CliError(
|
|
48734
|
+
EXIT.usage,
|
|
48735
|
+
"review_incomplete",
|
|
48736
|
+
`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>")}`
|
|
48737
|
+
);
|
|
48738
|
+
}
|
|
48739
|
+
return {
|
|
48740
|
+
verdict: flags.review,
|
|
48741
|
+
plays: flags.review === "pass",
|
|
48742
|
+
harmful: Boolean(flags.harmful),
|
|
48743
|
+
summary: flags.summary,
|
|
48744
|
+
moments
|
|
48745
|
+
};
|
|
48746
|
+
}
|
|
48747
|
+
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>"`;
|
|
48748
|
+
function lineOf(step) {
|
|
48749
|
+
if (step.stage === "testing" && step.needs_person) return "Testing on your PC: waiting for you";
|
|
48750
|
+
return step.line;
|
|
48751
|
+
}
|
|
48752
|
+
var sleep10 = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
48753
|
+
async function followRelease(ctx, ref, options = {}) {
|
|
48754
|
+
requireToken(ctx.target);
|
|
48755
|
+
const { out, api } = ctx;
|
|
48756
|
+
const review = reviewOf2(options);
|
|
48757
|
+
const wait2 = options.wait ?? followFor(void 0);
|
|
48758
|
+
const deadline = (options.since ?? Date.now()) + wait2 * 1e3;
|
|
48759
|
+
let body = {
|
|
48760
|
+
...review ? { review } : {},
|
|
48761
|
+
...options.testAgain ? { test_again: true } : {}
|
|
48762
|
+
};
|
|
48763
|
+
const people = forAPerson(out.json, runningIn());
|
|
48764
|
+
let said2 = "";
|
|
48765
|
+
let asked = "";
|
|
48766
|
+
let step;
|
|
48767
|
+
for (; ; ) {
|
|
48768
|
+
const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
|
|
48769
|
+
const answer = await api.POST("/projects/{owner}/{slug}/releases/{version}/publish", {
|
|
48770
|
+
params: { path: ref, query: { wait: Math.min(50, left) } },
|
|
48771
|
+
body
|
|
48772
|
+
});
|
|
48773
|
+
if (!answer.response.ok) throw fromResponse(answer.response, answer.error);
|
|
48774
|
+
step = answer.data;
|
|
48775
|
+
body = void 0;
|
|
48776
|
+
const stops = ENDS.has(step.stage) || step.status === "in_review";
|
|
48777
|
+
const line = lineOf(step);
|
|
48778
|
+
if (line !== said2 && !stops) out.progress(line, { stage: step.stage });
|
|
48779
|
+
said2 = line;
|
|
48780
|
+
if (step.needs_person && step.needs_person.ask !== asked) {
|
|
48781
|
+
asked = step.needs_person.ask;
|
|
48782
|
+
out.step(
|
|
48783
|
+
people ? step.needs_person.ask : `${step.needs_person.why} Ask the person: "${step.needs_person.ask}" This carries on by itself once they have, so keep it running, or run it again.`
|
|
48784
|
+
);
|
|
48785
|
+
}
|
|
48786
|
+
if (stops || Date.now() >= deadline) break;
|
|
48787
|
+
if (step.needs_person) await sleep10(PERSON_EVERY_MS);
|
|
48788
|
+
}
|
|
48789
|
+
return finish(ctx, ref, step, options);
|
|
48790
|
+
}
|
|
48791
|
+
async function finish(ctx, ref, step, options) {
|
|
48792
|
+
const { out } = ctx;
|
|
48793
|
+
const name = releaseName(ref);
|
|
48794
|
+
const url2 = step.urls.play ?? step.urls.page;
|
|
48795
|
+
const ended = ENDS.has(step.stage) && step.stage !== "stopped" && step.stage !== "uploading";
|
|
48796
|
+
const { extra = {}, notes = [] } = (ended || step.stage === "testing" || step.stage === "in_review" ? await options.after?.(step) : void 0) ?? {};
|
|
48797
|
+
const base = { release: { ...ref }, stage: step.stage, line: step.line, url: url2, ...extra };
|
|
48798
|
+
const say = (...lines) => {
|
|
48799
|
+
for (const line of [...notes, ...lines]) out.line(line);
|
|
48800
|
+
};
|
|
48801
|
+
switch (step.stage) {
|
|
48802
|
+
case "live":
|
|
48803
|
+
out.result({ ...base, next: null }, () => say(import_picocolors21.default.green(`Live: ${url2}`)));
|
|
48804
|
+
return;
|
|
48805
|
+
case "ready":
|
|
48806
|
+
out.result(
|
|
48807
|
+
{ ...base, next: null },
|
|
48808
|
+
() => say("Passed. Press publish in the chat to send it.")
|
|
48809
|
+
);
|
|
48810
|
+
return;
|
|
48811
|
+
case "your_review": {
|
|
48812
|
+
const sheets = await saveSheets(
|
|
48813
|
+
step.footage ?? [],
|
|
48814
|
+
options.out ? resolve16(options.out) : await mkdtemp4(join19(tmpdir4(), "pch-footage-"))
|
|
48815
|
+
);
|
|
48816
|
+
const next = reviewCommand2(name);
|
|
48817
|
+
out.result({ ...base, test: step.test_id, footage: sheets, next }, () => {
|
|
48818
|
+
say(`Recorded. Look at every sheet, the bursts too:`);
|
|
48819
|
+
for (const sheet of sheets) out.line(` ${sheet.path} (${sheet.from} to ${sheet.to})`);
|
|
48820
|
+
out.line(`Then send your review (add --harmful if it does anything harmful):`);
|
|
48821
|
+
out.line(`Next: ${next}`);
|
|
48822
|
+
});
|
|
48823
|
+
return;
|
|
48824
|
+
}
|
|
48825
|
+
case "stopped":
|
|
48826
|
+
case "uploading": {
|
|
48827
|
+
const code = step.code ?? "stopped";
|
|
48828
|
+
const exit = code === "test_failed" || code === "rejected" || code === "files_failed" ? EXIT.blocked : EXIT.error;
|
|
48829
|
+
throw new CliError(exit, code, `${name}: ${step.next}`, {
|
|
48830
|
+
findings: step.findings,
|
|
48831
|
+
issues: step.findings.map((finding) => ({
|
|
48832
|
+
path: finding.path ?? "",
|
|
48833
|
+
message: `${finding.rule_id}: ${finding.message}${finding.suggestion ? `. ${finding.suggestion}` : ""}`
|
|
48834
|
+
})),
|
|
48835
|
+
next: code === "couldnt_run" ? `pch publish ${name} --test-again` : "pch publish"
|
|
48836
|
+
});
|
|
48837
|
+
}
|
|
48838
|
+
default: {
|
|
48839
|
+
const staff = step.status === "in_review";
|
|
48840
|
+
const next = staff ? `pch status ${name}` : `pch publish ${name}`;
|
|
48841
|
+
out.result({ ...base, needs_person: step.needs_person, next }, () => {
|
|
48842
|
+
if (staff) say("Waiting for a staff member at Patch.");
|
|
48843
|
+
else say(`Still ${lineOf(step).replace(/^./, (first) => first.toLowerCase())}.`);
|
|
48844
|
+
out.line(`Next: ${next}`);
|
|
48845
|
+
});
|
|
48846
|
+
}
|
|
48847
|
+
}
|
|
48848
|
+
}
|
|
48849
|
+
async function firstTime(ctx, project2, what) {
|
|
48850
|
+
const key = `${ctx.target.origin} ${project2}`;
|
|
48851
|
+
ctx.config.told ??= {};
|
|
48852
|
+
const told = ctx.config.told[key] ?? [];
|
|
48853
|
+
if (told.includes(what)) return false;
|
|
48854
|
+
ctx.config.told[key] = [...told, what];
|
|
48855
|
+
await writeConfig(ctx.config).catch(() => void 0);
|
|
48856
|
+
return true;
|
|
48857
|
+
}
|
|
48858
|
+
|
|
48859
|
+
// src/commands/publish.ts
|
|
48390
48860
|
var MB = 1024 * 1024;
|
|
48391
48861
|
var FILES_AT_ONCE = 16;
|
|
48392
48862
|
var BIG_FILE = 16 * MB;
|
|
@@ -48402,8 +48872,23 @@ function forkNamesParent(named, clone3) {
|
|
|
48402
48872
|
{ named, clone: clone3 }
|
|
48403
48873
|
);
|
|
48404
48874
|
}
|
|
48405
|
-
async function publish(ctx,
|
|
48875
|
+
async function publish(ctx, target, options) {
|
|
48406
48876
|
requireToken(ctx.target);
|
|
48877
|
+
const started = Date.now();
|
|
48878
|
+
const follow4 = {
|
|
48879
|
+
...options,
|
|
48880
|
+
wait: followFor(options.wait),
|
|
48881
|
+
since: started
|
|
48882
|
+
};
|
|
48883
|
+
if (target.includes("@")) return followRelease(ctx, parseReleaseRef(target), follow4);
|
|
48884
|
+
if (options.review !== void 0 || options.testAgain) {
|
|
48885
|
+
throw new CliError(
|
|
48886
|
+
EXIT.usage,
|
|
48887
|
+
"release_required",
|
|
48888
|
+
"Say which release: pch publish <owner/slug@version> --review \u2026, as pch publish printed it."
|
|
48889
|
+
);
|
|
48890
|
+
}
|
|
48891
|
+
const dir = target;
|
|
48407
48892
|
const { out, api } = ctx;
|
|
48408
48893
|
const project2 = await loadProject(dir);
|
|
48409
48894
|
if (project2.issues.length) {
|
|
@@ -48462,53 +48947,41 @@ async function publish(ctx, dir, options) {
|
|
|
48462
48947
|
} else {
|
|
48463
48948
|
out.step("Patch already has every file");
|
|
48464
48949
|
}
|
|
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
48950
|
const branch = await source?.bringBranch();
|
|
48473
|
-
|
|
48474
|
-
|
|
48475
|
-
|
|
48476
|
-
|
|
48477
|
-
|
|
48478
|
-
|
|
48479
|
-
|
|
48480
|
-
|
|
48481
|
-
|
|
48482
|
-
|
|
48483
|
-
|
|
48484
|
-
|
|
48485
|
-
|
|
48486
|
-
|
|
48487
|
-
|
|
48488
|
-
|
|
48489
|
-
|
|
48490
|
-
|
|
48491
|
-
|
|
48492
|
-
|
|
48493
|
-
|
|
48494
|
-
|
|
48495
|
-
|
|
48496
|
-
|
|
48497
|
-
|
|
48498
|
-
|
|
48499
|
-
|
|
48500
|
-
|
|
48501
|
-
|
|
48502
|
-
|
|
48503
|
-
|
|
48504
|
-
|
|
48505
|
-
for (const entry of release3.files) {
|
|
48506
|
-
out.line(
|
|
48507
|
-
` ${entry.path.padEnd(40)} ${formatSize(entry.size).padStart(8)} sha256 ${entry.sha256}`
|
|
48508
|
-
);
|
|
48951
|
+
await followRelease(
|
|
48952
|
+
ctx,
|
|
48953
|
+
{ ...ref, version: version2 },
|
|
48954
|
+
{
|
|
48955
|
+
...follow4,
|
|
48956
|
+
// Who can see it, as it is once it stops. It's the same each time, so it's said once a
|
|
48957
|
+
// project, and --json always has it.
|
|
48958
|
+
after: async () => {
|
|
48959
|
+
const release3 = await unwrap(
|
|
48960
|
+
api.GET("/projects/{owner}/{slug}/releases/{version}", { params: { path: path2 } })
|
|
48961
|
+
);
|
|
48962
|
+
const openCode = await openCodeOf(ctx, ref, release3);
|
|
48963
|
+
const privately = release3.channel === "private" ? privateWords(release3.kind, release3.status !== "published") : null;
|
|
48964
|
+
const notes = [source ? fromCommit(source).trim() : null, branch?.say ?? null].filter(
|
|
48965
|
+
(note2) => Boolean(note2)
|
|
48966
|
+
);
|
|
48967
|
+
for (const [what, words2] of [
|
|
48968
|
+
["private", privately],
|
|
48969
|
+
["open_code", openCode]
|
|
48970
|
+
]) {
|
|
48971
|
+
if (words2 && await firstTime(ctx, file2.project, what)) notes.push(words2);
|
|
48972
|
+
}
|
|
48973
|
+
return {
|
|
48974
|
+
extra: {
|
|
48975
|
+
release: release3,
|
|
48976
|
+
uploaded: draft.uploads.length,
|
|
48977
|
+
...branch ? { branch } : {},
|
|
48978
|
+
...whoCanSeeIt(privately, openCode)
|
|
48979
|
+
},
|
|
48980
|
+
notes
|
|
48981
|
+
};
|
|
48982
|
+
}
|
|
48509
48983
|
}
|
|
48510
|
-
|
|
48511
|
-
});
|
|
48984
|
+
);
|
|
48512
48985
|
}
|
|
48513
48986
|
function fromCommit(source) {
|
|
48514
48987
|
const on = source.on === "GitHub" ? " on GitHub" : "";
|
|
@@ -48527,7 +49000,7 @@ async function releaseSubmit(ctx, ref) {
|
|
|
48527
49000
|
ctx.out.result({ ...submitted, next, ...whoCanSee }, () => {
|
|
48528
49001
|
showSubmitted(ctx, release3, submitted.status, submitted);
|
|
48529
49002
|
if (openCode) ctx.out.line(openCode);
|
|
48530
|
-
ctx.out.line(
|
|
49003
|
+
ctx.out.line(import_picocolors22.default.dim(projectUrl(ctx.target.origin, release3)));
|
|
48531
49004
|
});
|
|
48532
49005
|
}
|
|
48533
49006
|
async function releasePromote(ctx, ref, options) {
|
|
@@ -48581,7 +49054,7 @@ async function releasePromote(ctx, ref, options) {
|
|
|
48581
49054
|
const next = nextStep(name, "draft", null).command;
|
|
48582
49055
|
ctx.out.result({ from: ref, release: { version: version2, status: "draft" }, url: url2, next }, () => {
|
|
48583
49056
|
ctx.out.line(
|
|
48584
|
-
`Made ${
|
|
49057
|
+
`Made ${import_picocolors22.default.bold(name)} from ${ref}, as a draft for ${options.channel}. It needs a passed test of its own.`
|
|
48585
49058
|
);
|
|
48586
49059
|
ctx.out.line(`Next: ${next}`);
|
|
48587
49060
|
});
|
|
@@ -48589,7 +49062,7 @@ async function releasePromote(ctx, ref, options) {
|
|
|
48589
49062
|
}
|
|
48590
49063
|
ctx.out.result({ from: ref, release: submitted, url: url2 }, () => {
|
|
48591
49064
|
showSubmitted(ctx, to, submitted.status, submitted);
|
|
48592
|
-
ctx.out.line(
|
|
49065
|
+
ctx.out.line(import_picocolors22.default.dim(url2));
|
|
48593
49066
|
});
|
|
48594
49067
|
}
|
|
48595
49068
|
function draftUntilTested(error62) {
|
|
@@ -48626,7 +49099,7 @@ function whoCanSeeIt(...words2) {
|
|
|
48626
49099
|
function showSubmitted(ctx, ref, status2, { channel, kind }) {
|
|
48627
49100
|
const name = releaseName(ref);
|
|
48628
49101
|
if (status2 === "published") {
|
|
48629
|
-
ctx.out.line(
|
|
49102
|
+
ctx.out.line(import_picocolors22.default.green(`Published ${name} to ${channel}`));
|
|
48630
49103
|
if (channel === "private") {
|
|
48631
49104
|
ctx.out.line(privateWords(kind, false));
|
|
48632
49105
|
ctx.out.line(`pch status ${name} says whether your team can play it yet, and where.`);
|
|
@@ -48779,314 +49252,8 @@ async function uploadFailed(file2, upload, response) {
|
|
|
48779
49252
|
);
|
|
48780
49253
|
}
|
|
48781
49254
|
|
|
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
49255
|
// src/commands/search.ts
|
|
49089
|
-
var
|
|
49256
|
+
var import_picocolors23 = __toESM(require_picocolors(), 1);
|
|
49090
49257
|
init_guide_words();
|
|
49091
49258
|
var filter = (values) => values?.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean).join(",") || void 0;
|
|
49092
49259
|
var licences = (values) => values.flatMap((value) => value.split(",")).map((value) => licenceId(value.trim()) ?? value.trim());
|
|
@@ -49124,13 +49291,13 @@ async function search(ctx, q, options) {
|
|
|
49124
49291
|
out.result({ ...answer, ...aboutBlocks }, () => {
|
|
49125
49292
|
if (!data.length) out.line("Nothing found.");
|
|
49126
49293
|
for (const result of plain(data)) {
|
|
49127
|
-
out.line(`${
|
|
49294
|
+
out.line(`${import_picocolors23.default.bold(result.handle)} ${import_picocolors23.default.dim(facts(result))}`);
|
|
49128
49295
|
if (result.summary) out.line(` ${result.summary}`);
|
|
49129
49296
|
if (result.demo) out.line(` Live demo: ${ctx.target.origin}/${result.owner}/${result.slug}`);
|
|
49130
|
-
out.line(
|
|
49297
|
+
out.line(import_picocolors23.default.dim(` ${result.clone_url}`));
|
|
49131
49298
|
}
|
|
49132
49299
|
if (blocks) out.line(BLOCK_WORDS);
|
|
49133
|
-
if (cursor) out.line(
|
|
49300
|
+
if (cursor) out.line(import_picocolors23.default.dim("There are more; raise --limit."));
|
|
49134
49301
|
});
|
|
49135
49302
|
}
|
|
49136
49303
|
function facts(result) {
|
|
@@ -49155,21 +49322,21 @@ async function categories(ctx, options) {
|
|
|
49155
49322
|
const width = Math.max(0, ...data.map((category) => category.id.length));
|
|
49156
49323
|
for (const category of data) {
|
|
49157
49324
|
ctx.out.line(
|
|
49158
|
-
`${
|
|
49325
|
+
`${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
49326
|
);
|
|
49160
49327
|
}
|
|
49161
49328
|
});
|
|
49162
49329
|
}
|
|
49163
49330
|
|
|
49164
49331
|
// src/commands/update.ts
|
|
49165
|
-
var
|
|
49332
|
+
var import_picocolors24 = __toESM(require_picocolors(), 1);
|
|
49166
49333
|
var import_semver3 = __toESM(require_semver2(), 1);
|
|
49167
49334
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
49168
49335
|
import { createHash as createHash4 } from "node:crypto";
|
|
49169
49336
|
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
49170
49337
|
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
|
|
49172
|
-
import { setTimeout as
|
|
49338
|
+
import { basename as basename8, dirname as dirname7, join as join20 } from "node:path";
|
|
49339
|
+
import { setTimeout as sleep11 } from "node:timers/promises";
|
|
49173
49340
|
import { gunzipSync } from "node:zlib";
|
|
49174
49341
|
init_errors();
|
|
49175
49342
|
init_version();
|
|
@@ -49312,7 +49479,7 @@ async function whenFree(step, wait2 = 200) {
|
|
|
49312
49479
|
} catch (error62) {
|
|
49313
49480
|
const code = error62.code ?? "";
|
|
49314
49481
|
if (attempt >= 10 || !["EBUSY", "EPERM", "EACCES"].includes(code)) throw error62;
|
|
49315
|
-
await
|
|
49482
|
+
await sleep11(attempt * wait2);
|
|
49316
49483
|
}
|
|
49317
49484
|
}
|
|
49318
49485
|
}
|
|
@@ -49322,7 +49489,7 @@ function removeOldExecutables(target = process.execPath) {
|
|
|
49322
49489
|
const old = new RegExp(`^${basename8(target).replace(/\./g, "\\.")}\\.\\d+\\.old$`, "i");
|
|
49323
49490
|
try {
|
|
49324
49491
|
for (const name of readdirSync(folder)) {
|
|
49325
|
-
if (old.test(name)) rmSync(
|
|
49492
|
+
if (old.test(name)) rmSync(join20(folder, name), { force: true });
|
|
49326
49493
|
}
|
|
49327
49494
|
} catch {
|
|
49328
49495
|
}
|
|
@@ -49332,7 +49499,7 @@ function startUpdateNotice(ctx, command, now = Date.now()) {
|
|
|
49332
49499
|
const releases = releasesUrl(ctx.target.env);
|
|
49333
49500
|
if (!how || !releases || ctx.out.json || QUIET.has(command)) return null;
|
|
49334
49501
|
if (!process.stderr.isTTY || process.env.CI) return null;
|
|
49335
|
-
const stamp =
|
|
49502
|
+
const stamp = join20(dirname7(configPath()), "update-check.json");
|
|
49336
49503
|
try {
|
|
49337
49504
|
const { checked_at } = JSON.parse(readFileSync(stamp, "utf8"));
|
|
49338
49505
|
if (checked_at && now - Date.parse(checked_at) < DAY) return null;
|
|
@@ -49361,7 +49528,7 @@ function startUpdateNotice(ctx, command, now = Date.now()) {
|
|
|
49361
49528
|
if (version2 && import_semver3.default.gt(version2, VERSION)) {
|
|
49362
49529
|
process.stderr.write(
|
|
49363
49530
|
`
|
|
49364
|
-
${
|
|
49531
|
+
${import_picocolors24.default.dim("\u203A")} pch ${version2} is out (you have ${VERSION}). To update, run: ${how}
|
|
49365
49532
|
`
|
|
49366
49533
|
);
|
|
49367
49534
|
}
|
|
@@ -49370,7 +49537,7 @@ ${import_picocolors23.default.dim("\u203A")} pch ${version2} is out (you have ${
|
|
|
49370
49537
|
}
|
|
49371
49538
|
|
|
49372
49539
|
// src/commands/validate.ts
|
|
49373
|
-
var
|
|
49540
|
+
var import_picocolors25 = __toESM(require_picocolors(), 1);
|
|
49374
49541
|
init_errors();
|
|
49375
49542
|
async function validate2(ctx, dir) {
|
|
49376
49543
|
const project2 = await loadProject(dir);
|
|
@@ -49429,12 +49596,12 @@ async function validate2(ctx, dir) {
|
|
|
49429
49596
|
notes
|
|
49430
49597
|
};
|
|
49431
49598
|
ctx.out.result(result, () => {
|
|
49432
|
-
const mark = result.ok ?
|
|
49599
|
+
const mark = result.ok ? import_picocolors25.default.green("\u2713") : import_picocolors25.default.red("\u2717");
|
|
49433
49600
|
ctx.out.line(
|
|
49434
49601
|
`${mark} ${file2.project}@${file2.version} \xB7 ${project2.kind} \xB7 ${result.trust_tier} \xB7 ${project2.files.length} files, ${formatSize(total)}`
|
|
49435
49602
|
);
|
|
49436
|
-
for (const issue2 of issues) ctx.out.line(` ${
|
|
49437
|
-
for (const note2 of notes) ctx.out.line(` ${
|
|
49603
|
+
for (const issue2 of issues) ctx.out.line(` ${import_picocolors25.default.red(issue2.path || "\xB7")} ${issue2.message}`);
|
|
49604
|
+
for (const note2 of notes) ctx.out.line(` ${import_picocolors25.default.dim(note2)}`);
|
|
49438
49605
|
});
|
|
49439
49606
|
return result.ok ? EXIT.ok : EXIT.usage;
|
|
49440
49607
|
}
|
|
@@ -49469,8 +49636,7 @@ Start here:
|
|
|
49469
49636
|
pch search "grappling hook" find a block or game to start from, then pch remix or pch clone it, and change it
|
|
49470
49637
|
pch assets search "crate" --type model find a model, sound or texture, then pch assets add it
|
|
49471
49638
|
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
|
|
49639
|
+
pch validate, then pch publish upload, test on your PC, your review of the footage, then live
|
|
49474
49640
|
pch media add, pch project update the cover and the listing
|
|
49475
49641
|
|
|
49476
49642
|
Add --json to any command for results an agent can read. Exit codes: 0 fine, 1 error, 2 bad usage,
|
|
@@ -49513,7 +49679,7 @@ function topLevel(command) {
|
|
|
49513
49679
|
}
|
|
49514
49680
|
var collect = (value, previous = []) => [...previous, value];
|
|
49515
49681
|
program2.command("login").description(
|
|
49516
|
-
"Sign in through the browser (device code), and sign git in with it. It waits for a person, which can take a few minutes, so don't run it under a short timeout"
|
|
49682
|
+
"Sign in through the browser (device code), and sign git in with it. It waits for a person, which can take a few minutes, so don't run it under a short timeout. Agents: run it yourself, in the background, and give the person the link it prints, which has the code in it"
|
|
49517
49683
|
).option("--scope <scope>", "API key scope: read, publish, admin (repeatable)", collect).option("--no-browser", "print the link instead of opening a browser").action(action((ctx, options) => login(ctx, options)));
|
|
49518
49684
|
program2.command("logout").description("Revoke and forget the active login, and sign git out").action(action((ctx) => logout(ctx)));
|
|
49519
49685
|
program2.command("whoami").description("Who you're signed in as, and where").action(action((ctx) => whoami(ctx)));
|
|
@@ -49724,17 +49890,29 @@ program2.command("check [dir]").description(
|
|
|
49724
49890
|
(ctx, dir, options) => check2(ctx, dir ?? ".", options)
|
|
49725
49891
|
)
|
|
49726
49892
|
);
|
|
49727
|
-
program2.command("publish [
|
|
49728
|
-
"
|
|
49893
|
+
program2.command("publish [target]").description(
|
|
49894
|
+
"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. Agents: run it yourself, wherever you run (WSL, Linux, a Mac or a server). The person's Patch Desktop takes the test through Patch, and pch says if they need to open it or press Start"
|
|
49729
49895
|
).addOption(
|
|
49730
49896
|
new Option("--channel <channel>", `overrides patch.json. ${NOT_LOCKED_HELP}`).choices([
|
|
49731
49897
|
"private",
|
|
49732
49898
|
"unlisted",
|
|
49733
49899
|
"public"
|
|
49734
49900
|
])
|
|
49735
|
-
).option("--changelog <text>", "what changed, shown as the update notes").
|
|
49901
|
+
).option("--changelog <text>", "what changed, shown as the update notes").addOption(
|
|
49902
|
+
new Option("--review <verdict>", "your review of the footage: pass or fail").choices([
|
|
49903
|
+
"pass",
|
|
49904
|
+
"fail"
|
|
49905
|
+
])
|
|
49906
|
+
).option("--summary <text>", "with --review: what you saw, in a few lines").option(
|
|
49907
|
+
"--moment <moment>",
|
|
49908
|
+
'with --review: "<run> <mm:ss> <what you saw>", at least two, one per run (repeatable)',
|
|
49909
|
+
collect
|
|
49910
|
+
).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(
|
|
49911
|
+
"--wait <seconds>",
|
|
49912
|
+
"how long to follow it (default: until it stops, at a terminal; 100 s otherwise)"
|
|
49913
|
+
).action(
|
|
49736
49914
|
action(
|
|
49737
|
-
(ctx,
|
|
49915
|
+
(ctx, target, options) => publish(ctx, target ?? ".", options)
|
|
49738
49916
|
)
|
|
49739
49917
|
);
|
|
49740
49918
|
var project = program2.command("project").description("Projects, addressed owner/slug");
|