@kradle/cli 0.0.17 → 0.2.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 (62) hide show
  1. package/README.md +93 -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 +11 -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/experiment/recordings.d.ts +19 -0
  20. package/dist/commands/experiment/recordings.js +416 -0
  21. package/dist/commands/experiment/run.d.ts +17 -0
  22. package/dist/commands/experiment/run.js +67 -0
  23. package/dist/commands/init.js +2 -2
  24. package/dist/lib/api-client.d.ts +51 -10
  25. package/dist/lib/api-client.js +108 -39
  26. package/dist/lib/arguments.d.ts +3 -2
  27. package/dist/lib/arguments.js +5 -3
  28. package/dist/lib/challenge.d.ts +13 -18
  29. package/dist/lib/challenge.js +58 -62
  30. package/dist/lib/experiment/experimenter.d.ts +92 -0
  31. package/dist/lib/experiment/experimenter.js +368 -0
  32. package/dist/lib/{evaluation → experiment}/index.d.ts +1 -1
  33. package/dist/lib/{evaluation → experiment}/index.js +1 -1
  34. package/dist/lib/{evaluation → experiment}/runner.d.ts +2 -0
  35. package/dist/lib/{evaluation → experiment}/runner.js +21 -2
  36. package/dist/lib/{evaluation → experiment}/tui.d.ts +1 -1
  37. package/dist/lib/{evaluation → experiment}/tui.js +3 -3
  38. package/dist/lib/{evaluation → experiment}/types.d.ts +10 -4
  39. package/dist/lib/{evaluation → experiment}/types.js +5 -3
  40. package/dist/lib/flags.d.ts +47 -0
  41. package/dist/lib/flags.js +63 -0
  42. package/dist/lib/schemas.d.ts +63 -2
  43. package/dist/lib/schemas.js +27 -1
  44. package/dist/lib/utils.d.ts +9 -10
  45. package/dist/lib/utils.js +12 -12
  46. package/oclif.manifest.json +423 -64
  47. package/package.json +11 -8
  48. package/static/challenge.ts +12 -13
  49. package/static/experiment_template.ts +114 -0
  50. package/static/project_template/dev.env +5 -5
  51. package/static/project_template/prod.env +4 -4
  52. package/static/project_template/tsconfig.json +1 -1
  53. package/dist/commands/challenge/multi-upload.d.ts +0 -6
  54. package/dist/commands/challenge/multi-upload.js +0 -80
  55. package/dist/commands/evaluation/run.d.ts +0 -13
  56. package/dist/commands/evaluation/run.js +0 -61
  57. package/dist/lib/config.d.ts +0 -12
  58. package/dist/lib/config.js +0 -49
  59. package/dist/lib/evaluation/evaluator.d.ts +0 -88
  60. package/dist/lib/evaluation/evaluator.js +0 -268
  61. package/static/evaluation_template.ts +0 -69
  62. /package/dist/commands/{evaluation → experiment}/list.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-version # 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
173
188
 
174
- List all local evaluations:
189
+ List all local experiments:
175
190
 
176
191
  ```bash
177
- kradle evaluation list
192
+ kradle experiment list
193
+ ```
194
+
195
+ ## Agent Commands
196
+
197
+ ### List Agents
198
+
199
+ List all agents registered in the system:
200
+
201
+ ```bash
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`
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
207
234
 
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
213
-
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
 
@@ -232,6 +242,32 @@ npm run lint:fix # Auto-fix linting issues
232
242
  npm run format # Format code with Biome
233
243
  ```
234
244
 
245
+ ### Running Tests
246
+
247
+ The CLI has integration tests that verify commands work correctly with the dev API.
248
+
249
+ **Setup:**
250
+
251
+ 1. Copy `.env.test.example` to `.env.test`
252
+ 2. Add your Kradle API key (from https://dev.kradle.ai/settings/api-keys)
253
+
254
+ ```bash
255
+ cp .env.test.example .env.test
256
+ # Edit .env.test and add your API key
257
+ ```
258
+
259
+ **Run tests:**
260
+
261
+ ```bash
262
+ npm test # Run all tests
263
+ npm run test:watch # Run tests in watch mode
264
+ npm run test:integration # Run integration tests
265
+ ```
266
+
267
+ **Note:** Integration tests make real API calls to the dev environment and may create/delete challenges.
268
+
269
+ **CI Configuration:** Integration tests run in GitHub Actions on PRs. The `KRADLE_API_KEY` secret must be configured in the repository settings.
270
+
235
271
  ### Challenge Structure
236
272
 
237
273
  Each challenge is a folder in `challenges/<slug>/` containing:
@@ -246,10 +282,6 @@ Each challenge is a folder in `challenges/<slug>/` containing:
246
282
  4. `kradle challenge build <slug>` automatically uploads `config.ts` (if it exists) before building the datapack
247
283
  5. You can modify `config.ts` locally and run `build` to sync changes to the cloud
248
284
 
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
285
  ## Architecture
254
286
 
255
287
  The CLI is built with:
@@ -257,6 +289,8 @@ The CLI is built with:
257
289
  - **oclif**: CLI framework
258
290
  - **enquirer**: Interactive prompts
259
291
  - **listr2**: Task list UI
292
+ - **ink**: React-based terminal UI (for experiments)
293
+ - **react**: UI components for ink
260
294
  - **picocolors**: Terminal colors
261
295
  - **zod**: Schema validation
262
296
  - **chokidar**: File watching
@@ -268,22 +302,16 @@ The CLI is built with:
268
302
  kradle-cli/
269
303
  ├── src/
270
304
  │ ├── 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
305
+ │ │ ├── agent/ # Agent commands
306
+ │ │ ├── challenge/ # Challenge management commands
307
+ │ │ └── experiment/ # Experiment commands
278
308
  │ └── 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
309
+ └── experiment/ # Experiment system
310
+ ├── tests/ # Integration tests
311
+ ├── helpers/ # Test utilities
312
+ └── integration/ # Integration test suites
313
+ │ ├── challenge/ # Challenge command tests
314
+ │ └── experiment/ # Experiment command tests
315
+ └── static/ # Template files
316
+ └── project_template/ # Files for kradle init
289
317
  ```
@@ -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
+ await 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,16 @@ 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
+ // Extract short slug (after the colon) for local comparison
31
+ const shortSlug = slug.includes(":") ? slug.split(":")[1] : slug;
32
+ const inLocal = localChallenges.includes(shortSlug);
32
33
  let status;
33
34
  if (inCloud && inLocal) {
34
35
  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
  }