@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.
- package/README.md +62 -65
- package/dist/commands/agent/list.d.ts +4 -0
- package/dist/commands/agent/list.js +6 -4
- package/dist/commands/challenge/build.d.ts +9 -1
- package/dist/commands/challenge/build.js +40 -12
- package/dist/commands/challenge/create.d.ts +5 -1
- package/dist/commands/challenge/create.js +17 -18
- package/dist/commands/challenge/delete.d.ts +4 -1
- package/dist/commands/challenge/delete.js +5 -5
- package/dist/commands/challenge/list.d.ts +5 -0
- package/dist/commands/challenge/list.js +9 -10
- package/dist/commands/challenge/run.d.ts +8 -1
- package/dist/commands/challenge/run.js +13 -8
- package/dist/commands/challenge/watch.d.ts +4 -1
- package/dist/commands/challenge/watch.js +8 -8
- package/dist/commands/{evaluation → experiment}/create.d.ts +4 -0
- package/dist/commands/{evaluation → experiment}/create.js +22 -21
- package/dist/commands/{evaluation → experiment}/list.js +17 -19
- package/dist/commands/{evaluation → experiment}/run.d.ts +4 -1
- package/dist/commands/experiment/run.js +61 -0
- package/dist/commands/init.js +2 -2
- package/dist/lib/api-client.d.ts +29 -10
- package/dist/lib/api-client.js +81 -37
- package/dist/lib/arguments.d.ts +3 -2
- package/dist/lib/arguments.js +5 -3
- package/dist/lib/challenge.d.ts +13 -18
- package/dist/lib/challenge.js +58 -62
- package/dist/lib/experiment/experimenter.d.ts +87 -0
- package/dist/lib/{evaluation/evaluator.js → experiment/experimenter.js} +74 -72
- package/dist/lib/{evaluation → experiment}/index.d.ts +1 -1
- package/dist/lib/{evaluation → experiment}/index.js +1 -1
- package/dist/lib/{evaluation → experiment}/runner.js +2 -1
- package/dist/lib/{evaluation → experiment}/tui.d.ts +1 -1
- package/dist/lib/{evaluation → experiment}/tui.js +3 -3
- package/dist/lib/{evaluation → experiment}/types.d.ts +6 -4
- package/dist/lib/{evaluation → experiment}/types.js +4 -3
- package/dist/lib/flags.d.ts +47 -0
- package/dist/lib/flags.js +63 -0
- package/dist/lib/schemas.d.ts +32 -0
- package/dist/lib/schemas.js +8 -0
- package/dist/lib/utils.d.ts +9 -10
- package/dist/lib/utils.js +12 -12
- package/oclif.manifest.json +342 -64
- package/package.json +5 -6
- package/static/challenge.ts +12 -13
- package/static/experiment_template.ts +114 -0
- package/static/project_template/dev.env +5 -5
- package/static/project_template/prod.env +4 -4
- package/static/project_template/tsconfig.json +1 -1
- package/dist/commands/challenge/multi-upload.d.ts +0 -6
- package/dist/commands/challenge/multi-upload.js +0 -80
- package/dist/commands/evaluation/run.js +0 -61
- package/dist/lib/config.d.ts +0 -12
- package/dist/lib/config.js +0 -49
- package/dist/lib/evaluation/evaluator.d.ts +0 -88
- package/static/evaluation_template.ts +0 -69
- /package/dist/commands/{evaluation → experiment}/list.d.ts +0 -0
- /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,
|
|
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
|
-
* [
|
|
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
|
|
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
|
|
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
|
|
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
|
-
##
|
|
140
|
+
## Experiment Commands
|
|
126
141
|
|
|
127
|
-
|
|
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
|
-
**
|
|
146
|
+
**Experiment**: A named collection of run configurations defined in a `config.ts` file. Each experiment lives in `experiments/<name>/`.
|
|
132
147
|
|
|
133
|
-
**
|
|
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
|
-
|
|
139
|
-
- Resume an interrupted
|
|
140
|
-
- Re-run the same
|
|
141
|
-
- Compare results across different
|
|
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
|
|
158
|
+
### Create Experiment
|
|
144
159
|
|
|
145
|
-
Create a new
|
|
160
|
+
Create a new experiment with a template config file:
|
|
146
161
|
|
|
147
162
|
```bash
|
|
148
|
-
kradle
|
|
163
|
+
kradle experiment create <name>
|
|
149
164
|
```
|
|
150
165
|
|
|
151
|
-
This creates `
|
|
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
|
|
170
|
+
### Run Experiment
|
|
156
171
|
|
|
157
|
-
Execute or resume an
|
|
172
|
+
Execute or resume an experiment:
|
|
158
173
|
|
|
159
174
|
```bash
|
|
160
|
-
kradle
|
|
161
|
-
kradle
|
|
162
|
-
kradle
|
|
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
|
|
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
|
|
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
|
|
199
|
+
List all agents registered in the system:
|
|
175
200
|
|
|
176
201
|
```bash
|
|
177
|
-
kradle
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
│ │
|
|
272
|
-
│ │
|
|
273
|
-
│ │
|
|
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
|
-
│
|
|
280
|
-
|
|
281
|
-
|
|
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 {
|
|
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
|
|
10
|
-
|
|
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
|
-
|
|
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 {
|
|
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 = [
|
|
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
|
-
|
|
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 {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
21
|
-
const
|
|
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.
|
|
28
|
+
const exists = await api.challengeExists(args.challengeSlug);
|
|
28
29
|
if (exists) {
|
|
29
|
-
this.error(pc.red(`Challenge already exists: ${args.
|
|
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.
|
|
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.
|
|
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.
|
|
52
|
+
const challengeData = await api.getChallenge(args.challengeSlug);
|
|
52
53
|
// Remove fields that shouldn't be in the config file
|
|
53
|
-
const
|
|
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(
|
|
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
|
-
|
|
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.
|
|
82
|
-
this.log(pc.green(`↳ Run "kradle challenge watch ${args.
|
|
83
|
-
this.log(pc.dim(`\nSee your challenge at: ${
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
24
|
-
const
|
|
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 {
|
|
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
|
|
11
|
-
|
|
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,
|
|
28
|
+
const challenge = new Challenge(slug, flags["challenges-path"]);
|
|
30
29
|
const inCloud = cloudMap.has(slug);
|
|
31
|
-
const inLocal = localChallenges
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
23
|
-
const
|
|
24
|
-
const challenge = new Challenge(args.
|
|
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
|
|
34
|
+
const response = await studioApi.runChallenge(template);
|
|
33
35
|
if (response.runIds && response.runIds.length > 0) {
|
|
34
|
-
const baseUrl = flags.studio ?
|
|
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
|
-
|
|
12
|
+
challengeSlug: import("@oclif/core/interfaces").Arg<string>;
|
|
10
13
|
};
|
|
11
14
|
run(): Promise<void>;
|
|
12
15
|
}
|