@kradle/cli 0.0.2

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 (45) hide show
  1. package/README.md +224 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +14 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +14 -0
  6. package/dist/commands/agent/list.d.ts +6 -0
  7. package/dist/commands/agent/list.js +20 -0
  8. package/dist/commands/challenge/build.d.ts +9 -0
  9. package/dist/commands/challenge/build.js +25 -0
  10. package/dist/commands/challenge/create.d.ts +12 -0
  11. package/dist/commands/challenge/create.js +87 -0
  12. package/dist/commands/challenge/delete.d.ts +12 -0
  13. package/dist/commands/challenge/delete.js +99 -0
  14. package/dist/commands/challenge/list.d.ts +6 -0
  15. package/dist/commands/challenge/list.js +48 -0
  16. package/dist/commands/challenge/multi-upload.d.ts +6 -0
  17. package/dist/commands/challenge/multi-upload.js +80 -0
  18. package/dist/commands/challenge/run.d.ts +12 -0
  19. package/dist/commands/challenge/run.js +47 -0
  20. package/dist/commands/challenge/watch.d.ts +12 -0
  21. package/dist/commands/challenge/watch.js +113 -0
  22. package/dist/commands/init.d.ts +11 -0
  23. package/dist/commands/init.js +161 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/lib/api-client.d.ts +55 -0
  27. package/dist/lib/api-client.js +162 -0
  28. package/dist/lib/arguments.d.ts +7 -0
  29. package/dist/lib/arguments.js +17 -0
  30. package/dist/lib/challenge.d.ts +67 -0
  31. package/dist/lib/challenge.js +203 -0
  32. package/dist/lib/config.d.ts +13 -0
  33. package/dist/lib/config.js +51 -0
  34. package/dist/lib/schemas.d.ts +127 -0
  35. package/dist/lib/schemas.js +55 -0
  36. package/dist/lib/utils.d.ts +89 -0
  37. package/dist/lib/utils.js +170 -0
  38. package/oclif.manifest.json +310 -0
  39. package/package.json +78 -0
  40. package/static/challenge.ts +32 -0
  41. package/static/project_template/dev.env +6 -0
  42. package/static/project_template/package.json +17 -0
  43. package/static/project_template/prod.env +6 -0
  44. package/static/project_template/template-run.json +10 -0
  45. package/static/project_template/tsconfig.json +17 -0
