@kradle/cli 0.0.17 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +62 -65
  2. package/dist/commands/agent/list.d.ts +4 -0
  3. package/dist/commands/agent/list.js +6 -4
  4. package/dist/commands/challenge/build.d.ts +9 -1
  5. package/dist/commands/challenge/build.js +40 -12
  6. package/dist/commands/challenge/create.d.ts +5 -1
  7. package/dist/commands/challenge/create.js +17 -18
  8. package/dist/commands/challenge/delete.d.ts +4 -1
  9. package/dist/commands/challenge/delete.js +5 -5
  10. package/dist/commands/challenge/list.d.ts +5 -0
  11. package/dist/commands/challenge/list.js +9 -10
  12. package/dist/commands/challenge/run.d.ts +8 -1
  13. package/dist/commands/challenge/run.js +13 -8
  14. package/dist/commands/challenge/watch.d.ts +4 -1
  15. package/dist/commands/challenge/watch.js +8 -8
  16. package/dist/commands/{evaluation → experiment}/create.d.ts +4 -0
  17. package/dist/commands/{evaluation → experiment}/create.js +22 -21
  18. package/dist/commands/{evaluation → experiment}/list.js +17 -19
  19. package/dist/commands/{evaluation → experiment}/run.d.ts +4 -1
  20. package/dist/commands/experiment/run.js +61 -0
  21. package/dist/commands/init.js +2 -2
  22. package/dist/lib/api-client.d.ts +29 -10
  23. package/dist/lib/api-client.js +81 -37
  24. package/dist/lib/arguments.d.ts +3 -2
  25. package/dist/lib/arguments.js +5 -3
  26. package/dist/lib/challenge.d.ts +13 -18
  27. package/dist/lib/challenge.js +58 -62
  28. package/dist/lib/experiment/experimenter.d.ts +87 -0
  29. package/dist/lib/{evaluation/evaluator.js → experiment/experimenter.js} +74 -72
  30. package/dist/lib/{evaluation → experiment}/index.d.ts +1 -1
  31. package/dist/lib/{evaluation → experiment}/index.js +1 -1
  32. package/dist/lib/{evaluation → experiment}/runner.js +2 -1
  33. package/dist/lib/{evaluation → experiment}/tui.d.ts +1 -1
  34. package/dist/lib/{evaluation → experiment}/tui.js +3 -3
  35. package/dist/lib/{evaluation → experiment}/types.d.ts +6 -4
  36. package/dist/lib/{evaluation → experiment}/types.js +4 -3
  37. package/dist/lib/flags.d.ts +47 -0
  38. package/dist/lib/flags.js +63 -0
  39. package/dist/lib/schemas.d.ts +32 -0
  40. package/dist/lib/schemas.js +8 -0
  41. package/dist/lib/utils.d.ts +9 -10
  42. package/dist/lib/utils.js +12 -12
  43. package/oclif.manifest.json +342 -64
  44. package/package.json +5 -6
  45. package/static/challenge.ts +12 -13
  46. package/static/experiment_template.ts +114 -0
  47. package/static/project_template/dev.env +5 -5
  48. package/static/project_template/prod.env +4 -4
  49. package/static/project_template/tsconfig.json +1 -1
  50. package/dist/commands/challenge/multi-upload.d.ts +0 -6
  51. package/dist/commands/challenge/multi-upload.js +0 -80
  52. package/dist/commands/evaluation/run.js +0 -61
  53. package/dist/lib/config.d.ts +0 -12
  54. package/dist/lib/config.js +0 -49
  55. package/dist/lib/evaluation/evaluator.d.ts +0 -88
  56. package/static/evaluation_template.ts +0 -69
  57. /package/dist/commands/{evaluation → experiment}/list.d.ts +0 -0
  58. /package/dist/lib/{evaluation → experiment}/runner.d.ts +0 -0
@@ -6,27 +6,27 @@ import pc from "picocolors";
6
6
  import { ApiClient } from "../../lib/api-client.js";
7
7
  import { getChallengeSlugArgument } from "../../lib/arguments.js";
8
8
  import { Challenge, SOURCE_FOLDER } from "../../lib/challenge.js";
