@wasm-oj/cli 0.2.0 → 0.2.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 +16 -1
- package/dist/index.d.ts +13 -5
- package/dist/index.js +165 -328
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -8,9 +8,24 @@ server. `--offline` rejects every network-capable command before dispatch.
|
|
|
8
8
|
```sh
|
|
9
9
|
woj config set server https://example.invalid
|
|
10
10
|
woj auth login
|
|
11
|
-
woj problem pull <problem-
|
|
11
|
+
woj problem pull <problem-id> --language cpp --locale zh-TW
|
|
12
12
|
woj test
|
|
13
13
|
woj submit --wait
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
Run `woj --help` to see the complete role-based command tree.
|
|
17
|
+
|
|
18
|
+
`woj organizer collection build` materializes strict
|
|
19
|
+
`wasm-oj-platform/contests/v2` manifests. Contest workspaces pin timeline, rule, and problem epoch
|
|
20
|
+
tokens and send them through the contest Official Submit context; Prompt Program is a separate
|
|
21
|
+
prompt-attempt workflow, not a compiler language.
|
|
22
|
+
|
|
23
|
+
## Upgrading from 0.2.0
|
|
24
|
+
|
|
25
|
+
Connect and synchronize a repository catalog with `woj organizer catalog connect --repo
|
|
26
|
+
<numeric-repository-id>` and `woj organizer catalog sync <catalog-id> --ref <ref> --wait`.
|
|
27
|
+
These replace the collection create, validate, publish, and activate commands. Author catalog
|
|
28
|
+
content in `wasm-oj.json`; the resolved Git commit identifies the synchronized content.
|
|
29
|
+
|
|
30
|
+
Problem commands accept stable problem IDs. Start a rejudge with `woj organizer rejudge start
|
|
31
|
+
<problem-id> --from <commit> --to <commit> --wait`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { JudgeResult, parseStandaloneProblemBundle, ToolchainDescriptor } from '@wasm-oj/core';
|
|
2
2
|
|
|
3
|
-
declare const WOJ_CLI_VERSION = "0.2.
|
|
3
|
+
declare const WOJ_CLI_VERSION = "0.2.1";
|
|
4
4
|
/** Stable process exit codes. They are part of the public CLI contract. */
|
|
5
5
|
declare const WOJ_EXIT: Readonly<{
|
|
6
6
|
readonly success: 0;
|
|
@@ -226,19 +226,27 @@ interface CommandDependencies {
|
|
|
226
226
|
|
|
227
227
|
declare const CLI_TOOLCHAIN_DESCRIPTORS: readonly ToolchainDescriptor[];
|
|
228
228
|
|
|
229
|
-
declare const WOJ_WORKSPACE_SCHEMA = "wasm-oj-cli-workspace-
|
|
229
|
+
declare const WOJ_WORKSPACE_SCHEMA = "wasm-oj-cli-workspace-v3";
|
|
230
230
|
declare const WORKSPACE_FILE = "woj.json";
|
|
231
231
|
declare const LANGUAGES: readonly ["c", "cpp", "rust", "go", "python", "javascript", "typescript"];
|
|
232
232
|
type WorkspaceLanguage = typeof LANGUAGES[number];
|
|
233
233
|
interface PinnedProblem {
|
|
234
|
-
readonly
|
|
235
|
-
readonly
|
|
234
|
+
readonly problemId: string;
|
|
235
|
+
readonly catalogCommit: string;
|
|
236
236
|
readonly serverOrigin: string;
|
|
237
237
|
readonly contentUrl: string;
|
|
238
238
|
readonly contentSha256: string;
|
|
239
239
|
readonly contentFile: "problem.json";
|
|
240
240
|
readonly locale: "zh-TW" | "en";
|
|
241
|
-
readonly
|
|
241
|
+
readonly context: {
|
|
242
|
+
readonly kind: "practice";
|
|
243
|
+
} | {
|
|
244
|
+
readonly kind: "contest";
|
|
245
|
+
readonly contestId: string;
|
|
246
|
+
readonly timelineGeneration: number;
|
|
247
|
+
readonly ruleEpoch: number;
|
|
248
|
+
readonly problemEpoch: number;
|
|
249
|
+
};
|
|
242
250
|
}
|
|
243
251
|
interface WojWorkspace {
|
|
244
252
|
readonly schema: typeof WOJ_WORKSPACE_SCHEMA;
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { pathToFileURL } from "node:url";
|
|
|
9
9
|
import { PROJECT_SOURCE_LIMITS, WASM_OJ_JUDGE_PACKAGE_MAX_BYTES, decodeJudgePackageForExecution, parseStandaloneProblemBundle, trustedJudgeSpec, validateJudgePackage } from "@wasm-oj/core";
|
|
10
10
|
import { createServerEngine } from "@wasm-oj/server";
|
|
11
11
|
//#region src/cli/contracts.ts
|
|
12
|
-
var WOJ_CLI_VERSION = "0.2.
|
|
12
|
+
var WOJ_CLI_VERSION = "0.2.1";
|
|
13
13
|
/** Stable process exit codes. They are part of the public CLI contract. */
|
|
14
14
|
var WOJ_EXIT = Object.freeze({
|
|
15
15
|
success: 0,
|
|
@@ -75,12 +75,12 @@ var WOJ_COMMANDS = Object.freeze([
|
|
|
75
75
|
stdin: "string"
|
|
76
76
|
}),
|
|
77
77
|
local("watch", "Re-run a local action after workspace files change.", "", { command: "string" }),
|
|
78
|
-
remote("problem list", "List public
|
|
79
|
-
remote("problem show", "Show one public problem
|
|
78
|
+
remote("problem list", "List active public problems.", "", { locale: "string" }),
|
|
79
|
+
remote("problem show", "Show one active public problem.", "<problem-id>", {
|
|
80
80
|
locale: "string",
|
|
81
81
|
contest: "string"
|
|
82
82
|
}),
|
|
83
|
-
remote("problem pull", "Pull and pin one
|
|
83
|
+
remote("problem pull", "Pull and pin one problem at its active catalog commit.", "<problem-id> [directory]", {
|
|
84
84
|
locale: "string",
|
|
85
85
|
language: "string",
|
|
86
86
|
contest: "string",
|
|
@@ -99,14 +99,14 @@ var WOJ_COMMANDS = Object.freeze([
|
|
|
99
99
|
remote("submission policy", "Show the submission policy summary.", "<submission-id>"),
|
|
100
100
|
remote("contest list", "List visible contests."),
|
|
101
101
|
remote("contest show", "Show one contest.", "<contest-id>"),
|
|
102
|
-
remote("contest join", "Join
|
|
103
|
-
remote("contest problems", "List
|
|
102
|
+
remote("contest join", "Join a contest; invite contests require --code-file.", "<contest-id>", { "code-file": "string" }),
|
|
103
|
+
remote("contest problems", "List projected locked, open, and closed contest problem states.", "<contest-id>"),
|
|
104
104
|
remote("contest standings", "Show contest standings.", "<contest-id>", { limit: "string" }),
|
|
105
|
-
remote("performance frontier", "Show the verified performance frontier.", "<problem-
|
|
105
|
+
remote("performance frontier", "Show the verified performance frontier.", "<problem-id>", {
|
|
106
106
|
language: "string",
|
|
107
107
|
contest: "string"
|
|
108
108
|
}),
|
|
109
|
-
remote("performance evolution", "Show your verified performance evolution.", "<problem-
|
|
109
|
+
remote("performance evolution", "Show your verified performance evolution.", "<problem-id>", {
|
|
110
110
|
language: "string",
|
|
111
111
|
contest: "string"
|
|
112
112
|
}),
|
|
@@ -127,71 +127,29 @@ var WOJ_COMMANDS = Object.freeze([
|
|
|
127
127
|
remote("organizer repo list", "List authorized Organizer repositories."),
|
|
128
128
|
remote("organizer repo show", "Show one authorized Organizer repository.", "<repository-id>"),
|
|
129
129
|
local("organizer collection init", "Create a collection authoring skeleton.", "[directory]", { force: "boolean" }),
|
|
130
|
-
local("organizer collection build", "Build
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
local("organizer collection verify", "Verify collection bytes locally without executing judge code.", "[directory]", {
|
|
137
|
-
index: "string",
|
|
138
|
-
source: "string",
|
|
139
|
-
managed: "string"
|
|
140
|
-
}),
|
|
141
|
-
remote("organizer collection list", "List Organizer collections."),
|
|
142
|
-
remote("organizer collection show", "Show one Organizer collection.", "<collection-id>"),
|
|
143
|
-
remote("organizer collection create", "Register one repository collection.", "", {
|
|
144
|
-
repo: "string",
|
|
145
|
-
index: "string"
|
|
146
|
-
}),
|
|
147
|
-
remote("organizer collection validate", "Resolve a ref once and statically validate the exact commit.", "<collection-id>", {
|
|
130
|
+
local("organizer collection build", "Build repository manifests and deployable bundles locally.", "[directory]", { source: "string" }),
|
|
131
|
+
local("organizer collection verify", "Verify repository manifests and deployable bundles without executing judge code.", "[directory]", { source: "string" }),
|
|
132
|
+
remote("organizer catalog list", "List connected repository catalogs."),
|
|
133
|
+
remote("organizer catalog show", "Show one connected repository catalog.", "<catalog-id>"),
|
|
134
|
+
remote("organizer catalog connect", "Connect one authorized GitHub repository.", "", { repo: "string" }),
|
|
135
|
+
remote("organizer catalog sync", "Resolve one ref and sync its exact commit.", "<catalog-id>", {
|
|
148
136
|
ref: "string",
|
|
149
137
|
...wait
|
|
150
138
|
}),
|
|
151
|
-
remote("organizer
|
|
139
|
+
remote("organizer catalog sync-show", "Show or watch one catalog sync.", "<sync-id>", {
|
|
152
140
|
watch: "boolean",
|
|
153
141
|
interval: "string"
|
|
154
142
|
}),
|
|
155
|
-
remote("organizer collection publish", "Publish one validated immutable revision.", "<revision-id>", {
|
|
156
|
-
mode: "string",
|
|
157
|
-
...wait
|
|
158
|
-
}),
|
|
159
|
-
remote("organizer collection publication", "Show or watch a publication job.", "<publication-job-id>", {
|
|
160
|
-
watch: "boolean",
|
|
161
|
-
interval: "string"
|
|
162
|
-
}),
|
|
163
|
-
remote("organizer collection activate", "Explicitly activate a published official-practice revision.", "<publication-id>"),
|
|
164
143
|
remote("organizer contest list", "List contests you organize."),
|
|
165
144
|
remote("organizer contest show", "Show one Organizer contest.", "<contest-id>"),
|
|
166
|
-
remote("organizer contest
|
|
167
|
-
|
|
168
|
-
description: "string",
|
|
169
|
-
starts: "string",
|
|
170
|
-
ends: "string",
|
|
171
|
-
freeze: "string",
|
|
172
|
-
access: "string",
|
|
173
|
-
"invite-code-file": "string",
|
|
174
|
-
problem: "repeatable"
|
|
175
|
-
}),
|
|
176
|
-
remote("organizer contest update", "Update draft contest settings.", "<contest-id>", {
|
|
177
|
-
title: "string",
|
|
178
|
-
description: "string",
|
|
179
|
-
starts: "string",
|
|
180
|
-
ends: "string",
|
|
181
|
-
freeze: "string",
|
|
182
|
-
access: "string",
|
|
183
|
-
"invite-code-file": "string"
|
|
184
|
-
}),
|
|
185
|
-
remote("organizer contest add-problem", "Add an exact published problem version.", "<contest-id> <problem-version-id>"),
|
|
186
|
-
remote("organizer contest remove-problem", "Remove a problem from a draft contest.", "<contest-id> <problem-version-id>"),
|
|
187
|
-
remote("organizer contest publish", "Publish a draft contest.", "<contest-id>"),
|
|
188
|
-
remote("organizer contest archive", "Archive a contest.", "<contest-id>"),
|
|
189
|
-
remote("organizer contest participants", "List contest participants.", "<contest-id>", paging),
|
|
145
|
+
remote("organizer contest invite-rotate", "Rotate an invite contest credential.", "<contest-id>", { "code-file": "string" }),
|
|
146
|
+
remote("organizer contest participants", "List contest entrants.", "<contest-id>", paging),
|
|
190
147
|
remote("organizer contest standings", "Show organizer-visible standings.", "<contest-id>", { limit: "string" }),
|
|
191
|
-
remote("organizer rejudge options", "List valid
|
|
192
|
-
remote("organizer rejudge start", "Start a rejudge batch.", "", {
|
|
148
|
+
remote("organizer rejudge options", "List known commits or valid targets for one problem revision.", "[problem-id]", { from: "string" }),
|
|
149
|
+
remote("organizer rejudge start", "Start a commit-to-commit rejudge batch.", "<problem-id>", {
|
|
193
150
|
from: "string",
|
|
194
151
|
to: "string",
|
|
152
|
+
contest: "string",
|
|
195
153
|
...wait
|
|
196
154
|
}),
|
|
197
155
|
remote("organizer rejudge list", "List rejudge batches.", "", { limit: "string" }),
|
|
@@ -1063,7 +1021,7 @@ var CLI_TOOLCHAIN_DESCRIPTORS = Object.freeze([
|
|
|
1063
1021
|
]);
|
|
1064
1022
|
//#endregion
|
|
1065
1023
|
//#region src/cli/workspace.ts
|
|
1066
|
-
var WOJ_WORKSPACE_SCHEMA = "wasm-oj-cli-workspace-
|
|
1024
|
+
var WOJ_WORKSPACE_SCHEMA = "wasm-oj-cli-workspace-v3";
|
|
1067
1025
|
var WORKSPACE_FILE = "woj.json";
|
|
1068
1026
|
var LANGUAGES = [
|
|
1069
1027
|
"c",
|
|
@@ -1129,7 +1087,7 @@ function parseWorkspaceInternal(value) {
|
|
|
1129
1087
|
"sources",
|
|
1130
1088
|
...workspace.problem === void 0 ? [] : ["problem"]
|
|
1131
1089
|
], "woj workspace");
|
|
1132
|
-
if (workspace.schema !== "wasm-oj-cli-workspace-
|
|
1090
|
+
if (workspace.schema !== "wasm-oj-cli-workspace-v3") throw new CliError(`Unsupported workspace schema '${String(workspace.schema)}'.`);
|
|
1133
1091
|
if (typeof workspace.name !== "string" || !workspace.name.trim() || workspace.name.length > 128) throw new CliError("Workspace name is invalid.");
|
|
1134
1092
|
if (!LANGUAGES.includes(workspace.language)) throw new CliError("Workspace language is unsupported.");
|
|
1135
1093
|
if (workspace.target !== "wasip1" && workspace.target !== "wasix") throw new CliError("Workspace target must be 'wasip1' or 'wasix'.");
|
|
@@ -1142,18 +1100,18 @@ function parseWorkspaceInternal(value) {
|
|
|
1142
1100
|
if (workspace.problem !== void 0) {
|
|
1143
1101
|
const pinned = record(workspace.problem, "Pinned problem");
|
|
1144
1102
|
exactKeys(pinned, [
|
|
1145
|
-
"
|
|
1146
|
-
"
|
|
1103
|
+
"problemId",
|
|
1104
|
+
"catalogCommit",
|
|
1147
1105
|
"serverOrigin",
|
|
1148
1106
|
"contentUrl",
|
|
1149
1107
|
"contentSha256",
|
|
1150
1108
|
"contentFile",
|
|
1151
|
-
"
|
|
1152
|
-
|
|
1109
|
+
"context",
|
|
1110
|
+
"locale"
|
|
1153
1111
|
], "Pinned problem");
|
|
1154
1112
|
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
1155
|
-
if (typeof pinned.
|
|
1156
|
-
if (typeof pinned.
|
|
1113
|
+
if (typeof pinned.problemId !== "string" || !uuid.test(pinned.problemId)) throw new CliError("Pinned problem ID is invalid.");
|
|
1114
|
+
if (typeof pinned.catalogCommit !== "string" || !/^[0-9a-f]{40}$/.test(pinned.catalogCommit)) throw new CliError("Pinned catalog commit is invalid.");
|
|
1157
1115
|
let serverOrigin;
|
|
1158
1116
|
try {
|
|
1159
1117
|
serverOrigin = new URL(String(pinned.serverOrigin)).origin;
|
|
@@ -1165,16 +1123,38 @@ function parseWorkspaceInternal(value) {
|
|
|
1165
1123
|
if (typeof pinned.contentSha256 !== "string" || !/^[0-9a-f]{64}$/.test(pinned.contentSha256)) throw new CliError("Pinned problem digest is invalid.");
|
|
1166
1124
|
if (pinned.contentFile !== "problem.json") throw new CliError("Pinned problem content file is invalid.");
|
|
1167
1125
|
if (pinned.locale !== "zh-TW" && pinned.locale !== "en") throw new CliError("Pinned problem locale is invalid.");
|
|
1168
|
-
|
|
1126
|
+
const context = record(pinned.context, "Pinned problem context");
|
|
1127
|
+
if (context.kind === "practice") exactKeys(context, ["kind"], "Pinned practice context");
|
|
1128
|
+
else if (context.kind === "contest") {
|
|
1129
|
+
exactKeys(context, [
|
|
1130
|
+
"contestId",
|
|
1131
|
+
"kind",
|
|
1132
|
+
"problemEpoch",
|
|
1133
|
+
"ruleEpoch",
|
|
1134
|
+
"timelineGeneration"
|
|
1135
|
+
], "Pinned contest context");
|
|
1136
|
+
if (typeof context.contestId !== "string" || !uuid.test(context.contestId)) throw new CliError("Pinned contest ID is invalid.");
|
|
1137
|
+
for (const key of [
|
|
1138
|
+
"timelineGeneration",
|
|
1139
|
+
"ruleEpoch",
|
|
1140
|
+
"problemEpoch"
|
|
1141
|
+
]) if (!Number.isSafeInteger(context[key]) || context[key] < 1) throw new CliError(`Pinned contest ${key} is invalid.`);
|
|
1142
|
+
} else throw new CliError("Pinned problem context kind is invalid.");
|
|
1169
1143
|
problem = {
|
|
1170
|
-
|
|
1171
|
-
|
|
1144
|
+
problemId: pinned.problemId,
|
|
1145
|
+
catalogCommit: pinned.catalogCommit,
|
|
1172
1146
|
serverOrigin,
|
|
1173
1147
|
contentUrl: pinned.contentUrl,
|
|
1174
1148
|
contentSha256: pinned.contentSha256,
|
|
1175
1149
|
contentFile: "problem.json",
|
|
1176
1150
|
locale: pinned.locale,
|
|
1177
|
-
|
|
1151
|
+
context: context.kind === "practice" ? { kind: "practice" } : {
|
|
1152
|
+
kind: "contest",
|
|
1153
|
+
contestId: context.contestId,
|
|
1154
|
+
timelineGeneration: context.timelineGeneration,
|
|
1155
|
+
ruleEpoch: context.ruleEpoch,
|
|
1156
|
+
problemEpoch: context.problemEpoch
|
|
1157
|
+
}
|
|
1178
1158
|
};
|
|
1179
1159
|
}
|
|
1180
1160
|
return {
|
|
@@ -2140,12 +2120,7 @@ var TERMINAL_SUBMISSION_STATES = /* @__PURE__ */ new Set([
|
|
|
2140
2120
|
"infrastructure-error",
|
|
2141
2121
|
"cancelled"
|
|
2142
2122
|
]);
|
|
2143
|
-
var
|
|
2144
|
-
"valid",
|
|
2145
|
-
"invalid",
|
|
2146
|
-
"infrastructure-error"
|
|
2147
|
-
]);
|
|
2148
|
-
var TERMINAL_PUBLICATION_STATES = /* @__PURE__ */ new Set(["published", "failed"]);
|
|
2123
|
+
var TERMINAL_SYNC_STATES = /* @__PURE__ */ new Set(["succeeded", "failed"]);
|
|
2149
2124
|
var TERMINAL_REJUDGE_STATES = /* @__PURE__ */ new Set([
|
|
2150
2125
|
"effective",
|
|
2151
2126
|
"failed",
|
|
@@ -2202,11 +2177,6 @@ async function runInput(command, cwd) {
|
|
|
2202
2177
|
});
|
|
2203
2178
|
}
|
|
2204
2179
|
}
|
|
2205
|
-
function canonicalTimestamp(value, label) {
|
|
2206
|
-
if (value === void 0) return void 0;
|
|
2207
|
-
if (Number.isNaN(Date.parse(value)) || new Date(value).toISOString() !== value) throw usageError(`--${label} must be a canonical ISO timestamp.`);
|
|
2208
|
-
return value;
|
|
2209
|
-
}
|
|
2210
2180
|
function canonicalCursorTimestamp(value, label) {
|
|
2211
2181
|
if (Number.isNaN(Date.parse(value)) || new Date(value).toISOString() !== value) throw usageError(`${label} timestamp is invalid.`);
|
|
2212
2182
|
return value;
|
|
@@ -2404,13 +2374,28 @@ async function retryTurnstile(action, dependencies, origin) {
|
|
|
2404
2374
|
code: "turnstile-expired"
|
|
2405
2375
|
});
|
|
2406
2376
|
}
|
|
2407
|
-
async function exactPublicProblem(client,
|
|
2408
|
-
const metadataPath = `/api/problems/${
|
|
2377
|
+
async function exactPublicProblem(client, problemId, contestId) {
|
|
2378
|
+
const metadataPath = `/api/problems/${problemId}${query({ contestId })}`;
|
|
2409
2379
|
const metadata = object(await client.request(metadataPath, { authenticated: contestId ? true : "optional" }), "problem metadata");
|
|
2410
|
-
if (metadata.
|
|
2380
|
+
if (metadata.problemId !== problemId || metadata.schema !== "wasm-oj-platform/problem-content-pointer/v2" || typeof metadata.catalogCommit !== "string" || !/^[0-9a-f]{40}$/.test(metadata.catalogCommit)) throw new CliError("Problem metadata identity is invalid.", { exitCode: 4 });
|
|
2411
2381
|
const content = object(metadata.content, "problem content pointer");
|
|
2412
2382
|
const contentUrl = field(content, "url", "problem content URL");
|
|
2413
2383
|
const contentSha256 = field(content, "sha256", "problem content digest");
|
|
2384
|
+
const submissionContext = contestId ? (() => {
|
|
2385
|
+
const admission = object(metadata.contestAdmission, "contest admission");
|
|
2386
|
+
for (const key of [
|
|
2387
|
+
"timelineGeneration",
|
|
2388
|
+
"ruleEpoch",
|
|
2389
|
+
"problemEpoch"
|
|
2390
|
+
]) if (!Number.isSafeInteger(admission[key]) || admission[key] < 1) throw new CliError(`Contest admission ${key} is invalid.`, { exitCode: 4 });
|
|
2391
|
+
return {
|
|
2392
|
+
kind: "contest",
|
|
2393
|
+
contestId,
|
|
2394
|
+
timelineGeneration: admission.timelineGeneration,
|
|
2395
|
+
ruleEpoch: admission.ruleEpoch,
|
|
2396
|
+
problemEpoch: admission.problemEpoch
|
|
2397
|
+
};
|
|
2398
|
+
})() : { kind: "practice" };
|
|
2414
2399
|
if (!/^[0-9a-f]{64}$/.test(contentSha256) || !Number.isSafeInteger(content.bytes) || content.bytes < 1) throw new CliError("Problem content pointer is invalid.", { exitCode: 4 });
|
|
2415
2400
|
const bytes = await client.requestBytes(contentUrl, { authenticated: contestId ? true : "optional" });
|
|
2416
2401
|
if (bytes.byteLength !== content.bytes || createHash("sha256").update(bytes).digest("hex") !== contentSha256) throw new CliError("Downloaded problem bytes disagree with their immutable pointer.", {
|
|
@@ -2421,12 +2406,13 @@ async function exactPublicProblem(client, problemVersionId, contestId) {
|
|
|
2421
2406
|
metadata,
|
|
2422
2407
|
content,
|
|
2423
2408
|
bytes,
|
|
2424
|
-
problem: parsePublicProblem(bytes)
|
|
2409
|
+
problem: parsePublicProblem(bytes),
|
|
2410
|
+
submissionContext
|
|
2425
2411
|
};
|
|
2426
2412
|
}
|
|
2427
2413
|
function localizedList(value, locale) {
|
|
2428
2414
|
const response = object(value, "problem list");
|
|
2429
|
-
const
|
|
2415
|
+
const catalogs = array(response.catalogs, "problem catalogs").map((collectionValue) => {
|
|
2430
2416
|
const collection = object(collectionValue, "problem collection");
|
|
2431
2417
|
const problems = array(collection.problems, "collection problems").map((problemValue) => {
|
|
2432
2418
|
const problem = object(problemValue, "problem summary");
|
|
@@ -2447,19 +2433,19 @@ function localizedList(value, locale) {
|
|
|
2447
2433
|
return {
|
|
2448
2434
|
...response,
|
|
2449
2435
|
locale,
|
|
2450
|
-
|
|
2436
|
+
catalogs
|
|
2451
2437
|
};
|
|
2452
2438
|
}
|
|
2453
2439
|
async function problemPull(command, dependencies, client) {
|
|
2454
|
-
const [
|
|
2455
|
-
uuid(
|
|
2440
|
+
const [problemId, directory = "."] = exactPositionals(command, 1, 2);
|
|
2441
|
+
uuid(problemId, "problem-id");
|
|
2456
2442
|
const contestId = stringOption(command, "contest");
|
|
2457
2443
|
if (contestId) uuid(contestId, "contest");
|
|
2458
2444
|
const locale = localeOption(command);
|
|
2459
2445
|
const language = stringOption(command, "language");
|
|
2460
2446
|
if (!language || !LANGUAGES.includes(language)) throw usageError("problem pull requires a supported --language.");
|
|
2461
|
-
const { metadata, content, bytes, problem } = await exactPublicProblem(client,
|
|
2462
|
-
const
|
|
2447
|
+
const { metadata, content, bytes, problem, submissionContext } = await exactPublicProblem(client, problemId, contestId);
|
|
2448
|
+
const catalogCommit = field(metadata, "catalogCommit", "catalogCommit");
|
|
2463
2449
|
const contentUrl = field(content, "url", "problem content URL");
|
|
2464
2450
|
const contentSha256 = field(content, "sha256", "problem content digest");
|
|
2465
2451
|
const profile = object(object(metadata.allowedProfiles, "allowedProfiles")[language], `allowedProfiles.${language}`);
|
|
@@ -2488,21 +2474,21 @@ async function problemPull(command, dependencies, client) {
|
|
|
2488
2474
|
entry: template.entry,
|
|
2489
2475
|
sources: Object.keys(template.files).sort(),
|
|
2490
2476
|
problem: {
|
|
2491
|
-
|
|
2492
|
-
|
|
2477
|
+
problemId,
|
|
2478
|
+
catalogCommit,
|
|
2493
2479
|
serverOrigin: client.origin,
|
|
2494
2480
|
contentUrl,
|
|
2495
2481
|
contentSha256,
|
|
2496
2482
|
contentFile: "problem.json",
|
|
2497
2483
|
locale,
|
|
2498
|
-
|
|
2484
|
+
context: submissionContext
|
|
2499
2485
|
}
|
|
2500
2486
|
});
|
|
2501
2487
|
return {
|
|
2502
2488
|
value: {
|
|
2503
2489
|
directory: root,
|
|
2504
|
-
|
|
2505
|
-
|
|
2490
|
+
problemId,
|
|
2491
|
+
catalogCommit,
|
|
2506
2492
|
contentSha256,
|
|
2507
2493
|
language,
|
|
2508
2494
|
target: profile.target,
|
|
@@ -2532,13 +2518,25 @@ async function submit(command, dependencies, client) {
|
|
|
2532
2518
|
exitCode: 5,
|
|
2533
2519
|
code: "profile-pin-mismatch"
|
|
2534
2520
|
});
|
|
2535
|
-
const
|
|
2521
|
+
const pinnedContestId = workspace.problem.context.kind === "contest" ? workspace.problem.context.contestId : void 0;
|
|
2522
|
+
const contestId = stringOption(command, "contest") ?? pinnedContestId;
|
|
2536
2523
|
if (contestId !== void 0) uuid(contestId, "contest");
|
|
2537
|
-
if (contestId !==
|
|
2524
|
+
if (contestId !== pinnedContestId) throw new CliError("Official Submit contest context must match the pinned workspace.", { exitCode: 5 });
|
|
2538
2525
|
const sources = await readWorkspaceSources(dependencies.cwd, workspace);
|
|
2539
2526
|
const body = {
|
|
2540
|
-
|
|
2541
|
-
|
|
2527
|
+
context: workspace.problem.context.kind === "contest" ? {
|
|
2528
|
+
kind: "contest",
|
|
2529
|
+
contestId: workspace.problem.context.contestId,
|
|
2530
|
+
problemId: workspace.problem.problemId,
|
|
2531
|
+
contentCommit: workspace.problem.catalogCommit,
|
|
2532
|
+
timelineGeneration: workspace.problem.context.timelineGeneration,
|
|
2533
|
+
ruleEpoch: workspace.problem.context.ruleEpoch,
|
|
2534
|
+
problemEpoch: workspace.problem.context.problemEpoch
|
|
2535
|
+
} : {
|
|
2536
|
+
kind: "practice",
|
|
2537
|
+
problemId: workspace.problem.problemId,
|
|
2538
|
+
catalogCommit: workspace.problem.catalogCommit
|
|
2539
|
+
},
|
|
2542
2540
|
language,
|
|
2543
2541
|
target,
|
|
2544
2542
|
optimization,
|
|
@@ -2560,69 +2558,14 @@ async function submit(command, dependencies, client) {
|
|
|
2560
2558
|
};
|
|
2561
2559
|
return submissionWatch(client, serverUuid(object(created, "submission creation"), "submissionId"), 2e3, dependencies.sleep);
|
|
2562
2560
|
}
|
|
2563
|
-
async function currentContestDraft(client, id) {
|
|
2564
|
-
const value = object(await client.request(`/api/organizer/contests/${id}`), "Organizer contest");
|
|
2565
|
-
const contest = object(value.contest, "contest");
|
|
2566
|
-
const problems = array(value.problems, "contest problems");
|
|
2567
|
-
const freezeAt = contest.freezeAt;
|
|
2568
|
-
if (freezeAt !== null && freezeAt !== void 0 && typeof freezeAt !== "string") throw new CliError("Organizer contest freezeAt has an invalid shape.", {
|
|
2569
|
-
exitCode: 6,
|
|
2570
|
-
code: "server-response-invalid"
|
|
2571
|
-
});
|
|
2572
|
-
return {
|
|
2573
|
-
current: value,
|
|
2574
|
-
body: {
|
|
2575
|
-
title: contest.title,
|
|
2576
|
-
description: contest.description,
|
|
2577
|
-
accessMode: contest.accessMode,
|
|
2578
|
-
startsAt: contest.startsAt,
|
|
2579
|
-
endsAt: contest.endsAt,
|
|
2580
|
-
...typeof freezeAt === "string" ? { freezeAt } : {},
|
|
2581
|
-
problemVersionIds: problems.map((problem) => field(object(problem, "contest problem"), "problemVersionId"))
|
|
2582
|
-
}
|
|
2583
|
-
};
|
|
2584
|
-
}
|
|
2585
|
-
async function contestBody(command, cwd, base = {}) {
|
|
2586
|
-
const body = { ...base };
|
|
2587
|
-
for (const [option, key] of Object.entries({
|
|
2588
|
-
title: "title",
|
|
2589
|
-
description: "description",
|
|
2590
|
-
access: "accessMode",
|
|
2591
|
-
starts: "startsAt",
|
|
2592
|
-
ends: "endsAt",
|
|
2593
|
-
freeze: "freezeAt"
|
|
2594
|
-
})) {
|
|
2595
|
-
const value = stringOption(command, option);
|
|
2596
|
-
if (value !== void 0) body[key] = value;
|
|
2597
|
-
}
|
|
2598
|
-
const inviteCode = await readProtectedTextFile(cwd, stringOption(command, "invite-code-file"), "--invite-code-file");
|
|
2599
|
-
if (inviteCode !== void 0) body.inviteCode = inviteCode;
|
|
2600
|
-
const problems = repeatableOption(command, "problem");
|
|
2601
|
-
if (problems.length) body.problemVersionIds = problems.map((id) => uuid(id, "problem"));
|
|
2602
|
-
if (typeof body.title === "string" && (!body.title.trim() || body.title.length > 120)) throw usageError("--title must contain 1–120 characters.");
|
|
2603
|
-
if (typeof body.description === "string" && body.description.length > 1e4) throw usageError("--description must contain at most 10,000 characters.");
|
|
2604
|
-
if (Array.isArray(body.problemVersionIds) && (body.problemVersionIds.length < 1 || body.problemVersionIds.length > 100 || new Set(body.problemVersionIds).size !== body.problemVersionIds.length)) throw usageError("Contest problem IDs must be unique and contain 1–100 values.");
|
|
2605
|
-
if (body.accessMode !== "public" && body.accessMode !== "invite") throw usageError("--access must be public or invite.");
|
|
2606
|
-
const invite = body.inviteCode;
|
|
2607
|
-
if (invite !== void 0 && (typeof invite !== "string" || invite.length < 16 || invite.length > 128)) throw usageError("--invite-code-file must contain 16–128 characters.");
|
|
2608
|
-
if (body.accessMode === "public" && invite !== void 0) throw usageError("--invite-code-file cannot be used with --access public.");
|
|
2609
|
-
const startsAt = canonicalTimestamp(typeof body.startsAt === "string" ? body.startsAt : void 0, "starts");
|
|
2610
|
-
const endsAt = canonicalTimestamp(typeof body.endsAt === "string" ? body.endsAt : void 0, "ends");
|
|
2611
|
-
const freezeAt = canonicalTimestamp(typeof body.freezeAt === "string" ? body.freezeAt : void 0, "freeze");
|
|
2612
|
-
if (startsAt !== void 0 && endsAt !== void 0 && (endsAt <= startsAt || freezeAt !== void 0 && (freezeAt <= startsAt || freezeAt >= endsAt))) throw usageError("Contest timestamps must satisfy starts < freeze < ends (freeze is optional).");
|
|
2613
|
-
return body;
|
|
2614
|
-
}
|
|
2615
2561
|
async function createCollectionSkeleton(root, force) {
|
|
2616
2562
|
const file = path.join(root, "collection", "source.json");
|
|
2617
2563
|
await assertSafeFileDestinations(root, ["collection/source.json"], force);
|
|
2618
2564
|
await mkdir(path.dirname(file), { recursive: true });
|
|
2619
2565
|
await atomicWriteFile(file, `${JSON.stringify({
|
|
2620
|
-
schema: "wasm-oj-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
supportedLocales: ["zh-TW", "en"]
|
|
2624
|
-
},
|
|
2625
|
-
problems: []
|
|
2566
|
+
schema: "wasm-oj-platform/repository-authoring/v1",
|
|
2567
|
+
problems: [],
|
|
2568
|
+
contests: []
|
|
2626
2569
|
}, null, 2)}\n`);
|
|
2627
2570
|
return {
|
|
2628
2571
|
value: {
|
|
@@ -2877,16 +2820,8 @@ async function dispatchCommand(command, dependencies) {
|
|
|
2877
2820
|
if (key === "organizer collection build" || key === "organizer collection verify") {
|
|
2878
2821
|
const [directory = "."] = exactPositionals(command, 0, 1);
|
|
2879
2822
|
const arguments_ = [key.endsWith("build") ? "build" : "verify", path.resolve(dependencies.cwd, directory)];
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
"source",
|
|
2883
|
-
"managed",
|
|
2884
|
-
"managed-source"
|
|
2885
|
-
]) {
|
|
2886
|
-
const value = stringOption(command, name);
|
|
2887
|
-
if (value !== void 0) arguments_.push(`--${name}`, normalizedRelativePath(value, `--${name}`));
|
|
2888
|
-
}
|
|
2889
|
-
if (key.endsWith("build") && stringOption(command, "managed") !== void 0 && stringOption(command, "managed-source") === void 0) throw usageError("--managed requires --managed-source when building.");
|
|
2823
|
+
const source = stringOption(command, "source");
|
|
2824
|
+
if (source !== void 0) arguments_.push("--source", normalizedRelativePath(source, "--source"));
|
|
2890
2825
|
try {
|
|
2891
2826
|
await dependencies.collectionCli(arguments_);
|
|
2892
2827
|
} catch (error) {
|
|
@@ -2946,7 +2881,7 @@ async function dispatchCommand(command, dependencies) {
|
|
|
2946
2881
|
}
|
|
2947
2882
|
if (key === "problem show") {
|
|
2948
2883
|
const [id] = exactPositionals(command, 1);
|
|
2949
|
-
uuid(id, "problem-
|
|
2884
|
+
uuid(id, "problem-id");
|
|
2950
2885
|
const contestId = stringOption(command, "contest");
|
|
2951
2886
|
if (contestId) uuid(contestId, "contest");
|
|
2952
2887
|
const locale = localeOption(command);
|
|
@@ -2954,8 +2889,9 @@ async function dispatchCommand(command, dependencies) {
|
|
|
2954
2889
|
const problem = downloaded.problem;
|
|
2955
2890
|
return {
|
|
2956
2891
|
value: {
|
|
2957
|
-
|
|
2958
|
-
|
|
2892
|
+
problemId: id,
|
|
2893
|
+
catalogCommit: downloaded.metadata.catalogCommit,
|
|
2894
|
+
judgeDigest: downloaded.metadata.judgeDigest,
|
|
2959
2895
|
content: downloaded.metadata.content,
|
|
2960
2896
|
locale,
|
|
2961
2897
|
title: problem.title[locale],
|
|
@@ -3067,7 +3003,7 @@ async function dispatchCommand(command, dependencies) {
|
|
|
3067
3003
|
}
|
|
3068
3004
|
if (key === "performance frontier" || key === "performance evolution") {
|
|
3069
3005
|
const [id] = exactPositionals(command, 1);
|
|
3070
|
-
uuid(id, "problem-
|
|
3006
|
+
uuid(id, "problem-id");
|
|
3071
3007
|
const language = stringOption(command, "language");
|
|
3072
3008
|
if (language !== void 0 && !LANGUAGES.includes(language)) throw usageError("--language must name a supported language.");
|
|
3073
3009
|
const contestId = stringOption(command, "contest");
|
|
@@ -3103,138 +3039,83 @@ async function dispatchCommand(command, dependencies) {
|
|
|
3103
3039
|
exitCode: 0
|
|
3104
3040
|
};
|
|
3105
3041
|
}
|
|
3106
|
-
if (key === "organizer
|
|
3042
|
+
if (key === "organizer catalog list") {
|
|
3107
3043
|
exactPositionals(command, 0);
|
|
3108
3044
|
return {
|
|
3109
|
-
value: await client.request("/api/organizer/
|
|
3045
|
+
value: await client.request("/api/organizer/catalogs"),
|
|
3110
3046
|
exitCode: 0
|
|
3111
3047
|
};
|
|
3112
3048
|
}
|
|
3113
|
-
if (key === "organizer
|
|
3049
|
+
if (key === "organizer catalog show") {
|
|
3114
3050
|
const [id] = exactPositionals(command, 1);
|
|
3115
|
-
uuid(id, "
|
|
3051
|
+
uuid(id, "catalog-id");
|
|
3116
3052
|
return {
|
|
3117
|
-
value: await client.request(`/api/organizer/
|
|
3053
|
+
value: await client.request(`/api/organizer/catalogs/${id}`),
|
|
3118
3054
|
exitCode: 0
|
|
3119
3055
|
};
|
|
3120
3056
|
}
|
|
3121
|
-
if (key === "organizer
|
|
3057
|
+
if (key === "organizer catalog connect") {
|
|
3122
3058
|
exactPositionals(command, 0);
|
|
3123
3059
|
const repository = stringOption(command, "repo");
|
|
3124
|
-
const index = stringOption(command, "index") ?? "collection/index.json";
|
|
3125
3060
|
if (!repository) throw usageError("--repo must be a GitHub numeric repository ID.");
|
|
3126
3061
|
const repositoryId = positiveInteger(repository, 1, Number.MAX_SAFE_INTEGER, "--repo");
|
|
3127
3062
|
return {
|
|
3128
|
-
value: await client.request("/api/organizer/
|
|
3063
|
+
value: await client.request("/api/organizer/catalogs", {
|
|
3129
3064
|
method: "POST",
|
|
3130
|
-
body: {
|
|
3131
|
-
githubRepositoryId: repositoryId,
|
|
3132
|
-
indexPath: normalizedRelativePath(index, "--index")
|
|
3133
|
-
}
|
|
3065
|
+
body: { githubRepositoryId: repositoryId }
|
|
3134
3066
|
}),
|
|
3135
3067
|
exitCode: 0
|
|
3136
3068
|
};
|
|
3137
3069
|
}
|
|
3138
|
-
if (key === "organizer
|
|
3070
|
+
if (key === "organizer catalog sync") {
|
|
3139
3071
|
const [id] = exactPositionals(command, 1);
|
|
3140
|
-
uuid(id, "
|
|
3072
|
+
uuid(id, "catalog-id");
|
|
3141
3073
|
const ref = stringOption(command, "ref");
|
|
3142
3074
|
if (!ref || ref.length > 256 || /[\u0000-\u001f\u007f]/u.test(ref)) throw usageError("--ref must be a 1–256 character printable Git ref.");
|
|
3143
|
-
const created = await client.request(`/api/organizer/
|
|
3144
|
-
method: "POST",
|
|
3145
|
-
body: { ref }
|
|
3146
|
-
});
|
|
3147
|
-
if (!booleanOption(command, "wait")) return {
|
|
3148
|
-
value: created,
|
|
3149
|
-
exitCode: 0
|
|
3150
|
-
};
|
|
3151
|
-
const validation = object(object(created, "validation creation").validation, "validation");
|
|
3152
|
-
const initialState = field(validation, "state", "validation state");
|
|
3153
|
-
if (TERMINAL_VALIDATION_STATES.has(initialState)) return {
|
|
3154
|
-
value: created,
|
|
3155
|
-
exitCode: initialState === "valid" ? 0 : 1
|
|
3156
|
-
};
|
|
3157
|
-
return watchResource({
|
|
3158
|
-
client,
|
|
3159
|
-
path: `/api/organizer/validations/${serverUuid(validation, "id", "validation ID")}`,
|
|
3160
|
-
envelope: "validation",
|
|
3161
|
-
terminal: TERMINAL_VALIDATION_STATES,
|
|
3162
|
-
success: /* @__PURE__ */ new Set(["valid"]),
|
|
3163
|
-
intervalMs: 2e3,
|
|
3164
|
-
sleep: dependencies.sleep
|
|
3165
|
-
});
|
|
3166
|
-
}
|
|
3167
|
-
if (key === "organizer collection validation") {
|
|
3168
|
-
const [id] = exactPositionals(command, 1);
|
|
3169
|
-
uuid(id, "validation-id");
|
|
3170
|
-
if (!booleanOption(command, "watch")) return {
|
|
3171
|
-
value: await client.request(`/api/organizer/validations/${id}`),
|
|
3172
|
-
exitCode: 0
|
|
3173
|
-
};
|
|
3174
|
-
return watchResource({
|
|
3175
|
-
client,
|
|
3176
|
-
path: `/api/organizer/validations/${id}`,
|
|
3177
|
-
envelope: "validation",
|
|
3178
|
-
terminal: TERMINAL_VALIDATION_STATES,
|
|
3179
|
-
success: /* @__PURE__ */ new Set(["valid"]),
|
|
3180
|
-
intervalMs: positiveInteger(stringOption(command, "interval"), 2, 30, "interval") * 1e3,
|
|
3181
|
-
sleep: dependencies.sleep
|
|
3182
|
-
});
|
|
3183
|
-
}
|
|
3184
|
-
if (key === "organizer collection publish") {
|
|
3185
|
-
const [id] = exactPositionals(command, 1);
|
|
3186
|
-
uuid(id, "revision-id");
|
|
3187
|
-
const mode = stringOption(command, "mode") ?? "official-practice";
|
|
3188
|
-
if (mode !== "official-practice" && mode !== "contest") throw usageError("--mode must be official-practice or contest.");
|
|
3189
|
-
const created = await client.request(`/api/organizer/revisions/${id}/publications`, {
|
|
3075
|
+
const created = await client.request(`/api/organizer/catalogs/${id}/syncs`, {
|
|
3190
3076
|
method: "POST",
|
|
3191
3077
|
body: {
|
|
3192
|
-
|
|
3193
|
-
idempotencyKey: `woj-
|
|
3078
|
+
ref,
|
|
3079
|
+
idempotencyKey: `woj-sync-${randomUUID()}`
|
|
3194
3080
|
}
|
|
3195
3081
|
});
|
|
3196
3082
|
if (!booleanOption(command, "wait")) return {
|
|
3197
3083
|
value: created,
|
|
3198
3084
|
exitCode: 0
|
|
3199
3085
|
};
|
|
3086
|
+
const sync = object(object(created, "sync creation").sync, "sync");
|
|
3087
|
+
const initialState = field(sync, "state", "sync state");
|
|
3088
|
+
if (TERMINAL_SYNC_STATES.has(initialState)) return {
|
|
3089
|
+
value: created,
|
|
3090
|
+
exitCode: initialState === "succeeded" ? 0 : 1
|
|
3091
|
+
};
|
|
3200
3092
|
return watchResource({
|
|
3201
3093
|
client,
|
|
3202
|
-
path: `/api/organizer/
|
|
3203
|
-
envelope: "
|
|
3204
|
-
terminal:
|
|
3205
|
-
success: /* @__PURE__ */ new Set(["
|
|
3094
|
+
path: `/api/organizer/catalog-syncs/${serverUuid(sync, "id", "sync ID")}`,
|
|
3095
|
+
envelope: "sync",
|
|
3096
|
+
terminal: TERMINAL_SYNC_STATES,
|
|
3097
|
+
success: /* @__PURE__ */ new Set(["succeeded"]),
|
|
3206
3098
|
intervalMs: 2e3,
|
|
3207
3099
|
sleep: dependencies.sleep
|
|
3208
3100
|
});
|
|
3209
3101
|
}
|
|
3210
|
-
if (key === "organizer
|
|
3102
|
+
if (key === "organizer catalog sync-show") {
|
|
3211
3103
|
const [id] = exactPositionals(command, 1);
|
|
3212
|
-
uuid(id, "
|
|
3104
|
+
uuid(id, "sync-id");
|
|
3213
3105
|
if (!booleanOption(command, "watch")) return {
|
|
3214
|
-
value: await client.request(`/api/organizer/
|
|
3106
|
+
value: await client.request(`/api/organizer/catalog-syncs/${id}`),
|
|
3215
3107
|
exitCode: 0
|
|
3216
3108
|
};
|
|
3217
3109
|
return watchResource({
|
|
3218
3110
|
client,
|
|
3219
|
-
path: `/api/organizer/
|
|
3220
|
-
envelope: "
|
|
3221
|
-
terminal:
|
|
3222
|
-
success: /* @__PURE__ */ new Set(["
|
|
3111
|
+
path: `/api/organizer/catalog-syncs/${id}`,
|
|
3112
|
+
envelope: "sync",
|
|
3113
|
+
terminal: TERMINAL_SYNC_STATES,
|
|
3114
|
+
success: /* @__PURE__ */ new Set(["succeeded"]),
|
|
3223
3115
|
intervalMs: positiveInteger(stringOption(command, "interval"), 2, 30, "interval") * 1e3,
|
|
3224
3116
|
sleep: dependencies.sleep
|
|
3225
3117
|
});
|
|
3226
3118
|
}
|
|
3227
|
-
if (key === "organizer collection activate") {
|
|
3228
|
-
const [id] = exactPositionals(command, 1);
|
|
3229
|
-
uuid(id, "publication-id");
|
|
3230
|
-
return {
|
|
3231
|
-
value: await client.request(`/api/organizer/publications/${id}/activate`, {
|
|
3232
|
-
method: "POST",
|
|
3233
|
-
body: {}
|
|
3234
|
-
}),
|
|
3235
|
-
exitCode: 0
|
|
3236
|
-
};
|
|
3237
|
-
}
|
|
3238
3119
|
if (key === "organizer contest list") {
|
|
3239
3120
|
exactPositionals(command, 0);
|
|
3240
3121
|
return {
|
|
@@ -3250,68 +3131,15 @@ async function dispatchCommand(command, dependencies) {
|
|
|
3250
3131
|
exitCode: 0
|
|
3251
3132
|
};
|
|
3252
3133
|
}
|
|
3253
|
-
if (key === "organizer contest
|
|
3254
|
-
exactPositionals(command, 0);
|
|
3255
|
-
const body = await contestBody(command, dependencies.cwd);
|
|
3256
|
-
for (const name of [
|
|
3257
|
-
"title",
|
|
3258
|
-
"startsAt",
|
|
3259
|
-
"endsAt",
|
|
3260
|
-
"accessMode",
|
|
3261
|
-
"problemVersionIds"
|
|
3262
|
-
]) if (body[name] === void 0) throw usageError(`contest create requires ${name}.`);
|
|
3263
|
-
if (body.description === void 0) body.description = "";
|
|
3264
|
-
if (body.accessMode === "invite" && body.inviteCode === void 0) throw usageError("Invite contests require --invite-code-file.");
|
|
3265
|
-
return {
|
|
3266
|
-
value: await client.request("/api/contests", {
|
|
3267
|
-
method: "POST",
|
|
3268
|
-
body
|
|
3269
|
-
}),
|
|
3270
|
-
exitCode: 0
|
|
3271
|
-
};
|
|
3272
|
-
}
|
|
3273
|
-
if (key === "organizer contest update") {
|
|
3274
|
-
const [contestId] = exactPositionals(command, 1);
|
|
3275
|
-
const id = uuid(contestId, "contest-id");
|
|
3276
|
-
const draft = await currentContestDraft(client, id);
|
|
3277
|
-
return {
|
|
3278
|
-
value: await client.request(`/api/organizer/contests/${id}`, {
|
|
3279
|
-
method: "PUT",
|
|
3280
|
-
body: await contestBody(command, dependencies.cwd, draft.body)
|
|
3281
|
-
}),
|
|
3282
|
-
exitCode: 0
|
|
3283
|
-
};
|
|
3284
|
-
}
|
|
3285
|
-
if (key === "organizer contest add-problem" || key === "organizer contest remove-problem") {
|
|
3286
|
-
const [contestId, problemVersionId] = exactPositionals(command, 2);
|
|
3287
|
-
const id = uuid(contestId, "contest-id");
|
|
3288
|
-
const problem = uuid(problemVersionId, "problem-version-id");
|
|
3289
|
-
return {
|
|
3290
|
-
value: await client.request(`/api/organizer/contests/${id}/problems/${problem}`, {
|
|
3291
|
-
method: key.endsWith("add-problem") ? "POST" : "DELETE",
|
|
3292
|
-
body: {}
|
|
3293
|
-
}),
|
|
3294
|
-
exitCode: 0
|
|
3295
|
-
};
|
|
3296
|
-
}
|
|
3297
|
-
if (key === "organizer contest publish") {
|
|
3134
|
+
if (key === "organizer contest invite-rotate") {
|
|
3298
3135
|
const [id] = exactPositionals(command, 1);
|
|
3299
3136
|
uuid(id, "contest-id");
|
|
3137
|
+
const inviteCode = await readProtectedTextFile(dependencies.cwd, stringOption(command, "code-file"), "--code-file");
|
|
3138
|
+
if (!inviteCode || inviteCode.length < 16 || inviteCode.length > 128) throw usageError("invite-rotate requires --code-file containing 16–128 characters.");
|
|
3300
3139
|
return {
|
|
3301
|
-
value: await client.request(`/api/contests/${id}/
|
|
3140
|
+
value: await client.request(`/api/organizer/contests/${id}/invite-code`, {
|
|
3302
3141
|
method: "POST",
|
|
3303
|
-
body: {}
|
|
3304
|
-
}),
|
|
3305
|
-
exitCode: 0
|
|
3306
|
-
};
|
|
3307
|
-
}
|
|
3308
|
-
if (key === "organizer contest archive") {
|
|
3309
|
-
const [id] = exactPositionals(command, 1);
|
|
3310
|
-
uuid(id, "contest-id");
|
|
3311
|
-
return {
|
|
3312
|
-
value: await client.request(`/api/organizer/contests/${id}/archive`, {
|
|
3313
|
-
method: "POST",
|
|
3314
|
-
body: {}
|
|
3142
|
+
body: { inviteCode }
|
|
3315
3143
|
}),
|
|
3316
3144
|
exitCode: 0
|
|
3317
3145
|
};
|
|
@@ -3342,25 +3170,34 @@ async function dispatchCommand(command, dependencies) {
|
|
|
3342
3170
|
};
|
|
3343
3171
|
}
|
|
3344
3172
|
if (key === "organizer rejudge options") {
|
|
3345
|
-
const [
|
|
3346
|
-
|
|
3173
|
+
const [problemId] = exactPositionals(command, 0, 1);
|
|
3174
|
+
const fromCommit = stringOption(command, "from");
|
|
3175
|
+
if (problemId === void 0 !== (fromCommit === void 0)) throw usageError("rejudge options requires both [problem-id] and --from, or neither.");
|
|
3176
|
+
if (problemId) uuid(problemId, "problem-id");
|
|
3177
|
+
if (fromCommit && !/^[0-9a-f]{40}$/.test(fromCommit)) throw usageError("--from must be an exact lowercase Git commit.");
|
|
3347
3178
|
return {
|
|
3348
|
-
value: await client.request(`/api/organizer/rejudges/options${query({
|
|
3179
|
+
value: await client.request(`/api/organizer/rejudges/options${query({
|
|
3180
|
+
problemId,
|
|
3181
|
+
fromCommit
|
|
3182
|
+
})}`),
|
|
3349
3183
|
exitCode: 0
|
|
3350
3184
|
};
|
|
3351
3185
|
}
|
|
3352
3186
|
if (key === "organizer rejudge start") {
|
|
3353
|
-
exactPositionals(command,
|
|
3187
|
+
const [problemId] = exactPositionals(command, 1);
|
|
3188
|
+
uuid(problemId, "problem-id");
|
|
3354
3189
|
const from = stringOption(command, "from");
|
|
3355
3190
|
const to = stringOption(command, "to");
|
|
3356
|
-
if (!from || !to) throw usageError("rejudge start requires --from and --to.");
|
|
3357
|
-
|
|
3358
|
-
uuid(
|
|
3191
|
+
if (!from || !to || !/^[0-9a-f]{40}$/.test(from) || !/^[0-9a-f]{40}$/.test(to)) throw usageError("rejudge start requires exact lowercase Git commits in --from and --to.");
|
|
3192
|
+
const contestId = stringOption(command, "contest");
|
|
3193
|
+
if (contestId) uuid(contestId, "contest-id");
|
|
3359
3194
|
const created = await client.request("/api/organizer/rejudges", {
|
|
3360
3195
|
method: "POST",
|
|
3361
3196
|
body: {
|
|
3362
|
-
|
|
3363
|
-
|
|
3197
|
+
problemId,
|
|
3198
|
+
fromCommit: from,
|
|
3199
|
+
toCommit: to,
|
|
3200
|
+
...contestId ? { contestId } : {},
|
|
3364
3201
|
idempotencyKey: `woj-rejudge-${randomUUID()}`
|
|
3365
3202
|
}
|
|
3366
3203
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wasm-oj/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The local-first woj CLI for students and WASM-OJ Organizers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "JacobLinCool",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@napi-rs/keyring": "1.3.0",
|
|
51
|
-
"@wasm-oj/core": "0.2.
|
|
52
|
-
"@wasm-oj/organizer": "0.2.
|
|
53
|
-
"@wasm-oj/server": "0.2.
|
|
51
|
+
"@wasm-oj/core": "0.2.1",
|
|
52
|
+
"@wasm-oj/organizer": "0.2.1",
|
|
53
|
+
"@wasm-oj/server": "0.2.1"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "node ../../scripts/build-library.mjs --package @wasm-oj/cli"
|