@@ -0,0 +1,80 @@
1
+ import { Command } from "@oclif/core";
2
+ import enquirer from "enquirer";
3
+ import { Listr } from "listr2";
4
+ import pc from "picocolors";
5
+ import { ApiClient } from "../../lib/api-client.js";
6
+ import { Challenge } from "../../lib/challenge.js";
7
+ import { loadConfig } from "../../lib/config.js";
8
+ export default class MultiUpload extends Command {
9
+ static description = "Interactively select and upload multiple challenges";
10
+ static examples = ["<%= config.bin %> <%= command.id %>"];
11
+ async run() {
12
+ // Not necessary since we don't have any args, but oclif will raise a warning if we don't parse the args
13
+ this.parse(MultiUpload);
14
+ const config = loadConfig();
15
+ const api = new ApiClient(config);
16
+ this.log(pc.blue(">> Loading challenges..."));
17
+ const [cloudChallenges, localChallenges, human] = await Promise.all([
18
+ api.listChallenges(),
19
+ Challenge.getLocalChallenges(),
20
+ api.getHuman(),
21
+ ]);
22
+ // Get local challenges that exist
23
+ const localChallengeIds = Object.keys(localChallenges);
24
+ if (localChallengeIds.length === 0) {
25
+ this.log(pc.yellow("No local challenges found"));
26
+ return;
27
+ }
28
+ // Create choices with status information
29
+ const cloudMap = new Map(cloudChallenges.map((c) => [c.slug, c]));
30
+ const choices = localChallengeIds.map((id) => {
31
+ const fullSlug = `${human.username}:${id}`;
32
+ const inCloud = cloudMap.has(fullSlug);
33
+ const status = inCloud ? pc.green("☁️ ") : pc.blue("💻");
34
+ return {
35
+ name: id,
36
+ message: `${status} ${id}`,
37
+ };
38
+ });
39
+ // Prompt user to select challenges
40
+ let response;
41
+ try {
42
+ response = await enquirer.prompt({
43
+ type: "multiselect",
44
+ name: "challenges",
45
+ message: "Select challenges to upload. ☁️ = exists in cloud, 📁 = exists locally only",
46
+ choices: choices.map((c) => c.message),
47
+ });
48
+ }
49
+ catch (error) {
50
+ this.log(pc.yellow(">> No challenges selected"));
51
+ return;
52
+ }
53
+ // Map back to challenge IDs
54
+ const selectedChallenges = response.challenges.map((selected) => {
55
+ const choice = choices.find((c) => c.message === selected);
56
+ return choice?.name || selected.replace(/^[✓⊡]\s+/, "");
57
+ });
58
+ // Create tasks for each challenge
59
+ const tasks = new Listr(selectedChallenges.map((challengeId) => ({
60
+ title: challengeId,
61
+ task: async () => {
62
+ const challenge = new Challenge(challengeId, config);
63
+ await challenge.build();
64
+ await challenge.upload(api);
65
+ },
66
+ })), {
67
+ concurrent: false,
68
+ exitOnError: false,
69
+ });
70
+ try {
71
+ await tasks.run();
72
+ this.log(pc.green(`\n✓ Uploaded ${selectedChallenges.length} challenges`));
73
+ }
74
+ catch (error) {
75
+ this.error(pc.red(`Some uploads failed: ${error instanceof Error ? error.message : String(error)}`), {
76
+ exit: false,
77
+ });
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Run extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ challenge: import("@oclif/core/interfaces").Arg<string>;
7
+ };
8
+ static flags: {
9
+ studio: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,47 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import pc from "picocolors";
3
+ import { ApiClient } from "../../lib/api-client.js";
4
+ import { getChallengeSlugArgument } from "../../lib/arguments.js";
5
+ import { Challenge } from "../../lib/challenge.js";
6
+ import { loadConfig } from "../../lib/config.js";
7
+ import { loadTemplateRun } from "../../lib/utils.js";
8
+ export default class Run extends Command {
9
+ static description = "Run a challenge";
10
+ static examples = [
11
+ "<%= config.bin %> <%= command.id %> my-challenge",
12
+ "<%= config.bin %> <%= command.id %> my-challenge --studio",
13
+ ];
14
+ static args = {
15
+ challenge: getChallengeSlugArgument({ description: "Challenge slug to run" }),
16
+ };
17
+ static flags = {
18
+ studio: Flags.boolean({ char: "s", description: "Run in studio environment", default: false }),
19
+ };
20
+ async run() {
21
+ const { args, flags } = await this.parse(Run);
22
+ const config = loadConfig();
23
+ const api = new ApiClient(config);
24
+ const challenge = new Challenge(args.challenge, config);
25
+ try {
26
+ const { participants } = (await loadTemplateRun());
27
+ const template = {
28
+ challenge: challenge.shortSlug,
29
+ participants,
30
+ };
31
+ this.log(pc.blue(`>> Running challenge: ${challenge.shortSlug}${flags.studio ? " (studio)" : ""}...`));
32
+ const response = await api.runChallenge(template, flags.studio);
33
+ if (response.runIds && response.runIds.length > 0) {
34
+ const baseUrl = flags.studio ? config.STUDIO_URL : config.WEB_URL;
35
+ const runUrl = `${baseUrl}/runs/${response.runIds[0]}`;
36
+ this.log(pc.green("\n✓ Challenge started!"));
37
+ this.log(pc.dim(`Run URL: ${runUrl}`));
38
+ }
39
+ else {
40
+ this.log(pc.yellow("⚠ Challenge started but no run ID returned"));
41
+ }
42
+ }
43
+ catch (error) {
44
+ this.error(pc.red(`Run failed: ${error instanceof Error ? error.message : String(error)}`));
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Watch extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ };
8
+ static args: {
9
+ challenge: import("@oclif/core/interfaces").Arg<string>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,113 @@
1
+ import path from "node:path";
2
+ import { Command, Flags } from "@oclif/core";
3
+ import chokidar from "chokidar";
4
+ import { Listr } from "listr2";
5
+ import pc from "picocolors";
6
+ import { ApiClient } from "../../lib/api-client.js";
7
+ import { getChallengeSlugArgument } from "../../lib/arguments.js";
8
+ import { Challenge } from "../../lib/challenge.js";
9
+ import { loadConfig } from "../../lib/config.js";
10
+ import { clearScreen, debounced } from "../../lib/utils.js";
11
+ export default class Watch extends Command {
12
+ static description = "Watch a challenge for changes and auto-rebuild/upload";
13
+ static examples = ["<%= config.bin %> <%= command.id %> my-challenge"];
14
+ static flags = {
15
+ verbose: Flags.boolean({ description: "Enable verbose output", default: false }),
16
+ };
17
+ static args = {
18
+ challenge: getChallengeSlugArgument({ description: "Challenge slug to watch" }),
19
+ };
20
+ async run() {
21
+ 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);
25
+ const debounceSeconds = 1;
26
+ let building = false;
27
+ // Perform the initial build and upload
28
+ this.log(pc.blue(`Performing initial build and upload...`));
29
+ let { config: lastConfig, datapackHash: lastHash } = await challenge.buildAndUpload(api);
30
+ this.log(pc.green(`✓ Initial build and upload complete`));
31
+ const executeRebuild = async () => {
32
+ building = true;
33
+ // Clear screen before subsequent rebuilds for clean output
34
+ clearScreen();
35
+ this.log(pc.cyan(`\n Rebuild started...\n`));
36
+ const tasks = new Listr([
37
+ {
38
+ title: "Checking configuration",
39
+ task: async (_ctx, task) => {
40
+ const newConfig = await challenge.loadConfig();
41
+ if (JSON.stringify(newConfig) !== JSON.stringify(lastConfig)) {
42
+ task.title = "Uploading configuration";
43
+ await api.updateChallenge(challenge, newConfig);
44
+ lastConfig = newConfig;
45
+ task.title = "Configuration uploaded";
46
+ }
47
+ else {
48
+ task.title = "Configuration unchanged";
49
+ task.skip("No configuration changes detected");
50
+ }
51
+ },
52
+ },
53
+ {
54
+ title: "Building datapack",
55
+ task: async (_ctx, task) => {
56
+ await challenge.build(!flags.verbose);
57
+ task.title = "Datapack built";
58
+ },
59
+ },
60
+ {
61
+ title: "Checking datapack changes",
62
+ task: async (_ctx, task) => {
63
+ const newHash = await challenge.getDatapackHash();
64
+ if (newHash !== lastHash) {
65
+ task.title = "Uploading datapack";
66
+ await challenge.upload(api);
67
+ lastHash = newHash;
68
+ task.title = "Datapack uploaded";
69
+ }
70
+ else {
71
+ task.title = "Datapack unchanged";
72
+ task.skip("No datapack changes detected");
73
+ }
74
+ },
75
+ },
76
+ ], {
77
+ renderer: flags.verbose ? "verbose" : "default",
78
+ });
79
+ try {
80
+ await tasks.run();
81
+ this.log(pc.green(`\n✓ Rebuild complete`));
82
+ }
83
+ catch (error) {
84
+ this.log(pc.red(`\n✗ Error: ${error instanceof Error ? error.message : String(error)}`));
85
+ }
86
+ this.log(pc.dim("Watching for changes... (Ctrl+C to stop)\n"));
87
+ building = false;
88
+ };
89
+ const debouncedBuild = debounced(() => executeRebuild(), debounceSeconds * 1000);
90
+ this.log(pc.blue(`\nStarting watch mode for ${pc.bold(challenge.shortSlug)}\n`));
91
+ this.log(pc.dim("Watching for changes... (Ctrl+C to stop)\n"));
92
+ const watcher = chokidar.watch([challenge.challengeDir], {
93
+ ignored: /(^|[/\\])\../, // ignore dotfiles
94
+ persistent: true,
95
+ ignoreInitial: true,
96
+ });
97
+ watcher
98
+ .on("change", (filePath) => {
99
+ if (building)
100
+ return;
101
+ this.log(pc.cyan(` → File changed: ${pc.bold(path.basename(filePath))}`));
102
+ debouncedBuild();
103
+ })
104
+ .on("add", (filePath) => {
105
+ if (building)
106
+ return;
107
+ this.log(pc.cyan(` → File added: ${pc.bold(path.basename(filePath))}`));
108
+ debouncedBuild();
109
+ });
110
+ // Keep process alive indefinitely
111
+ await new Promise(() => { });
112
+ }
113
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Init extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ dev: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,161 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Command, Flags } from "@oclif/core";
4
+ import enquirer from "enquirer";
5
+ import { Listr } from "listr2";
6
+ import pc from "picocolors";
7
+ import { executeCommand, getStaticResourcePath, readDirSorted } from "../lib/utils.js";
8
+ export default class Init extends Command {
9
+ static description = "Initialize a new Kradle project. If the current directory is not empty, it will create a subdirectory for the project. Else, the current directory will be used.";
10
+ static examples = ["<%= config.bin %> <%= command.id %>"];
11
+ static flags = {
12
+ name: Flags.string({
13
+ char: "n",
14
+ description: "Project name",
15
+ required: false,
16
+ }),
17
+ dev: Flags.boolean({
18
+ char: "d",
19
+ description: "Use Kradle's development environment instead of production",
20
+ required: false,
21
+ }),
22
+ "api-key": Flags.string({
23
+ char: "k",
24
+ description: "Kradle API key",
25
+ required: false,
26
+ }),
27
+ };
28
+ async run() {
29
+ try {
30
+ // Check if current directory is empty
31
+ const cwd = process.cwd();
32
+ const files = await fs.readdir(cwd);
33
+ const { flags } = await this.parse(Init);
34
+ const nonHiddenFiles = files.filter((f) => !f.startsWith("."));
35
+ const useCurrentDir = nonHiddenFiles.length === 0;
36
+ if (useCurrentDir) {
37
+ this.log(pc.yellow("Current directory is empty, it will be used as the project directory."));
38
+ }
39
+ else {
40
+ this.log(pc.yellow("Current directory is not empty, a subdirectory will be created for the project."));
41
+ }
42
+ let projectName;
43
+ if (flags.name) {
44
+ projectName = flags.name;
45
+ }
46
+ else {
47
+ let initial;
48
+ if (useCurrentDir) {
49
+ initial = path.basename(cwd);
50
+ }
51
+ const { name } = await enquirer.prompt({
52
+ type: "input",
53
+ name: "name",
54
+ message: "Enter the project name:",
55
+ initial: initial,
56
+ });
57
+ projectName = name;
58
+ }
59
+ let useDev = flags.dev;
60
+ if (!useDev) {
61
+ const { confirm } = await enquirer.prompt({
62
+ type: "confirm",
63
+ name: "confirm",
64
+ message: "Do you want to use Kradle's development environment?",
65
+ initial: false,
66
+ });
67
+ useDev = confirm;
68
+ }
69
+ if (useDev) {
70
+ this.log(pc.yellow("Using Kradle's development environment."));
71
+ }
72
+ else {
73
+ this.log(pc.green("Using Kradle's production environment."));
74
+ }
75
+ const domain = useDev ? "dev.kradle.ai" : "kradle.ai";
76
+ let apiKey;
77
+ if (flags["api-key"]) {
78
+ apiKey = flags["api-key"];
79
+ }
80
+ else {
81
+ this.log(pc.dim(`\nGet your API key at: https://${domain}/settings#api-keys`));
82
+ const { key } = await enquirer.prompt({
83
+ type: "password",
84
+ name: "key",
85
+ message: "Enter your Kradle API key:",
86
+ required: true,
87
+ });
88
+ apiKey = key;
89
+ }
90
+ const targetDir = useCurrentDir ? cwd : path.join(cwd, projectName);
91
+ const tasks = new Listr([
92
+ {
93
+ title: "Creating project directory",
94
+ skip: () => useCurrentDir,
95
+ task: async () => {
96
+ await fs.mkdir(targetDir, { recursive: true });
97
+ },
98
+ },
99
+ {
100
+ title: "Copying template files",
101
+ task: async () => {
102
+ const templateDir = getStaticResourcePath("project_template");
103
+ const templateFiles = await readDirSorted(templateDir);
104
+ for (const file of templateFiles) {
105
+ if (file.name.endsWith(".env")) {
106
+ // We ignore .env as it will be created later
107
+ continue;
108
+ }
109
+ const srcPath = path.join(file.parentPath, file.name);
110
+ const srcRelativePath = path.relative(templateDir, srcPath);
111
+ const destPath = path.join(targetDir, srcRelativePath);
112
+ await fs.copyFile(srcPath, destPath);
113
+ }
114
+ // Create .env file based on the environment
115
+ const sourceEnvPath = useDev ? path.join(templateDir, "dev.env") : path.join(templateDir, "prod.env");
116
+ const destEnvPath = path.join(targetDir, ".env");
117
+ const sourceEnvContent = await fs.readFile(sourceEnvPath, "utf-8");
118
+ const updatedEnvContent = `${sourceEnvContent}\nKRADLE_API_KEY=${apiKey}`;
119
+ await fs.writeFile(destEnvPath, updatedEnvContent);
120
+ },
121
+ },
122
+ {
123
+ title: "Updating package.json",
124
+ task: async () => {
125
+ await executeCommand("npm", ["pkg", "set", `name=${projectName}`, `description=${projectName} challenges`], {
126
+ cwd: targetDir,
127
+ });
128
+ },
129
+ },
130
+ {
131
+ title: "Installing dependencies",
132
+ task: async (_) => {
133
+ try {
134
+ await executeCommand("npm", ["install"], {
135
+ cwd: targetDir,
136
+ });
137
+ }
138
+ catch (error) {
139
+ throw new Error(pc.red(`Failed to install dependencies: ${error instanceof Error ? error.message : String(error)}`));
140
+ }
141
+ },
142
+ },
143
+ ]);
144
+ await tasks.run();
145
+ // Display success message and next steps
146
+ this.log(pc.green(`\n✓ Project initialized successfully!`));
147
+ this.log(pc.dim(`\nProject location: ${targetDir}`));
148
+ this.log(pc.bold("\n📝 Next steps:"));
149
+ if (targetDir !== cwd) {
150
+ this.log(pc.cyan(` cd ${projectName}`));
151
+ }
152
+ this.log(pc.cyan(` Create your first challenge: "kradle challenge create <challenge-name>"
153
+
154
+ 💡 Tip: Enable shell autocomplete with "kradle autocomplete"
155
+ `));
156
+ }
157
+ catch (error) {
158
+ this.error(pc.red(`Init failed: ${error instanceof Error ? error.message : String(error)}`));
159
+ }
160
+ }
161
+ }
@@ -0,0 +1 @@
1
+ export { run } from "@oclif/core";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from "@oclif/core";
@@ -0,0 +1,55 @@
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 } from "./schemas.js";
5
+ export declare class ApiClient {
6
+ private config;
7
+ constructor(config: Config);
8
+ private request;
9
+ private get;
10
+ private post;
11
+ private put;
12
+ private delete;
13
+ /**
14
+ * List a resource from the API.
15
+ * @param url - The URL to list the resource from.
16
+ * @param property - The property of the response that contains the resources.
17
+ * @param schema - The schema to parse the response.
18
+ * @param pageSize - The number of resources to list per page.
19
+ * @returns The resources.
20
+ *
21
+ * @example
22
+ * // The "list_challenges" API returns a response like:
23
+ * // { challenges: [...], nextPageToken: string }
24
+ *
25
+ * const challenges = await this.listResource("list_challenges", "challenges", ChallengesResponseSchema);
26
+ * console.log(challenges);
27
+ */
28
+ private listResource;
29
+ getHuman(): Promise<z.infer<typeof HumanSchema>>;
30
+ listChallenges(): Promise<ChallengeSchemaType[]>;
31
+ listKradleAgents(): Promise<AgentSchemaType[]>;
32
+ getChallenge(challengeId: string): Promise<ChallengeSchemaType>;
33
+ /**
34
+ * Check if a challenge exists in the cloud.
35
+ * @param slug - The slug of the challenge.
36
+ * @returns True if the challenge exists, false otherwise.
37
+ */
38
+ challengeExists(slug: string): Promise<boolean>;
39
+ createChallenge(slug: string): Promise<unknown>;
40
+ /**
41
+ * Update a challenge definition in the cloud.
42
+ * @param challenge - The challenge to update.
43
+ * @param challengeConfig - The challenge config to upload. If not provided, the config will be loaded from the challenge.
44
+ * @returns The updated challenge.
45
+ */
46
+ updateChallenge(challenge: Challenge, challengeConfig?: ChallengeSchemaType): Promise<void>;
47
+ getChallengeUploadUrl(challenge: Challenge): Promise<string>;
48
+ runChallenge(runData: {
49
+ challenge: string;
50
+ participants: unknown[];
51
+ }, studio?: boolean): Promise<{
52
+ runIds?: string[] | undefined;
53
+ }>;
54
+ deleteChallenge(challengeId: string): Promise<void>;
55
+ }
@@ -0,0 +1,162 @@
1
+ import { AgentsResponseSchema, ChallengeSchema, ChallengesResponseSchema, HumanSchema, RunResponseSchema, UploadUrlResponseSchema, } from "./schemas.js";
2
+ const DEFAULT_PAGE_SIZE = 30;
3
+ const DEFAULT_CHALLENGE_SCHEMA = {
4
+ slug: "",
5
+ name: "",
6
+ visibility: "private",
7
+ domain: "minecraft",
8
+ world: "team-kradle:colosseum",
9
+ challengeConfig: { cheat: false, datapack: true, gameMode: "survival" },
10
+ task: ".",
11
+ roles: { "default-role": { description: "default-role", specificTask: "do your best!" } },
12
+ objective: {
13
+ fieldName: "success_rate",
14
+ direction: "maximize",
15
+ },
16
+ };
17
+ export class ApiClient {
18
+ config;
19
+ constructor(config) {
20
+ this.config = config;
21
+ }
22
+ async request(target, url, options) {
23
+ const baseUrl = target === "web" ? this.config.WEB_API_URL : this.config.STUDIO_API_URL;
24
+ const fullUrl = `${baseUrl}/${url}`;
25
+ const response = await fetch(fullUrl, {
26
+ ...options,
27
+ headers: {
28
+ Authorization: `Bearer ${this.config.KRADLE_API_KEY}`,
29
+ "Content-Type": "application/json",
30
+ ...options.headers,
31
+ },
32
+ });
33
+ if (!response.ok) {
34
+ const text = await response.text();
35
+ throw new Error(`API call failed: ${url} - ${response.status} ${response.statusText}\n${text}`);
36
+ }
37
+ return response;
38
+ }
39
+ async get(target, url, options = {}, schema) {
40
+ const response = await this.request(target, url, {
41
+ method: "GET",
42
+ ...options,
43
+ });
44
+ const data = await response.json();
45
+ return schema ? schema.parse(data) : data;
46
+ }
47
+ async post(target, url, options = {}, schema) {
48
+ const response = await this.request(target, url, {
49
+ method: "POST",
50
+ ...options,
51
+ });
52
+ const data = await response.json();
53
+ return schema ? schema.parse(data) : data;
54
+ }
55
+ async put(target, url, options = {}) {
56
+ await this.request(target, url, {
57
+ method: "PUT",
58
+ ...options,
59
+ });
60
+ }
61
+ async delete(target, url, options = {}) {
62
+ await this.request(target, url, {
63
+ method: "DELETE",
64
+ ...options,
65
+ });
66
+ }
67
+ /**
68
+ * List a resource from the API.
69
+ * @param url - The URL to list the resource from.
70
+ * @param property - The property of the response that contains the resources.
71
+ * @param schema - The schema to parse the response.
72
+ * @param pageSize - The number of resources to list per page.
73
+ * @returns The resources.
74
+ *
75
+ * @example
76
+ * // The "list_challenges" API returns a response like:
77
+ * // { challenges: [...], nextPageToken: string }
78
+ *
79
+ * const challenges = await this.listResource("list_challenges", "challenges", ChallengesResponseSchema);
80
+ * console.log(challenges);
81
+ */
82
+ async listResource(url, property, schema, pageSize = DEFAULT_PAGE_SIZE) {
83
+ let pageToken;
84
+ const resources = [];
85
+ do {
86
+ const params = new URLSearchParams();
87
+ if (pageToken)
88
+ params.set("page_token", pageToken);
89
+ params.set("page_size", String(pageSize));
90
+ const response = await this.get("web", `${url}?${params}`, {
91
+ headers: {
92
+ "Content-Type": "application/json",
93
+ },
94
+ }, schema);
95
+ resources.push(...response[property]);
96
+ pageToken = response?.nextPageToken;
97
+ } while (pageToken);
98
+ return resources;
99
+ }
100
+ async getHuman() {
101
+ const url = "human";
102
+ return this.get("web", url, {}, HumanSchema);
103
+ }
104
+ async listChallenges() {
105
+ return this.listResource("challenges", "challenges", ChallengesResponseSchema);
106
+ }
107
+ async listKradleAgents() {
108
+ return this.listResource("humans/team-kradle/agents", "agents", AgentsResponseSchema);
109
+ }
110
+ async getChallenge(challengeId) {
111
+ const url = `challenges/${challengeId}`;
112
+ return this.get("web", url, {}, ChallengeSchema);
113
+ }
114
+ /**
115
+ * Check if a challenge exists in the cloud.
116
+ * @param slug - The slug of the challenge.
117
+ * @returns True if the challenge exists, false otherwise.
118
+ */
119
+ async challengeExists(slug) {
120
+ try {
121
+ await this.getChallenge(slug);
122
+ return true;
123
+ }
124
+ catch (error) {
125
+ return false;
126
+ }
127
+ }
128
+ async createChallenge(slug) {
129
+ const challenge = { ...DEFAULT_CHALLENGE_SCHEMA, slug: slug, name: slug };
130
+ const url = "challenges";
131
+ return this.post("web", url, {
132
+ body: JSON.stringify(challenge),
133
+ });
134
+ }
135
+ /**
136
+ * Update a challenge definition in the cloud.
137
+ * @param challenge - The challenge to update.
138
+ * @param challengeConfig - The challenge config to upload. If not provided, the config will be loaded from the challenge.
139
+ * @returns The updated challenge.
140
+ */
141
+ async updateChallenge(challenge, challengeConfig) {
142
+ const url = `challenges/${challenge.shortSlug}`;
143
+ const config = challengeConfig ?? (await challenge.loadConfig());
144
+ return this.put("web", url, {
145
+ body: JSON.stringify(config),
146
+ });
147
+ }
148
+ async getChallengeUploadUrl(challenge) {
149
+ const response = await this.get("web", `challenges/${challenge.shortSlug}/datapackUploadUrl`, {}, UploadUrlResponseSchema);
150
+ return response.uploadUrl;
151
+ }
152
+ async runChallenge(runData, studio = false) {
153
+ const url = "jobs";
154
+ return this.post(studio ? "studio" : "web", url, {
155
+ body: JSON.stringify(runData),
156
+ }, RunResponseSchema);
157
+ }
158
+ async deleteChallenge(challengeId) {
159
+ const url = `challenges/${challengeId}`;
160
+ await this.delete("web", url);
161
+ }
162
+ }
@@ -0,0 +1,7 @@
1
+ import type { Arg } from "@oclif/core/interfaces";
2
+ /**
3
+ * Returns a "challenge slug" argument, and validates it to be a valid challenge slug.
4
+ */
5
+ export declare function getChallengeSlugArgument({ description }: {
6
+ description: string;
7
+ }): Arg<string>;