9
- import { loadConfig } from "../../lib/config.js";
9
+ import { getConfigFlags } from "../../lib/flags.js";
10
10
  import { clearScreen, debounced } from "../../lib/utils.js";
11
11
  export default class Watch extends Command {
12
12
  static description = "Watch a challenge for changes and auto-rebuild/upload";
13
13
  static examples = ["<%= config.bin %> <%= command.id %> my-challenge"];
14
14
  static flags = {
15
15
  verbose: Flags.boolean({ description: "Enable verbose output", default: false }),
16
+ ...getConfigFlags("api-key", "api-url", "challenges-path"),
16
17
  };
17
18
  static args = {
18
- challenge: getChallengeSlugArgument({ description: "Challenge slug to watch" }),
19
+ challengeSlug: getChallengeSlugArgument({ description: "Challenge slug to watch" }),
19
20
  };
20
21
  async run() {
21
22
  const { args, flags } = await this.parse(Watch);
22
- const config = loadConfig();
23
- const challenge = new Challenge(args.challenge, config);
24
- const api = new ApiClient(config);
23
+ const challenge = new Challenge(args.challengeSlug, flags["challenges-path"]);
24
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
25
25
  const debounceSeconds = 1;
26
26
  let building = false;
27
27
  // Perform the initial build and upload
28
28
  this.log(pc.blue(`Performing initial build and upload...`));
29
- let { config: lastConfig, datapackHash: lastHash } = await challenge.buildAndUpload(api);
29
+ let { config: lastConfig, datapackHash: lastHash } = await challenge.buildAndUpload(api, false);
30
30
  this.log(pc.green(`✓ Initial build and upload complete`));
31
31
  const executeRebuild = async () => {
32
32
  building = true;
@@ -40,7 +40,7 @@ export default class Watch extends Command {
40
40
  const newConfig = await challenge.loadConfig();
41
41
  if (JSON.stringify(newConfig) !== JSON.stringify(lastConfig)) {
42
42
  task.title = "Uploading configuration";
43
- await api.updateChallenge(challenge, newConfig);
43
+ await api.updateChallenge(challenge.shortSlug, newConfig, "private");
44
44
  lastConfig = newConfig;
45
45
  task.title = "Configuration uploaded";
46
46
  }
@@ -63,7 +63,7 @@ export default class Watch extends Command {
63
63
  const newHash = await challenge.getDatapackHash();
64
64
  if (newHash !== lastHash) {
65
65
  task.title = "Uploading datapack";
66
- await challenge.upload(api);
66
+ await api.uploadChallengeDatapack(challenge.shortSlug, challenge.tarballPath);
67
67
  lastHash = newHash;
68
68
  task.title = "Datapack uploaded";
69
69
  }
@@ -5,5 +5,9 @@ export default class Create extends Command {
5
5
  static args: {
6
6
  name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
7
  };
8
+ static flags: {
9
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
8
12
  run(): Promise<void>;
9
13
  }
@@ -5,35 +5,36 @@ import { Args, Command } from "@oclif/core";
5
5
  import enquirer from "enquirer";
6
6
  import pc from "picocolors";
7
7
  import { ApiClient } from "../../lib/api-client.js";
8
- import { loadConfig } from "../../lib/config.js";
8
+ import { getConfigFlags } from "../../lib/flags.js";
9
9
  import { getStaticResourcePath } from "../../lib/utils.js";
10
10
  export default class Create extends Command {
11
- static description = "Create a new evaluation";
12
- static examples = ["<%= config.bin %> <%= command.id %> my-evaluation"];
11
+ static description = "Create a new experiment";
12
+ static examples = ["<%= config.bin %> <%= command.id %> my-experiment"];
13
13
  static args = {
14
14
  name: Args.string({
15
- description: "Name of the evaluation",
15
+ description: "Name of the experiment",
16
16
  required: true,
17
17
  }),
18
18
  };
19
+ static flags = {
20
+ ...getConfigFlags("api-key", "api-url"),
21
+ };
19
22
  async run() {
20
- const { args } = await this.parse(Create);
21
- loadConfig(); // Validate config is available
22
- const evaluationDir = path.resolve(process.cwd(), "evaluations", args.name);
23
- const configPath = path.join(evaluationDir, "config.ts");
24
- // Check if evaluation already exists
23
+ const { args, flags } = await this.parse(Create);
24
+ const experimentDir = path.resolve(process.cwd(), "experiments", args.name);
25
+ const configPath = path.join(experimentDir, "config.ts");
26
+ // Check if experiment already exists
25
27
  try {
26
- await fs.access(evaluationDir);
27
- this.error(pc.red(`Evaluation '${args.name}' already exists at ${evaluationDir}`));
28
+ await fs.access(experimentDir);
29
+ this.error(pc.red(`Experiment '${args.name}' already exists at ${experimentDir}`));
28
30
  }
29
31
  catch {
30
32
  // Directory doesn't exist, which is what we want
31
33
  }
32
- // Create evaluation directory
33
- await fs.mkdir(evaluationDir, { recursive: true });
34
- // Ask for the slug of the challenge to evaluate
35
- const config = loadConfig();
36
- const api = new ApiClient(config);
34
+ // Create experiment directory
35
+ await fs.mkdir(experimentDir, { recursive: true });
36
+ // Ask for the slug of the challenge to run the experiment on
37
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
37
38
  const [kradleChallenges, cloudChallenges] = await Promise.all([api.listKradleChallenges(), api.listChallenges()]);
38
39
  const choices = [...kradleChallenges, ...cloudChallenges]
39
40
  .map((c) => c.slug)
@@ -45,15 +46,15 @@ export default class Create extends Command {
45
46
  const response = await enquirer.prompt({
46
47
  type: "select",
47
48
  name: "challenge",
48
- message: "Select the challenge to evaluate",
49
+ message: "Select the challenge for this experiment",
49
50
  choices: choices,
50
51
  });
51
52
  // Read template file and fill in the challenge slug, then write to config file
52
- const templatePath = getStaticResourcePath("evaluation_template.ts");
53
+ const templatePath = getStaticResourcePath("experiment_template.ts");
53
54
  const template = await fs.readFile(templatePath, "utf-8");
54
55
  const filledTemplate = template.replace("[INSERT CHALLENGE SLUG HERE]", response.challenge);
55
56
  await fs.writeFile(configPath, filledTemplate);
56
- this.log(pc.green(`✓ Created evaluation '${args.name}'`));
57
+ this.log(pc.green(`✓ Created experiment '${args.name}'`));
57
58
  this.log(pc.dim(` Config: ${configPath}`));
58
59
  // Offer to open in editor on macOS
59
60
  if (process.platform === "darwin") {
@@ -73,7 +74,7 @@ export default class Create extends Command {
73
74
  }
74
75
  this.log("");
75
76
  this.log(pc.blue(">> Next steps:"));
76
- this.log(pc.dim(` 1. Edit ${path.basename(configPath)} to define your evaluation runs`));
77
- this.log(pc.dim(` 2. Run: kradle evaluation run ${args.name}`));
77
+ this.log(pc.dim(` 1. Edit ${path.basename(configPath)} to define your experiment runs`));
78
+ this.log(pc.dim(` 2. Run: kradle experiment run ${args.name}`));
78
79
  }
79
80
  }
@@ -2,29 +2,27 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { Command } from "@oclif/core";
4
4
  import pc from "picocolors";
5
- import { loadConfig } from "../../lib/config.js";
6
5
  export default class List extends Command {
7
- static description = "List all evaluations";
6
+ static description = "List all experiments";
8
7
  static examples = ["<%= config.bin %> <%= command.id %>"];
9
8
  async run() {
10
- this.parse(List);
11
- loadConfig(); // Validate config is available
12
- const evaluationsDir = path.resolve(process.cwd(), "evaluations");
9
+ await this.parse(List);
10
+ const experimentsDir = path.resolve(process.cwd(), "experiments");
13
11
  try {
14
- const entries = await fs.readdir(evaluationsDir, { withFileTypes: true });
15
- const evaluations = entries.filter((e) => e.isDirectory());
16
- if (evaluations.length === 0) {
17
- this.log(pc.yellow("No evaluations found."));
18
- this.log(pc.dim(` Run 'kradle evaluation init <name>' to create one.`));
12
+ const entries = await fs.readdir(experimentsDir, { withFileTypes: true });
13
+ const experiments = entries.filter((e) => e.isDirectory());
14
+ if (experiments.length === 0) {
15
+ this.log(pc.yellow("No experiments found."));
16
+ this.log(pc.dim(` Run 'kradle experiment create <name>' to create one.`));
19
17
  return;
20
18
  }
21
- this.log(pc.blue(">> Evaluations:"));
19
+ this.log(pc.blue(">> Experiments:"));
22
20
  this.log("");
23
- for (const evaluation of evaluations) {
24
- const evalDir = path.join(evaluationsDir, evaluation.name);
25
- const hasConfig = await this.fileExists(path.join(evalDir, "config.ts"));
26
- const hasManifest = await this.fileExists(path.join(evalDir, "manifest.json"));
27
- const hasProgress = await this.fileExists(path.join(evalDir, "progress.json"));
21
+ for (const experiment of experiments) {
22
+ const expDir = path.join(experimentsDir, experiment.name);
23
+ const hasConfig = await this.fileExists(path.join(expDir, "config.ts"));
24
+ const hasManifest = await this.fileExists(path.join(expDir, "manifest.json"));
25
+ const hasProgress = await this.fileExists(path.join(expDir, "progress.json"));
28
26
  let status = "";
29
27
  if (hasProgress) {
30
28
  status = pc.yellow(" (in progress)");
@@ -35,12 +33,12 @@ export default class List extends Command {
35
33
  else if (hasConfig) {
36
34
  status = pc.dim(" (config only)");
37
35
  }
38
- this.log(` ${pc.bold(evaluation.name)}${status}`);
36
+ this.log(` ${pc.bold(experiment.name)}${status}`);
39
37
  }
40
38
  }
41
39
  catch {
42
- this.log(pc.yellow("No evaluations directory found."));
43
- this.log(pc.dim(` Run 'kradle evaluation init <name>' to create your first evaluation.`));
40
+ this.log(pc.yellow("No experiments directory found."));
41
+ this.log(pc.dim(` Run 'kradle experiment create <name>' to create your first experiment.`));
44
42
  }
45
43
  }
46
44
  async fileExists(filePath) {
@@ -6,7 +6,10 @@ export default class Run extends Command {
6
6
  name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
7
  };
8
8
  static flags: {
9
- new: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ "web-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ "new-version": import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
13
  "max-concurrent": import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
14
  };
12
15
  run(): Promise<void>;
@@ -0,0 +1,61 @@
1
+ import { Args, Command, Flags } from "@oclif/core";
2
+ import pc from "picocolors";
3
+ import { ApiClient } from "../../lib/api-client.js";
4
+ import { Experimenter } from "../../lib/experiment/experimenter.js";
5
+ import { getConfigFlags } from "../../lib/flags.js";
6
+ const DEFAULT_MAX_CONCURRENT = 5;
7
+ export default class Run extends Command {
8
+ static description = "Run an experiment. If the experiment had an ongoing version, it will resume from the last state.";
9
+ static examples = [
10
+ "<%= config.bin %> <%= command.id %> my-experiment",
11
+ "<%= config.bin %> <%= command.id %> my-experiment --new-version",
12
+ "<%= config.bin %> <%= command.id %> my-experiment --max-concurrent 10",
13
+ ];
14
+ static args = {
15
+ name: Args.string({
16
+ description: "Name of the experiment to run",
17
+ required: true,
18
+ }),
19
+ };
20
+ static flags = {
21
+ "new-version": Flags.boolean({
22
+ char: "n",
23
+ description: "Start a new version of the experiment",
24
+ default: false,
25
+ }),
26
+ "max-concurrent": Flags.integer({
27
+ char: "m",
28
+ description: "Maximum concurrent runs",
29
+ default: DEFAULT_MAX_CONCURRENT,
30
+ }),
31
+ ...getConfigFlags("api-key", "api-url", "web-url"),
32
+ };
33
+ async run() {
34
+ const { args, flags } = await this.parse(Run);
35
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
36
+ const experimenter = new Experimenter(args.name, flags["web-url"], api);
37
+ // Check if experiment exists
38
+ if (!(await experimenter.exists())) {
39
+ this.error(pc.red(`Experiment '${args.name}' does not exist. Run 'kradle experiment create ${args.name}' first.`));
40
+ }
41
+ // Check if config.ts exists
42
+ if (!(await experimenter.configExists())) {
43
+ this.error(pc.red(`Config file not found at ${experimenter.configPath}`));
44
+ }
45
+ this.log(pc.blue(`>> Starting experiment: ${args.name}`));
46
+ if (flags["new-version"]) {
47
+ this.log(pc.yellow(" Starting a new version of the experiment."));
48
+ }
49
+ try {
50
+ await experimenter.run({
51
+ new: flags["new-version"],
52
+ maxConcurrent: flags["max-concurrent"],
53
+ openMetabase: true,
54
+ });
55
+ this.log(pc.green("\n✓ Experiment complete!"));
56
+ }
57
+ catch (error) {
58
+ this.error(pc.red(`Experiment failed: ${error instanceof Error ? error.message : String(error)}`));
59
+ }
60
+ }
61
+ }
@@ -34,10 +34,10 @@ export default class Init extends Command {
34
34
  const nonHiddenFiles = files.filter((f) => !f.startsWith("."));
35
35
  const useCurrentDir = nonHiddenFiles.length === 0;
36
36
  if (useCurrentDir) {
37
- this.log(pc.yellow("Current directory is empty, it will be used to store challenges and evaluations."));
37
+ this.log(pc.yellow("Current directory is empty, it will be used to store challenges and experiments."));
38
38
  }
39
39
  else {
40
- this.log(pc.yellow("Current directory is not empty, a subdirectory will be created to store challenges and evaluations."));
40
+ this.log(pc.yellow("Current directory is not empty, a subdirectory will be created to store challenges and experiments."));
41
41
  }
42
42
  let projectName;
43
43
  if (flags.name) {
@@ -1,10 +1,9 @@
1
1
  import type z from "zod";
2
- import type { Challenge } from "./challenge.js";
3
- import type { Config } from "./config.js";
4
- import { type AgentSchemaType, type ChallengeSchemaType, HumanSchema, type RunStatusSchemaType } from "./schemas.js";
2
+ import { type AgentSchemaType, type ChallengeConfigSchemaType, type ChallengeSchemaType, HumanSchema, type RunStatusSchemaType } from "./schemas.js";
5
3
  export declare class ApiClient {
6
- private config;
7
- constructor(config: Config);
4
+ private apiUrl;
5
+ private kradleApiKey;
6
+ constructor(apiUrl: string, kradleApiKey: string);
8
7
  private request;
9
8
  private get;
10
9
  private post;
@@ -40,16 +39,36 @@ export declare class ApiClient {
40
39
  createChallenge(slug: string): Promise<unknown>;
41
40
  /**
42
41
  * Update a challenge definition in the cloud.
43
- * @param challenge - The challenge to update.
44
- * @param challengeConfig - The challenge config to upload. If not provided, the config will be loaded from the challenge.
42
+ * @param challengeSlug - The slug of the challenge.
43
+ * @param challengeConfig - The challenge config to upload.
44
+ * @param visibility - The visibility to set.
45
45
  * @returns The updated challenge.
46
46
  */
47
- updateChallenge(challenge: Challenge, challengeConfig?: ChallengeSchemaType): Promise<void>;
48
- getChallengeUploadUrl(challenge: Challenge): Promise<string>;
47
+ updateChallenge(challengeSlug: string, challengeConfig: ChallengeConfigSchemaType, visibility: "private" | "public"): Promise<void>;
48
+ /**
49
+ * Update the visibility of a challenge.
50
+ * @param challengeSlug - The slug of the challenge.
51
+ * @param visibility - The visibility to set.
52
+ * @returns The updated challenge.
53
+ */
54
+ updateChallengeVisibility(challengeSlug: string, visibility: "private" | "public"): Promise<void>;
55
+ /**
56
+ * Upload a challenge datapack to Google Cloud Storage.
57
+ * @param slug - The slug of the challenge.
58
+ * @param tarballPath - The path to the tarball file.
59
+ * @returns The upload URL.
60
+ */
61
+ uploadChallengeDatapack(slug: string, tarballPath: string): Promise<void>;
62
+ /**
63
+ * Get the upload URL for a challenge datapack.
64
+ * @param slug - The slug of the challenge.
65
+ * @returns The upload URL.
66
+ */
67
+ getChallengeUploadUrl(slug: string): Promise<string>;
49
68
  runChallenge(runData: {
50
69
  challenge: string;
51
70
  participants: unknown[];
52
- }, studio?: boolean): Promise<{
71
+ }): Promise<{
53
72
  runIds?: string[] | undefined;
54
73
  }>;
55
74
  deleteChallenge(challengeId: string): Promise<void>;
@@ -1,3 +1,5 @@
1
+ import { existsSync } from "node:fs";
2
+ import fs from "node:fs/promises";
1
3
  import { AgentsResponseSchema, ChallengeSchema, ChallengesResponseSchema, HumanSchema, RunResponseSchema, RunStatusSchema, UploadUrlResponseSchema, } from "./schemas.js";
2
4
  const DEFAULT_PAGE_SIZE = 30;
3
5
  const DEFAULT_CHALLENGE_SCHEMA = {
@@ -21,37 +23,38 @@ const DEFAULT_CHALLENGE_SCHEMA = {
21
23
  },*/,
22
24
  };
23
25
  export class ApiClient {
24
- config;
25
- constructor(config) {
26
- this.config = config;
27
- }
28
- async request(target, url, options) {
29
- const baseUrl = target === "web" ? this.config.WEB_API_URL : this.config.STUDIO_API_URL;
30
- const fullUrl = `${baseUrl}/${url}`;
26
+ apiUrl;
27
+ kradleApiKey;
28
+ constructor(apiUrl, kradleApiKey) {
29
+ this.apiUrl = apiUrl;
30
+ this.kradleApiKey = kradleApiKey;
31
+ }
32
+ async request(endpoint, options) {
33
+ const fullUrl = `${this.apiUrl}/${endpoint}`;
31
34
  const response = await fetch(fullUrl, {
32
35
  ...options,
33
36
  headers: {
34
- Authorization: `Bearer ${this.config.KRADLE_API_KEY}`,
37
+ Authorization: `Bearer ${this.kradleApiKey}`,
35
38
  "Content-Type": "application/json",
36
39
  ...options.headers,
37
40
  },
38
41
  });
39
42
  if (!response.ok) {
40
43
  const text = await response.text();
41
- throw new Error(`API call failed: ${url} - ${response.status} ${response.statusText}\n${text}`);
44
+ throw new Error(`API call failed: ${fullUrl} - ${response.status} ${response.statusText}\n${text}`);
42
45
  }
43
46
  return response;
44
47
  }
45
- async get(target, url, options = {}, schema) {
46
- const response = await this.request(target, url, {
48
+ async get(url, options = {}, schema) {
49
+ const response = await this.request(url, {
47
50
  method: "GET",
48
51
  ...options,
49
52
  });
50
53
  const data = await response.json();
51
54
  return schema ? schema.parse(data) : data;
52
55
  }
53
- async post(target, url, options = {}, schema) {
54
- const response = await this.request(target, url, {
56
+ async post(url, options = {}, schema) {
57
+ const response = await this.request(url, {
55
58
  method: "POST",
56
59
  ...options,
57
60
  });
@@ -62,14 +65,14 @@ export class ApiClient {
62
65
  const data = JSON.parse(text);
63
66
  return schema ? schema.parse(data) : data;
64
67
  }
65
- async put(target, url, options = {}) {
66
- return await this.request(target, url, {
68
+ async put(url, options = {}) {
69
+ return await this.request(url, {
67
70
  method: "PUT",
68
71
  ...options,
69
72
  });
70
73
  }
71
- async delete(target, url, options = {}) {
72
- return await this.request(target, url, {
74
+ async delete(url, options = {}) {
75
+ return await this.request(url, {
73
76
  method: "DELETE",
74
77
  ...options,
75
78
  });
@@ -97,7 +100,7 @@ export class ApiClient {
97
100
  if (pageToken)
98
101
  params.set("page_token", pageToken);
99
102
  params.set("page_size", String(pageSize));
100
- const response = await this.get("web", `${url}?${params}`, {
103
+ const response = await this.get(`${url}?${params}`, {
101
104
  headers: {
102
105
  "Content-Type": "application/json",
103
106
  },
@@ -109,7 +112,7 @@ export class ApiClient {
109
112
  }
110
113
  async getHuman() {
111
114
  const url = "human";
112
- return this.get("web", url, {}, HumanSchema);
115
+ return this.get(url, {}, HumanSchema);
113
116
  }
114
117
  async listChallenges() {
115
118
  return this.listResource("challenges", "challenges", ChallengesResponseSchema);
@@ -122,7 +125,7 @@ export class ApiClient {
122
125
  }
123
126
  async getChallenge(challengeId) {
124
127
  const url = `challenges/${challengeId}`;
125
- return this.get("web", url, {}, ChallengeSchema);
128
+ return this.get(url, {}, ChallengeSchema);
126
129
  }
127
130
  /**
128
131
  * Check if a challenge exists in the cloud.
@@ -132,7 +135,7 @@ export class ApiClient {
132
135
  async challengeExists(slug) {
133
136
  const url = `challenges/${slug}`;
134
137
  try {
135
- const res = await this.get("web", url, {});
138
+ const res = await this.get(url, {});
136
139
  return res !== null && res !== undefined;
137
140
  }
138
141
  catch (error) {
@@ -142,37 +145,78 @@ export class ApiClient {
142
145
  async createChallenge(slug) {
143
146
  const challenge = { ...DEFAULT_CHALLENGE_SCHEMA, slug: slug, name: slug };
144
147
  const url = "challenges";
145
- return this.post("web", url, {
148
+ return this.post(url, {
146
149
  body: JSON.stringify(challenge),
147
150
  });
148
151
  }
149
152
  /**
150
153
  * Update a challenge definition in the cloud.
151
- * @param challenge - The challenge to update.
152
- * @param challengeConfig - The challenge config to upload. If not provided, the config will be loaded from the challenge.
154
+ * @param challengeSlug - The slug of the challenge.
155
+ * @param challengeConfig - The challenge config to upload.
156
+ * @param visibility - The visibility to set.
157
+ * @returns The updated challenge.
158
+ */
159
+ async updateChallenge(challengeSlug, challengeConfig, visibility) {
160
+ const url = `challenges/${challengeSlug}`;
161
+ const fullConfig = { ...challengeConfig, slug: challengeSlug, visibility };
162
+ await this.put(url, {
163
+ body: JSON.stringify(fullConfig),
164
+ });
165
+ }
166
+ /**
167
+ * Update the visibility of a challenge.
168
+ * @param challengeSlug - The slug of the challenge.
169
+ * @param visibility - The visibility to set.
153
170
  * @returns The updated challenge.
154
171
  */
155
- async updateChallenge(challenge, challengeConfig) {
156
- const url = `challenges/${challenge.shortSlug}`;
157
- console.log(url);
158
- const config = challengeConfig ?? (await challenge.loadConfig());
159
- await this.put("web", url, {
160
- body: JSON.stringify(config),
172
+ async updateChallengeVisibility(challengeSlug, visibility) {
173
+ const url = `challenges/${challengeSlug}/visibility`;
174
+ await this.post(url, {
175
+ body: JSON.stringify({ visibility }),
176
+ });
177
+ }
178
+ /**
179
+ * Upload a challenge datapack to Google Cloud Storage.
180
+ * @param slug - The slug of the challenge.
181
+ * @param tarballPath - The path to the tarball file.
182
+ * @returns The upload URL.
183
+ */
184
+ async uploadChallengeDatapack(slug, tarballPath) {
185
+ const uploadUrl = await this.getChallengeUploadUrl(slug);
186
+ if (!existsSync(tarballPath)) {
187
+ throw new Error(`Tarball not found at ${tarballPath}`);
188
+ }
189
+ const fileBuffer = await fs.readFile(tarballPath);
190
+ const response = await fetch(uploadUrl, {
191
+ method: "PUT",
192
+ headers: {
193
+ "Content-Type": "application/gzip",
194
+ "Content-Length": fileBuffer.length.toString(),
195
+ },
196
+ body: fileBuffer,
161
197
  });
198
+ if (!response.ok) {
199
+ throw new Error(`Failed to upload datapack: ${response.statusText}`);
200
+ }
162
201
  }
163
- async getChallengeUploadUrl(challenge) {
164
- const response = await this.get("web", `challenges/${challenge.shortSlug}/datapackUploadUrl`, {}, UploadUrlResponseSchema);
202
+ /**
203
+ * Get the upload URL for a challenge datapack.
204
+ * @param slug - The slug of the challenge.
205
+ * @returns The upload URL.
206
+ */
207
+ async getChallengeUploadUrl(slug) {
208
+ const response = await this.get(`challenges/${slug}/datapackUploadUrl`, {}, UploadUrlResponseSchema);
165
209
  return response.uploadUrl;
166
210
  }
167
- async runChallenge(runData, studio = false) {
211
+ async runChallenge(runData) {
168
212
  const url = "jobs";
169
- return this.post(studio ? "studio" : "web", url, {
213
+ return this.post(url, {
170
214
  body: JSON.stringify(runData),
171
215
  }, RunResponseSchema);
172
216
  }
173
217
  async deleteChallenge(challengeId) {
174
218
  const url = `challenges/${challengeId}`;
175
- await this.delete("web", url);
219
+ await this.delete(url);
176
220
  }
177
221
  /**
178
222
  * Get the status of a run.
@@ -181,7 +225,7 @@ export class ApiClient {
181
225
  */
182
226
  async getRunStatus(runId) {
183
227
  const url = `runs/${runId}`;
184
- return this.get("web", url, {}, RunStatusSchema);
228
+ return this.get(url, {}, RunStatusSchema);
185
229
  }
186
230
  /**
187
231
  * Add a tag to a run.
@@ -191,7 +235,7 @@ export class ApiClient {
191
235
  */
192
236
  async tagRun(runId, tag) {
193
237
  const url = `runs/${runId}/tag`;
194
- await this.post("web", url, {
238
+ await this.post(url, {
195
239
  body: JSON.stringify({ tag }),
196
240
  });
197
241
  }
@@ -2,6 +2,7 @@ import type { Arg } from "@oclif/core/interfaces";
2
2
  /**
3
3
  * Returns a "challenge slug" argument, and validates it to be a valid challenge slug.
4
4
  */
5
- export declare function getChallengeSlugArgument({ description }: {
5
+ export declare function getChallengeSlugArgument<R extends boolean = true>({ description, required, }: {
6
6
  description: string;
7
- }): Arg<string>;
7
+ required?: R;
8
+ }): Arg<R extends true ? string : string | undefined>;
@@ -3,10 +3,10 @@ const CHALLENGE_SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3
3
  /**
4
4
  * Returns a "challenge slug" argument, and validates it to be a valid challenge slug.
5
5
  */
6
- export function getChallengeSlugArgument({ description }) {
7
- return Args.string({
6
+ export function getChallengeSlugArgument({ description, required, }) {
7
+ const arg = Args.string({
8
8
  description,
9
- required: true,
9
+ required: required ?? true,
10
10
  parse: async (input) => {
11
11
  if (!CHALLENGE_SLUG_REGEX.test(input)) {
12
12
  throw new Error(`Invalid challenge slug: ${input}. Challenge slugs must be lowercase alphanumeric characters and hyphens, and must not start or end with a hyphen.`);
@@ -14,4 +14,6 @@ export function getChallengeSlugArgument({ description }) {
14
14
  return input;
15
15
  },
16
16
  });
17
+ // biome-ignore lint/suspicious/noExplicitAny: Typescript can't handle the conditional "required"
18
+ return arg;
17
19
  }