@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.
- package/README.md +93 -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 +11 -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/experiment/recordings.d.ts +19 -0
- package/dist/commands/experiment/recordings.js +416 -0
- package/dist/commands/experiment/run.d.ts +17 -0
- package/dist/commands/experiment/run.js +67 -0
- package/dist/commands/init.js +2 -2
- package/dist/lib/api-client.d.ts +51 -10
- package/dist/lib/api-client.js +108 -39
- 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 +92 -0
- package/dist/lib/experiment/experimenter.js +368 -0
- 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.d.ts +2 -0
- package/dist/lib/{evaluation → experiment}/runner.js +21 -2
- 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 +10 -4
- package/dist/lib/{evaluation → experiment}/types.js +5 -3
- package/dist/lib/flags.d.ts +47 -0
- package/dist/lib/flags.js +63 -0
- package/dist/lib/schemas.d.ts +63 -2
- package/dist/lib/schemas.js +27 -1
- package/dist/lib/utils.d.ts +9 -10
- package/dist/lib/utils.js +12 -12
- package/oclif.manifest.json +423 -64
- package/package.json +11 -8
- 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.d.ts +0 -13
- 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/dist/lib/evaluation/evaluator.js +0 -268
- package/static/evaluation_template.ts +0 -69
- /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,
|
|
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-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
|
|
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
|
|
173
188
|
|
|
174
|
-
List all local
|
|
189
|
+
List all local experiments:
|
|
175
190
|
|
|
176
191
|
```bash
|
|
177
|
-
kradle
|
|
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
|
-
|
|
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
|
-
│ │
|
|
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
|
|
305
|
+
│ │ ├── agent/ # Agent commands
|
|
306
|
+
│ │ ├── challenge/ # Challenge management commands
|
|
307
|
+
│ │ └── experiment/ # Experiment commands
|
|
278
308
|
│ └── lib/ # Core libraries
|
|
279
|
-
│
|
|
280
|
-
|
|
281
|
-
│
|
|
282
|
-
│
|
|
283
|
-
│ ├──
|
|
284
|
-
│ └──
|
|
285
|
-
|
|
286
|
-
|
|
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 {
|
|
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
|
-
await challenge.
|
|
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.
|
|
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,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,
|
|
28
|
+
const challenge = new Challenge(slug, flags["challenges-path"]);
|
|
30
29
|
const inCloud = cloudMap.has(slug);
|
|
31
|
-
|
|
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
|
-
|
|
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
|
}
|