@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 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-version-id> --language cpp --locale zh-TW
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.0";
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-v1";
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 problemVersionId: string;
235
- readonly catalogPublicationId: string;
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 contestId?: string;
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.0";
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 problem versions.", "", { locale: "string" }),
79
- remote("problem show", "Show one public problem version.", "<problem-version-id>", {
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 exact public problem version.", "<problem-version-id> [directory]", {
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 an invite contest.", "<contest-id>", { "code-file": "string" }),
103
- remote("contest problems", "List the exact problem versions in a contest.", "<contest-id>"),
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-version-id>", {
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-version-id>", {
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 deterministic collection artifacts locally.", "[directory]", {
131
- index: "string",
132
- source: "string",
133
- managed: "string",
134
- "managed-source": "string"
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 collection validation", "Show or watch a static validation.", "<validation-id>", {
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 create", "Create a draft contest.", "", {
167
- title: "string",
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 immutable rejudge endpoints.", "<problem-version-id>"),
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-v1";
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-v1") throw new CliError(`Unsupported workspace schema '${String(workspace.schema)}'.`);
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
- "problemVersionId",
1146
- "catalogPublicationId",
1103
+ "problemId",
1104
+ "catalogCommit",
1147
1105
  "serverOrigin",
1148
1106
  "contentUrl",
1149
1107
  "contentSha256",
1150
1108
  "contentFile",
1151
- "locale",
1152
- ...pinned.contestId === void 0 ? [] : ["contestId"]
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.problemVersionId !== "string" || !uuid.test(pinned.problemVersionId)) throw new CliError("Pinned problem version ID is invalid.");
1156
- if (typeof pinned.catalogPublicationId !== "string" || !uuid.test(pinned.catalogPublicationId)) throw new CliError("Pinned catalog publication ID is invalid.");
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
- if (pinned.contestId !== void 0 && (typeof pinned.contestId !== "string" || !uuid.test(pinned.contestId))) throw new CliError("Pinned contest ID is invalid.");
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
- problemVersionId: pinned.problemVersionId,
1171
- catalogPublicationId: pinned.catalogPublicationId,
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
- ...pinned.contestId ? { contestId: pinned.contestId } : {}
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 TERMINAL_VALIDATION_STATES = /* @__PURE__ */ new Set([
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, problemVersionId, contestId) {
2408
- const metadataPath = `/api/problems/${problemVersionId}${query({ contestId })}`;
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.problemVersionId !== problemVersionId || metadata.schema !== "wasm-oj-platform/problem-content-pointer/v2") throw new CliError("Problem metadata identity is invalid.", { exitCode: 4 });
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 collections = array(response.collections, "problem collections").map((collectionValue) => {
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
- collections
2436
+ catalogs
2451
2437
  };
2452
2438
  }
2453
2439
  async function problemPull(command, dependencies, client) {
2454
- const [problemVersionId, directory = "."] = exactPositionals(command, 1, 2);
2455
- uuid(problemVersionId, "problem-version-id");
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, problemVersionId, contestId);
2462
- const catalogPublicationId = serverUuid(metadata, "catalogPublicationId", "catalogPublicationId");
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
- problemVersionId,
2492
- catalogPublicationId,
2477
+ problemId,
2478
+ catalogCommit,
2493
2479
  serverOrigin: client.origin,
2494
2480
  contentUrl,
2495
2481
  contentSha256,
2496
2482
  contentFile: "problem.json",
2497
2483
  locale,
2498
- ...contestId ? { contestId } : {}
2484
+ context: submissionContext
2499
2485
  }
2500
2486
  });
2501
2487
  return {
2502
2488
  value: {
2503
2489
  directory: root,
2504
- problemVersionId,
2505
- catalogPublicationId,
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 contestId = stringOption(command, "contest") ?? workspace.problem.contestId;
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 !== workspace.problem.contestId) throw new CliError("Official Submit contest context must match the pinned workspace.", { exitCode: 5 });
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
- problemVersionId: workspace.problem.problemVersionId,
2541
- ...contestId ? { contestId } : {},
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-browser-collection-source-v1",
2621
- localization: {
2622
- defaultLocale: "zh-TW",
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
- for (const name of [
2881
- "index",
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-version-id");
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
- problemVersionId: id,
2958
- catalogPublicationId: downloaded.metadata.catalogPublicationId,
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-version-id");
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 collection list") {
3042
+ if (key === "organizer catalog list") {
3107
3043
  exactPositionals(command, 0);
3108
3044
  return {
3109
- value: await client.request("/api/organizer/collections"),
3045
+ value: await client.request("/api/organizer/catalogs"),
3110
3046
  exitCode: 0
3111
3047
  };
3112
3048
  }
3113
- if (key === "organizer collection show") {
3049
+ if (key === "organizer catalog show") {
3114
3050
  const [id] = exactPositionals(command, 1);
3115
- uuid(id, "collection-id");
3051
+ uuid(id, "catalog-id");
3116
3052
  return {
3117
- value: await client.request(`/api/organizer/collections/${id}`),
3053
+ value: await client.request(`/api/organizer/catalogs/${id}`),
3118
3054
  exitCode: 0
3119
3055
  };
3120
3056
  }
3121
- if (key === "organizer collection create") {
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/collections", {
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 collection validate") {
3070
+ if (key === "organizer catalog sync") {
3139
3071
  const [id] = exactPositionals(command, 1);
3140
- uuid(id, "collection-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/collections/${id}/validations`, {
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
- mode,
3193
- idempotencyKey: `woj-publish-${randomUUID()}`
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/publications/${serverUuid(object(object(created, "publication creation").publicationJob, "publication job"), "id", "publication job ID")}`,
3203
- envelope: "publication",
3204
- terminal: TERMINAL_PUBLICATION_STATES,
3205
- success: /* @__PURE__ */ new Set(["published"]),
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 collection publication") {
3102
+ if (key === "organizer catalog sync-show") {
3211
3103
  const [id] = exactPositionals(command, 1);
3212
- uuid(id, "publication-job-id");
3104
+ uuid(id, "sync-id");
3213
3105
  if (!booleanOption(command, "watch")) return {
3214
- value: await client.request(`/api/organizer/publications/${id}`),
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/publications/${id}`,
3220
- envelope: "publication",
3221
- terminal: TERMINAL_PUBLICATION_STATES,
3222
- success: /* @__PURE__ */ new Set(["published"]),
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 create") {
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}/publish`, {
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 [source] = exactPositionals(command, 1);
3346
- uuid(source, "problem-version-id");
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({ source })}`),
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, 0);
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
- uuid(from, "from");
3358
- uuid(to, "to");
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
- oldProblemVersionId: from,
3363
- newProblemVersionId: to,
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.0",
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.0",
52
- "@wasm-oj/organizer": "0.2.0",
53
- "@wasm-oj/server": "0.2.0"
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"