@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
package/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # Kradle CLI
2
2
 
3
- Kradle's CLI for managing Minecraft challenges, evaluations, agents, and more!
3
+ Kradle's CLI for managing Minecraft challenges, experiments, agents, and more!
4
4
 
5
5
  * [Installation](#installation)
6
6
  * [Autocomplete](#autocomplete)
7
7
  * [Configuration](#configuration)
8
8
  * [Challenge](#challenge-commands)
9
- * [Evaluations](#evaluation-commands)
9
+ * [Experiments](#experiment-commands)
10
+ * [Agents](#agent-commands)
10
11
  * [Publishing a New Version](#publishing-a-new-version)
11
12
  * [Development](#development)
12
13
  * [Architecture](#architecture)
@@ -17,14 +18,14 @@ Kradle's CLI for managing Minecraft challenges, evaluations, agents, and more!
17
18
  ```
18
19
  npm i -g @kradle/cli
19
20
  ```
20
- 2. Initialize a new directory to store challenges and evaluations
21
+ 2. Initialize a new directory to store challenges and experiments
21
22
  ```
22
23
  kradle init
23
24
  ```
24
- 3. Congrats 🎉 You can now create a new challenge or a new evaluation:
25
+ 3. Congrats 🎉 You can now create a new challenge or a new experiment:
25
26
  ```
26
27
  kradle challenge create <challenge-name>
27
- kradle evaluation create <evaluation-name>
28
+ kradle experiment create <experiment-name>
28
29
  ```
29
30
 
30
31
  In addition, you can enable [autocomplete](#Autocomplete).
@@ -45,6 +46,20 @@ After setup, you will be able to use Tab to autocomplete:
45
46
  kradle challenge <TAB> # Shows: build, create, list, run, upload, watch, etc.
46
47
  ```
47
48
 
49
+ ## Configuration
50
+
51
+ The CLI requires a `.env` file with your Kradle API key and environment settings.
52
+
53
+ **For new projects:** Run `kradle init` which will prompt for your API key and create the `.env` automatically.
54
+
55
+ **Manual setup:** Create a `.env` file in your project root:
56
+ ```bash
57
+ KRADLE_API_KEY=your-api-key-here
58
+ KRADLE_API_URL=https://dev.kradle.ai/api
59
+ ```
60
+
61
+ Get your API key at: https://dev.kradle.ai/settings#api-keys
62
+
48
63
  ## Challenge Commands
49
64
 
50
65
  ### Create Challenge
@@ -122,59 +137,69 @@ kradle challenge multi-upload
122
137
 
123
138
  Provides an interactive UI to select multiple challenges and uploads them in parallel.
124
139
 
125
- ## Evaluation Commands
140
+ ## Experiment Commands
126
141
 
127
- Evaluations allow you to run batches of challenge runs with different agents and configurations, then analyze the results. This is useful for benchmarking agents, testing challenge difficulty, or gathering statistics across many runs.
142
+ Experiments allow you to run batches of challenge runs with different agents and configurations, then analyze the results. This is useful for benchmarking agents, testing challenge difficulty, or gathering statistics across many runs.
128
143
 
129
144
  ### Concepts
130
145
 
131
- **Evaluation**: A named collection of run configurations defined in a `config.ts` file. Each evaluation lives in `evaluations/<name>/`.
146
+ **Experiment**: A named collection of run configurations defined in a `config.ts` file. Each experiment lives in `experiments/<name>/`.
132
147
 
133
- **Iteration**: A snapshot of an evaluation execution. When you run an evaluation, it creates an iteration containing:
148
+ **Version**: A snapshot of an experiment execution. When you run an experiment, it creates a version containing:
134
149
  - A copy of the `config.ts` at that point in time
135
150
  - A `manifest.json` with the generated list of runs
136
151
  - A `progress.json` tracking the status of each run
137
152
 
138
- Iterations are stored in `evaluations/<name>/iterations/001/`, `002/`, etc. This allows you to:
139
- - Resume an interrupted evaluation from where it left off
140
- - Re-run the same evaluation with `--new` to create a fresh iteration
141
- - Compare results across different iterations
153
+ Versions are stored in `experiments/<name>/versions/001/`, `002/`, etc. This allows you to:
154
+ - Resume an interrupted experiment from where it left off
155
+ - Re-run the same experiment with `--new` to create a fresh version
156
+ - Compare results across different versions
142
157
 
143
- ### Create Evaluation
158
+ ### Create Experiment
144
159
 
145
- Create a new evaluation with a template config file:
160
+ Create a new experiment with a template config file:
146
161
 
147
162
  ```bash
148
- kradle evaluation create <name>
163
+ kradle experiment create <name>
149
164
  ```
150
165
 
151
- This creates `evaluations/<name>/config.ts` with a template that you can customize. The config exports a `main()` function that returns a manifest with:
166
+ This creates `experiments/<name>/config.ts` with a template that you can customize. The config exports a `main()` function that returns a manifest with:
152
167
  - `runs`: Array of run configurations (challenge + participants)
153
168
  - `tags`: Optional tags applied to all runs for filtering in analytics
154
169
 
155
- ### Run Evaluation
170
+ ### Run Experiment
156
171
 
157
- Execute or resume an evaluation:
172
+ Execute or resume an experiment:
158
173
 
159
174
  ```bash
160
- kradle evaluation run <name> # Resume current iteration or create first one
161
- kradle evaluation run <name> --new # Start a new iteration
162
- kradle evaluation run <name> --max-concurrent 10 # Control parallelism (default: 5)
175
+ kradle experiment run <name> # Resume current version or create first one
176
+ kradle experiment run <name> --new # Start a new version
177
+ kradle experiment run <name> --max-concurrent 10 # Control parallelism (default: 5)
163
178
  ```
164
179
 
165
180
  The run command:
166
- 1. Creates a new iteration (or resumes the current one)
181
+ 1. Creates a new version (or resumes the current one)
167
182
  2. Generates a manifest by executing `config.ts`
168
183
  3. Displays an interactive TUI showing run progress
169
184
  4. Saves progress periodically (allows resuming if interrupted)
170
185
  5. Opens Metabase dashboard with results when complete
171
186
 
172
- ### List Evaluations
187
+ ### List Experiments
188
+
189
+ List all local experiments:
190
+
191
+ ```bash
192
+ kradle experiment list
193
+ ```
194
+
195
+ ## Agent Commands
196
+
197
+ ### List Agents
173
198
 
174
- List all local evaluations:
199
+ List all agents registered in the system:
175
200
 
176
201
  ```bash
177
- kradle evaluation list
202
+ kradle agent list
178
203
  ```
179
204
 
180
205
  ## Publishing a New Version
@@ -203,25 +228,10 @@ npm run build
203
228
  npm link
204
229
  ```
205
230
 
206
- ### `kradle` vs `kradle-dev`
207
-
208
- The repository provides two CLI commands:
209
-
210
- - **`kradle`**: Production CLI that runs compiled JavaScript from `dist/`
211
- - Requires `npm run build` after every code change
212
- - This is what end users will use
231
+ The repository provides the `kradle` CLI command. It runs compiled JavaScript from `dist/`:
232
+ - It requires running `npm run build` after every code change
233
+ - You can use `npm run watch` to make sure your code automatically recompiles after any change
213
234
 
214
- - **`kradle-dev`**: Development CLI that runs TypeScript directly
215
- - No build step required
216
- - Changes are reflected immediately
217
- - **Always use this during development**
218
-
219
- Example usage:
220
- ```bash
221
- kradle-dev challenge list
222
- kradle-dev challenge build <challenge-name>
223
- kradle-dev challenge run <challenge-name>
224
- ```
225
235
 
226
236
  ### Build & Lint
227
237
 
@@ -246,10 +256,6 @@ Each challenge is a folder in `challenges/<slug>/` containing:
246
256
  4. `kradle challenge build <slug>` automatically uploads `config.ts` (if it exists) before building the datapack
247
257
  5. You can modify `config.ts` locally and run `build` to sync changes to the cloud
248
258
 
249
- ### Configuration Note
250
-
251
- The CLI relies on a `.env` file in the **parent directory (kradle-sandstone root)**, not in the kradle-cli directory itself. The `.env` file should be at the same level as both `kradle-cli/` and `challenges/` folders.
252
-
253
259
  ## Architecture
254
260
 
255
261
  The CLI is built with:
@@ -257,6 +263,8 @@ The CLI is built with:
257
263
  - **oclif**: CLI framework
258
264
  - **enquirer**: Interactive prompts
259
265
  - **listr2**: Task list UI
266
+ - **ink**: React-based terminal UI (for experiments)
267
+ - **react**: UI components for ink
260
268
  - **picocolors**: Terminal colors
261
269
  - **zod**: Schema validation
262
270
  - **chokidar**: File watching
@@ -268,22 +276,11 @@ The CLI is built with:
268
276
  kradle-cli/
269
277
  ├── src/
270
278
  │ ├── commands/ # CLI commands
271
- │ │ └── challenge/ # Challenge management commands
272
- │ │ ├── build.ts # Build & upload datapack + config
273
- │ │ ├── create.ts # Create new challenge
274
- │ │ ├── list.ts # List local & cloud challenges
275
- │ │ ├── multi-upload.ts # Interactive multi-select upload
276
- │ │ ├── run.ts # Run challenge (prod or studio)
277
- │ │ ├── watch.ts # Watch for changes & auto-rebuild
279
+ │ │ ├── agent/ # Agent commands
280
+ │ │ ├── challenge/ # Challenge management commands
281
+ │ │ └── experiment/ # Experiment commands
278
282
  │ └── lib/ # Core libraries
279
- ├── api-client.ts # Typed API client with Zod validation
280
- │ ├── arguments.ts # Shared CLI arguments with autocomplete
281
- │ ├── challenge.ts # Challenge class (build, upload, hash, config)
282
- │ ├── config.ts # Environment config with Zod schemas
283
- │ ├── schemas.ts # Zod schemas for type-safe API interactions
284
- │ └── utils.ts # Utility functions
285
- ├── static/ # Template files (challenge.ts)
286
- ├── biome.json # Biome linter & formatter config
287
- ├── package.json
288
- └── tsconfig.json # TypeScript ESM configuration
283
+ └── experiment/ # Experiment system
284
+ └── static/ # Template files
285
+ └── project_template/ # Files for kradle init
289
286
  ```
@@ -2,5 +2,9 @@ import { Command } from "@oclif/core";
2
2
  export default class List extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
+ static flags: {
6
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ };
5
9
  run(): Promise<void>;
6
10
  }
@@ -1,14 +1,16 @@
1
1
  import { Command } from "@oclif/core";
2
2
  import pc from "picocolors";
3
3
  import { ApiClient } from "../../lib/api-client.js";
4
- import { loadConfig } from "../../lib/config.js";
4
+ import { getConfigFlags } from "../../lib/flags.js";
5
5
  export default class List extends Command {
6
6
  static description = "List all agents";
7
7
  static examples = ["<%= config.bin %> <%= command.id %>"];
8
+ static flags = {
9
+ ...getConfigFlags("api-key", "api-url"),
10
+ };
8
11
  async run() {
9
- const config = loadConfig();
10
- await this.parse(List);
11
- const api = new ApiClient(config);
12
+ const { flags } = await this.parse(List);
13
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
12
14
  this.log(pc.blue(">> Loading agents..."));
13
15
  const agents = await api.listKradleAgents();
14
16
  agents.sort((a, b) => a.username?.localeCompare(b.username || "") || 0);
@@ -3,7 +3,15 @@ export default class Build extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static args: {
6
- challenge: import("@oclif/core/interfaces").Arg<string>;
6
+ challengeSlug: import("@oclif/core/interfaces").Arg<string | undefined>;
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
+ "challenges-path": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ public: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ };
15
+ static strict: boolean;
8
16
  run(): Promise<void>;
9
17
  }
@@ -1,25 +1,53 @@
1
- import { Command } from "@oclif/core";
1
+ import { Command, Flags, loadHelpClass } from "@oclif/core";
2
2
  import pc from "picocolors";
3
3
  import { ApiClient } from "../../lib/api-client.js";
4
4
  import { getChallengeSlugArgument } from "../../lib/arguments.js";
5
5
  import { Challenge } from "../../lib/challenge.js";
6
- import { loadConfig } from "../../lib/config.js";
6
+ import { getConfigFlags } from "../../lib/flags.js";
7
7
  export default class Build extends Command {
8
8
  static description = "Build and upload challenge datapack and config";
9
- static examples = ["<%= config.bin %> <%= command.id %> my-challenge"];
9
+ static examples = [
10
+ "<%= config.bin %> <%= command.id %> my-challenge",
11
+ "<%= config.bin %> <%= command.id %> my-challenge my-other-challenge",
12
+ "<%= config.bin %> <%= command.id %> --all",
13
+ ];
10
14
  static args = {
11
- challenge: getChallengeSlugArgument({ description: "Challenge slug to build" }),
15
+ challengeSlug: getChallengeSlugArgument({
16
+ description: "Challenge slug to build and upload. Can be used multiple times to build and upload multiple challenges at once. Incompatible with --all flag.",
17
+ required: false,
18
+ }),
12
19
  };
20
+ static flags = {
21
+ all: Flags.boolean({ char: "a", description: "Build all challenges in the challenges directory", default: false }),
22
+ public: Flags.boolean({ char: "p", description: "Upload challenges as public.", default: false }),
23
+ ...getConfigFlags("api-key", "api-url", "challenges-path"),
24
+ };
25
+ static strict = false;
13
26
  async run() {
14
- const { args } = await this.parse(Build);
15
- const config = loadConfig();
16
- const api = new ApiClient(config);
17
- const challenge = new Challenge(args.challenge, config);
18
- try {
19
- await challenge.buildAndUpload(api);
27
+ const { argv, flags } = await this.parse(Build);
28
+ if (flags.all && argv.length > 0) {
29
+ this.error(pc.red("Cannot use --all flag with challenge slugs"));
30
+ }
31
+ if (!flags.all && argv.length === 0) {
32
+ // Show help if no challenge slugs are provided - https://github.com/oclif/oclif/issues/183#issuecomment-1933104981
33
+ await new (await loadHelpClass(this.config))(this.config).showHelp([Build.id]);
34
+ return;
35
+ }
36
+ if (flags.all) {
37
+ this.log(pc.blue("Building all challenges"));
20
38
  }
21
- catch (error) {
22
- this.error(pc.red(`Build failed: ${error instanceof Error ? error.message : String(error)}`));
39
+ const challengeSlugs = flags.all ? await Challenge.getLocalChallenges() : argv;
40
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
41
+ for (const challengeSlug of challengeSlugs) {
42
+ const challenge = new Challenge(challengeSlug, flags["challenges-path"]);
43
+ this.log(pc.blue(`==== Building challenge: ${challenge.shortSlug} ====`));
44
+ try {
45
+ await challenge.buildAndUpload(api, flags.public);
46
+ }
47
+ catch (error) {
48
+ this.error(pc.red(`Build failed: ${error instanceof Error ? error.message : String(error)}`));
49
+ }
50
+ this.log();
23
51
  }
24
52
  }
25
53
  }
@@ -3,9 +3,13 @@ export default class Create extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static args: {
6
- challenge: import("@oclif/core/interfaces").Arg<string>;
6
+ challengeSlug: import("@oclif/core/interfaces").Arg<string>;
7
7
  };
8
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
+ "web-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ "challenges-path": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
13
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
14
  };
11
15
  run(): Promise<void>;
@@ -5,28 +5,29 @@ import pc from "picocolors";
5
5
  import { ApiClient } from "../../lib/api-client.js";
6
6
  import { getChallengeSlugArgument } from "../../lib/arguments.js";
7
7
  import { Challenge } from "../../lib/challenge.js";
8
- import { loadConfig } from "../../lib/config.js";
8
+ import { getConfigFlags } from "../../lib/flags.js";
9
+ import { ChallengeConfigSchema } from "../../lib/schemas.js";
9
10
  export default class Create extends Command {
10
11
  static description = "Create a new challenge locally and in the cloud";
11
12
  static examples = ["<%= config.bin %> <%= command.id %> my-challenge"];
12
13
  static args = {
13
- challenge: getChallengeSlugArgument({ description: "Challenge slug to create" }),
14
+ challengeSlug: getChallengeSlugArgument({ description: "Challenge slug to create" }),
14
15
  };
15
16
  static flags = {
16
17
  verbose: Flags.boolean({ char: "v", description: "Verbose output", default: false }),
18
+ ...getConfigFlags("api-key", "api-url", "challenges-path", "web-url"),
17
19
  };
18
20
  async run() {
19
21
  const { args, flags } = await this.parse(Create);
20
- const config = loadConfig();
21
- const api = new ApiClient(config);
22
- const challenge = new Challenge(args.challenge, config);
22
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
23
+ const challenge = new Challenge(args.challengeSlug, flags["challenges-path"]);
23
24
  const tasks = new Listr([
24
25
  {
25
26
  title: "Checking if challenge exists",
26
27
  task: async (_, task) => {
27
- const exists = await api.challengeExists(args.challenge);
28
+ const exists = await api.challengeExists(args.challengeSlug);
28
29
  if (exists) {
29
- this.error(pc.red(`Challenge already exists: ${args.challenge}`));
30
+ this.error(pc.red(`Challenge already exists: ${args.challengeSlug}`));
30
31
  }
31
32
  task.title = `Challenge does not exist.`;
32
33
  },
@@ -34,27 +35,25 @@ export default class Create extends Command {
34
35
  {
35
36
  title: "Creating local challenge folder",
36
37
  task: async (_, task) => {
37
- await Challenge.createLocal(args.challenge, config);
38
+ await Challenge.createLocal(args.challengeSlug, flags["challenges-path"]);
38
39
  task.title = `Created local challenge folder: ${challenge.challengeDir}`;
39
40
  },
40
41
  },
41
42
  {
42
43
  title: "Creating cloud challenge",
43
44
  task: async (_, task) => {
44
- await api.createChallenge(args.challenge);
45
+ await api.createChallenge(args.challengeSlug);
45
46
  task.title = `Created cloud challenge`;
46
47
  },
47
48
  },
48
49
  {
49
50
  title: "Downloading challenge config",
50
51
  task: async (_, task) => {
51
- const challengeData = await api.getChallenge(args.challenge);
52
+ const challengeData = await api.getChallenge(args.challengeSlug);
52
53
  // Remove fields that shouldn't be in the config file
53
- const { id, creationTime, updateTime, creator, ...cleanChallenge } = challengeData;
54
- // We remove the username prefix from the slug, to make the challenge easy to share with others
55
- cleanChallenge.slug = cleanChallenge.slug.split(":")[1];
54
+ const config = ChallengeConfigSchema.parse(challengeData);
56
55
  // Remove quotes from keys
57
- const configStr = JSON.stringify(cleanChallenge, null, 2).replace(/"([a-zA-Z0-9_]+)":/g, "$1:");
56
+ const configStr = JSON.stringify(config, null, 2).replace(/"([a-zA-Z0-9_]+)":/g, "$1:");
58
57
  await fs.writeFile(challenge.configPath, `
59
58
  export const config = ${configStr};
60
59
  `.trim());
@@ -71,16 +70,16 @@ export const config = ${configStr};
71
70
  {
72
71
  title: "Uploading initial datapack",
73
72
  task: async (_, task) => {
74
- await challenge.upload(api);
73
+ api.uploadChallengeDatapack(args.challengeSlug, challenge.tarballPath);
75
74
  task.title = `Uploaded initial datapack`;
76
75
  },
77
76
  },
78
77
  ]);
79
78
  try {
80
79
  await tasks.run();
81
- this.log(pc.green(`\n✓ Challenge created: ${args.challenge}`));
82
- this.log(pc.green(`↳ Run "kradle challenge watch ${args.challenge}" to watch your challenge and start testing!`));
83
- this.log(pc.dim(`\nSee your challenge at: ${config.WEB_URL}/studio/challenges/${args.challenge}?tab=challenge-definition`));
80
+ this.log(pc.green(`\n✓ Challenge created: ${args.challengeSlug}`));
81
+ this.log(pc.green(`↳ Run "kradle challenge watch ${args.challengeSlug}" to watch your challenge and start testing!`));
82
+ this.log(pc.dim(`\nSee your challenge at: ${flags["web-url"]}/studio/challenges/${args.challengeSlug}?tab=challenge-definition`));
84
83
  }
85
84
  catch (error) {
86
85
  this.error(pc.red(`Create failed: ${error instanceof Error ? error.message : String(error)}`));
@@ -3,9 +3,12 @@ export default class Delete extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static args: {
6
- challenge: import("@oclif/core/interfaces").Arg<string>;
6
+ challengeSlug: import("@oclif/core/interfaces").Arg<string>;
7
7
  };
8
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
+ "challenges-path": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
12
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
13
  };
11
14
  run(): Promise<void>;
@@ -5,7 +5,7 @@ import pc from "picocolors";
5
5
  import { ApiClient } from "../../lib/api-client.js";
6
6
  import { getChallengeSlugArgument } from "../../lib/arguments.js";
7
7
  import { Challenge } from "../../lib/challenge.js";
8
- import { loadConfig } from "../../lib/config.js";
8
+ import { getConfigFlags } from "../../lib/flags.js";
9
9
  export default class Delete extends Command {
10
10
  static description = "Delete a challenge locally and from the cloud";
11
11
  static examples = [
@@ -13,16 +13,16 @@ export default class Delete extends Command {
13
13
  "<%= config.bin %> <%= command.id %> my-challenge --yes",
14
14
  ];
15
15
  static args = {
16
- challenge: getChallengeSlugArgument({ description: "Challenge slug to delete" }),
16
+ challengeSlug: getChallengeSlugArgument({ description: "Challenge slug to delete" }),
17
17
  };
18
18
  static flags = {
19
19
  yes: Flags.boolean({ char: "y", description: "Skip confirmation prompts", default: false }),
20
+ ...getConfigFlags("api-key", "api-url", "challenges-path"),
20
21
  };
21
22
  async run() {
22
23
  const { args, flags } = await this.parse(Delete);
23
- const config = loadConfig();
24
- const api = new ApiClient(config);
25
- const challenge = new Challenge(args.challenge, config);
24
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
25
+ const challenge = new Challenge(args.challengeSlug, flags["challenges-path"]);
26
26
  // Check if challenge exists locally
27
27
  const existsLocally = challenge.exists();
28
28
  // Check if challenge exists in cloud
@@ -2,5 +2,10 @@ import { Command } from "@oclif/core";
2
2
  export default class List extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
+ static flags: {
6
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ "challenges-path": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
5
10
  run(): Promise<void>;
6
11
  }
@@ -2,14 +2,16 @@ import { Command } from "@oclif/core";
2
2
  import pc from "picocolors";
3
3
  import { ApiClient } from "../../lib/api-client.js";
4
4
  import { Challenge } from "../../lib/challenge.js";
5
- import { loadConfig } from "../../lib/config.js";
5
+ import { getConfigFlags } from "../../lib/flags.js";
6
6
  export default class List extends Command {
7
7
  static description = "List all challenges (local and cloud)";
8
8
  static examples = ["<%= config.bin %> <%= command.id %>"];
9
+ static flags = {
10
+ ...getConfigFlags("api-key", "api-url", "challenges-path"),
11
+ };
9
12
  async run() {
10
- const config = loadConfig();
11
- await this.parse(List);
12
- const api = new ApiClient(config);
13
+ const { flags } = await this.parse(List);
14
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
13
15
  this.log(pc.blue(">> Loading challenges..."));
14
16
  const [cloudChallenges, localChallenges, human] = await Promise.all([
15
17
  api.listChallenges(),
@@ -18,17 +20,14 @@ export default class List extends Command {
18
20
  ]);
19
21
  // Create a map for easy lookup
20
22
  const cloudMap = new Map(cloudChallenges.map((c) => [c.slug, c]));
21
- const allSlugs = new Set([
22
- ...cloudMap.keys(),
23
- ...Object.keys(localChallenges).map((id) => `${human.username}:${id}`),
24
- ]);
23
+ const allSlugs = new Set([...cloudMap.keys(), ...localChallenges.map((id) => `${human.username}:${id}`)]);
25
24
  this.log(pc.bold("\nChallenges:\n"));
26
25
  this.log(`${"Status".padEnd(15)} ${"Slug".padEnd(40)} ${"Name".padEnd(30)}`);
27
26
  this.log("-".repeat(90));
28
27
  for (const slug of Array.from(allSlugs).sort()) {
29
- const challenge = new Challenge(slug, config);
28
+ const challenge = new Challenge(slug, flags["challenges-path"]);
30
29
  const inCloud = cloudMap.has(slug);
31
- const inLocal = localChallenges[challenge.shortSlug];
30
+ const inLocal = localChallenges.includes(slug);
32
31
  let status;
33
32
  if (inCloud && inLocal) {
34
33
  status = pc.green("✓ synced");
@@ -3,10 +3,17 @@ export default class Run extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static args: {
6
- challenge: import("@oclif/core/interfaces").Arg<string>;
6
+ challengeSlug: import("@oclif/core/interfaces").Arg<string>;
7
7
  };
8
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
+ "web-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ "studio-api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ "studio-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
+ "challenges-path": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
15
  studio: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ open: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
17
  };
11
18
  run(): Promise<void>;
12
19
  }
@@ -3,8 +3,8 @@ import pc from "picocolors";
3
3
  import { ApiClient } from "../../lib/api-client.js";
4
4
  import { getChallengeSlugArgument } from "../../lib/arguments.js";
5
5
  import { Challenge } from "../../lib/challenge.js";
6
- import { loadConfig } from "../../lib/config.js";
7
- import { loadTemplateRun } from "../../lib/utils.js";
6
+ import { getConfigFlags } from "../../lib/flags.js";
7
+ import { loadTemplateRun, openInBrowser } from "../../lib/utils.js";
8
8
  export default class Run extends Command {
9
9
  static description = "Run a challenge";
10
10
  static examples = [
@@ -12,16 +12,18 @@ export default class Run extends Command {
12
12
  "<%= config.bin %> <%= command.id %> my-challenge --studio",
13
13
  ];
14
14
  static args = {
15
- challenge: getChallengeSlugArgument({ description: "Challenge slug to run" }),
15
+ challengeSlug: getChallengeSlugArgument({ description: "Challenge slug to run" }),
16
16
  };
17
17
  static flags = {
18
18
  studio: Flags.boolean({ char: "s", description: "Run in studio environment", default: false }),
19
+ open: Flags.boolean({ char: "o", description: "Open the run URL in the browser", default: false }),
20
+ ...getConfigFlags("api-key", "api-url", "challenges-path", "web-url", "studio-url", "studio-api-url"),
19
21
  };
20
22
  async run() {
21
23
  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);
24
+ const apiUrl = flags.studio ? flags["studio-api-url"] : flags["api-url"];
25
+ const studioApi = new ApiClient(apiUrl, flags["api-key"]);
26
+ const challenge = new Challenge(args.challengeSlug, flags["challenges-path"]);
25
27
  try {
26
28
  const { participants } = (await loadTemplateRun());
27
29
  const template = {
@@ -29,12 +31,15 @@ export default class Run extends Command {
29
31
  participants,
30
32
  };
31
33
  this.log(pc.blue(`>> Running challenge: ${challenge.shortSlug}${flags.studio ? " (studio)" : ""}...`));
32
- const response = await api.runChallenge(template, flags.studio);
34
+ const response = await studioApi.runChallenge(template);
33
35
  if (response.runIds && response.runIds.length > 0) {
34
- const baseUrl = flags.studio ? config.STUDIO_URL : config.WEB_URL;
36
+ const baseUrl = flags.studio ? flags["studio-url"] : flags["web-url"];
35
37
  const runUrl = `${baseUrl}/runs/${response.runIds[0]}`;
36
38
  this.log(pc.green("\n✓ Challenge started!"));
37
39
  this.log(pc.dim(`Run URL: ${runUrl}`));
40
+ if (flags.open) {
41
+ await openInBrowser(runUrl);
42
+ }
38
43
  }
39
44
  else {
40
45
  this.log(pc.yellow("⚠ Challenge started but no run ID returned"));
@@ -3,10 +3,13 @@ export default class Watch extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ "challenges-path": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
9
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
10
  };
8
11
  static args: {
9
- challenge: import("@oclif/core/interfaces").Arg<string>;
12
+ challengeSlug: import("@oclif/core/interfaces").Arg<string>;
10
13
  };
11
14
  run(): Promise<void>;
12
15
  }