@kradle/cli 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +224 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +14 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +14 -0
  6. package/dist/commands/agent/list.d.ts +6 -0
  7. package/dist/commands/agent/list.js +20 -0
  8. package/dist/commands/challenge/build.d.ts +9 -0
  9. package/dist/commands/challenge/build.js +25 -0
  10. package/dist/commands/challenge/create.d.ts +12 -0
  11. package/dist/commands/challenge/create.js +87 -0
  12. package/dist/commands/challenge/delete.d.ts +12 -0
  13. package/dist/commands/challenge/delete.js +99 -0
  14. package/dist/commands/challenge/list.d.ts +6 -0
  15. package/dist/commands/challenge/list.js +48 -0
  16. package/dist/commands/challenge/multi-upload.d.ts +6 -0
  17. package/dist/commands/challenge/multi-upload.js +80 -0
  18. package/dist/commands/challenge/run.d.ts +12 -0
  19. package/dist/commands/challenge/run.js +47 -0
  20. package/dist/commands/challenge/watch.d.ts +12 -0
  21. package/dist/commands/challenge/watch.js +113 -0
  22. package/dist/commands/init.d.ts +11 -0
  23. package/dist/commands/init.js +161 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/lib/api-client.d.ts +55 -0
  27. package/dist/lib/api-client.js +162 -0
  28. package/dist/lib/arguments.d.ts +7 -0
  29. package/dist/lib/arguments.js +17 -0
  30. package/dist/lib/challenge.d.ts +67 -0
  31. package/dist/lib/challenge.js +203 -0
  32. package/dist/lib/config.d.ts +13 -0
  33. package/dist/lib/config.js +51 -0
  34. package/dist/lib/schemas.d.ts +127 -0
  35. package/dist/lib/schemas.js +55 -0
  36. package/dist/lib/utils.d.ts +89 -0
  37. package/dist/lib/utils.js +170 -0
  38. package/oclif.manifest.json +310 -0
  39. package/package.json +78 -0
  40. package/static/challenge.ts +32 -0
  41. package/static/project_template/dev.env +6 -0
  42. package/static/project_template/package.json +17 -0
  43. package/static/project_template/prod.env +6 -0
  44. package/static/project_template/template-run.json +10 -0
  45. package/static/project_template/tsconfig.json +17 -0
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # Kradle CLI
2
+
3
+ Kradle's CLI for managing Minecraft challenges, evaluations, agents, and more!
4
+
5
+ ## Kradle - private installation
6
+
7
+ 1. Install Kradle's CLI globally
8
+ ```
9
+ npm i -g @kradle/cli
10
+ ```
11
+ 2. Initialize a new project
12
+ ```
13
+ kradle init
14
+ ```
15
+ 3. Congrats šŸŽ‰ You can now create a new challenge:
16
+ ```
17
+ kradle challenge create <challenge-name>
18
+ ```
19
+
20
+ In addition, you can enable [autocomplete](#Autocomplete).
21
+
22
+ ## Autocomplete
23
+
24
+ Kradle CLI supports shell autocomplete for faster command entry. After installation, enable autocomplete for your shell:
25
+
26
+ ```bash
27
+ kradle autocomplete
28
+ # Follow the instructions printed
29
+ ```
30
+
31
+ The command will display instructions for your specific shell.
32
+
33
+ After setup, you will be able to use Tab to autocomplete:
34
+ ```bash
35
+ kradle challenge <TAB> # Shows: build, create, list, run, upload, watch, etc.
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ The `.env` should have the following variables:
41
+
42
+ ```env
43
+ WEB_API_URL=https://api.kradle.ai
44
+ WEB_URL=https://kradle.ai
45
+ STUDIO_API_URL=http://localhost:8080
46
+ STUDIO_URL=kradle-studio://
47
+ KRADLE_API_KEY=your-api-key
48
+ GCS_BUCKET=your-gcs-bucket
49
+ KRADLE_CHALLENGES_PATH=~/Documents/kradle-studio/challenges
50
+ ```
51
+
52
+ ## Commands
53
+
54
+ ### Create Challenge
55
+
56
+ Create a new challenge locally and in the cloud:
57
+
58
+ ```bash
59
+ kradle challenge create <challenge-name>
60
+ ```
61
+
62
+ This creates a `challenges/<challenge-name>/` folder with:
63
+ - `challenge.ts`: The entrypoint defining challenge behavior
64
+ - `config.ts`: TypeScript file with challenge metadata (auto-generated from cloud API)
65
+
66
+ ### Build Challenge
67
+
68
+ Build challenge datapack and upload both config and datapack:
69
+
70
+ ```bash
71
+ kradle challenge build <challenge-name>
72
+ ```
73
+
74
+ This command:
75
+ 1. Creates the challenge in the cloud (if it doesn't already exists)
76
+ 2. Uploads `config.ts` to cloud (if it exists)
77
+ 3. Builds the datapack by executing `challenge.ts`
78
+ 4. Uploads the datapack to GCS
79
+
80
+ ### Delete Challenge
81
+
82
+ Delete a challenge locally, from the cloud, or both:
83
+
84
+ ```bash
85
+ # Will ask confirmation for local & cloud deletion
86
+ kradle challenge delete <challenge-name>
87
+
88
+ # Doesn't ask for confirmation
89
+ kradle challenge delete <challenge-name> --yes
90
+ ```
91
+
92
+ ### List Challenges
93
+
94
+ List all challenges (local and cloud):
95
+
96
+ ```bash
97
+ kradle challenge list
98
+ ```
99
+
100
+ ### Watch Challenge
101
+
102
+ Watch a challenge for changes and auto-rebuild/upload:
103
+
104
+ ```bash
105
+ kradle challenge watch <challenge-name>
106
+ ```
107
+
108
+ Uses file watching with debouncing (300ms) and hash comparison to minimize unnecessary rebuilds.
109
+
110
+ ### Run Challenge
111
+
112
+ Run a challenge in production or studio environment:
113
+
114
+ ```bash
115
+ kradle challenge run <challenge-name>
116
+ kradle challenge run <challenge-name> --studio # Run in local studio environment
117
+ ```
118
+
119
+ ### Multi-Upload
120
+
121
+ Interactively select and upload multiple challenges:
122
+
123
+ ```bash
124
+ kradle challenge multi-upload
125
+ ```
126
+
127
+ Provides an interactive UI to select multiple challenges and uploads them in parallel.
128
+
129
+ ## Development
130
+
131
+ ### Setup
132
+
133
+ This CLI requires linking to be used locally:
134
+
135
+ ```bash
136
+ npm install
137
+ npm run build
138
+ npm link
139
+ ```
140
+
141
+ ### `kradle` vs `kradle-dev`
142
+
143
+ The repository provides two CLI commands:
144
+
145
+ - **`kradle`**: Production CLI that runs compiled JavaScript from `dist/`
146
+ - Requires `npm run build` after every code change
147
+ - This is what end users will use
148
+
149
+ - **`kradle-dev`**: Development CLI that runs TypeScript directly
150
+ - No build step required
151
+ - Changes are reflected immediately
152
+ - **Always use this during development**
153
+
154
+ Example usage:
155
+ ```bash
156
+ kradle-dev challenge list
157
+ kradle-dev challenge build <challenge-name>
158
+ kradle-dev challenge run <challenge-name>
159
+ ```
160
+
161
+ ### Build & Lint
162
+
163
+ ```bash
164
+ npm run build # Compile TypeScript to dist/
165
+ npm run lint # Check for linting issues
166
+ npm run lint:fix # Auto-fix linting issues
167
+ npm run format # Format code with Biome
168
+ ```
169
+
170
+ ### Challenge Structure
171
+
172
+ Each challenge is a folder in `challenges/<slug>/` containing:
173
+
174
+ - **`challenge.ts`**: Entrypoint that defines challenge behavior using the Sandstone API
175
+ - **`config.ts`**: TypeScript file exporting challenge metadata (name, visibility, roles, objectives, etc.)
176
+
177
+ **Workflow:**
178
+ 1. `kradle challenge create <slug>` creates the folder with `challenge.ts`
179
+ 2. The create command automatically builds, uploads, and downloads the config from the cloud API
180
+ 3. The downloaded JSON is converted into a typed TypeScript `config.ts` file
181
+ 4. `kradle challenge build <slug>` automatically uploads `config.ts` (if it exists) before building the datapack
182
+ 5. You can modify `config.ts` locally and run `build` to sync changes to the cloud
183
+
184
+ ### Configuration Note
185
+
186
+ 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.
187
+
188
+ ## Architecture
189
+
190
+ The CLI is built with:
191
+
192
+ - **oclif**: CLI framework
193
+ - **enquirer**: Interactive prompts
194
+ - **listr2**: Task list UI
195
+ - **picocolors**: Terminal colors
196
+ - **zod**: Schema validation
197
+ - **chokidar**: File watching
198
+ - **biome**: Linting and formatting
199
+
200
+ ### Project Structure
201
+
202
+ ```
203
+ kradle-cli/
204
+ ā”œā”€ā”€ src/
205
+ │ ā”œā”€ā”€ commands/ # CLI commands
206
+ │ │ └── challenge/ # Challenge management commands
207
+ │ │ ā”œā”€ā”€ build.ts # Build & upload datapack + config
208
+ │ │ ā”œā”€ā”€ create.ts # Create new challenge
209
+ │ │ ā”œā”€ā”€ list.ts # List local & cloud challenges
210
+ │ │ ā”œā”€ā”€ multi-upload.ts # Interactive multi-select upload
211
+ │ │ ā”œā”€ā”€ run.ts # Run challenge (prod or studio)
212
+ │ │ ā”œā”€ā”€ watch.ts # Watch for changes & auto-rebuild
213
+ │ └── lib/ # Core libraries
214
+ │ ā”œā”€ā”€ api-client.ts # Typed API client with Zod validation
215
+ │ ā”œā”€ā”€ arguments.ts # Shared CLI arguments with autocomplete
216
+ │ ā”œā”€ā”€ challenge.ts # Challenge class (build, upload, hash, config)
217
+ │ ā”œā”€ā”€ config.ts # Environment config with Zod schemas
218
+ │ ā”œā”€ā”€ schemas.ts # Zod schemas for type-safe API interactions
219
+ │ └── utils.ts # Utility functions
220
+ ā”œā”€ā”€ static/ # Template files (challenge.ts)
221
+ ā”œā”€ā”€ biome.json # Biome linter & formatter config
222
+ ā”œā”€ā”€ package.json
223
+ └── tsconfig.json # TypeScript ESM configuration
224
+ ```
package/bin/dev.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ npx --yes tsx "%~dp0\dev" %*
package/bin/dev.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env -S npx --yes tsx
2
+
3
+ import { execute } from "@oclif/core";
4
+ import path from "node:path";
5
+ import fs from "node:fs";
6
+ import dotenv from "dotenv";
7
+
8
+ // Load .env file from cwd if it exists
9
+ const envPath = path.join(process.cwd(), ".env");
10
+ if (fs.existsSync(envPath)) {
11
+ dotenv.config({ path: envPath, quiet: true });
12
+ }
13
+
14
+ await execute({ development: true, dir: import.meta.url });
package/bin/run.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node "%~dp0\run" %*
package/bin/run.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node --no-warnings
2
+
3
+ import { execute } from "@oclif/core";
4
+ import path from "node:path";
5
+ import fs from "node:fs";
6
+ import dotenv from "dotenv";
7
+
8
+ // Load .env file from cwd if it exists
9
+ const envPath = path.join(process.cwd(), ".env");
10
+ if (fs.existsSync(envPath)) {
11
+ dotenv.config({ path: envPath, quiet: true });
12
+ }
13
+
14
+ await execute({ dir: import.meta.url });
@@ -0,0 +1,6 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class List extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,20 @@
1
+ import { Command } from "@oclif/core";
2
+ import pc from "picocolors";
3
+ import { ApiClient } from "../../lib/api-client.js";
4
+ import { loadConfig } from "../../lib/config.js";
5
+ export default class List extends Command {
6
+ static description = "List all agents";
7
+ static examples = ["<%= config.bin %> <%= command.id %>"];
8
+ async run() {
9
+ const config = loadConfig();
10
+ await this.parse(List);
11
+ const api = new ApiClient(config);
12
+ this.log(pc.blue(">> Loading agents..."));
13
+ const agents = await api.listKradleAgents();
14
+ agents.sort((a, b) => a.username?.localeCompare(b.username || "") || 0);
15
+ this.log(pc.bold(`\nFound ${agents.length} agents:\n`));
16
+ for (const agent of agents) {
17
+ this.log(pc.bold(`- ${agent.username}`));
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Build extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ challenge: import("@oclif/core/interfaces").Arg<string>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,25 @@
1
+ import { Command } from "@oclif/core";
2
+ import pc from "picocolors";
3
+ import { ApiClient } from "../../lib/api-client.js";
4
+ import { getChallengeSlugArgument } from "../../lib/arguments.js";
5
+ import { Challenge } from "../../lib/challenge.js";
6
+ import { loadConfig } from "../../lib/config.js";
7
+ export default class Build extends Command {
8
+ static description = "Build and upload challenge datapack and config";
9
+ static examples = ["<%= config.bin %> <%= command.id %> my-challenge"];
10
+ static args = {
11
+ challenge: getChallengeSlugArgument({ description: "Challenge slug to build" }),
12
+ };
13
+ 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);
20
+ }
21
+ catch (error) {
22
+ this.error(pc.red(`Build failed: ${error instanceof Error ? error.message : String(error)}`));
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Create extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ challenge: import("@oclif/core/interfaces").Arg<string>;
7
+ };
8
+ static flags: {
9
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,87 @@
1
+ import fs from "node:fs/promises";
2
+ import { Command, Flags } from "@oclif/core";
3
+ import { Listr } from "listr2";
4
+ import pc from "picocolors";
5
+ import { ApiClient } from "../../lib/api-client.js";
6
+ import { getChallengeSlugArgument } from "../../lib/arguments.js";
7
+ import { Challenge } from "../../lib/challenge.js";
8
+ import { loadConfig } from "../../lib/config.js";
9
+ export default class Create extends Command {
10
+ static description = "Create a new challenge locally and in the cloud";
11
+ static examples = ["<%= config.bin %> <%= command.id %> my-challenge"];
12
+ static args = {
13
+ challenge: getChallengeSlugArgument({ description: "Challenge slug to create" }),
14
+ };
15
+ static flags = {
16
+ verbose: Flags.boolean({ char: "v", description: "Verbose output", default: false }),
17
+ };
18
+ async run() {
19
+ 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);
23
+ const tasks = new Listr([
24
+ {
25
+ title: "Checking if challenge exists",
26
+ task: async (_, task) => {
27
+ const exists = await api.challengeExists(args.challenge);
28
+ if (exists) {
29
+ this.error(pc.red(`Challenge already exists: ${args.challenge}`));
30
+ }
31
+ task.title = `Challenge does not exist.`;
32
+ },
33
+ },
34
+ {
35
+ title: "Creating local challenge folder",
36
+ task: async (_, task) => {
37
+ await Challenge.createLocal(args.challenge, config);
38
+ task.title = `Created local challenge folder: ${challenge.challengeDir}`;
39
+ },
40
+ },
41
+ {
42
+ title: "Creating cloud challenge",
43
+ task: async (_, task) => {
44
+ await api.createChallenge(args.challenge);
45
+ task.title = `Created cloud challenge`;
46
+ },
47
+ },
48
+ {
49
+ title: "Downloading challenge config",
50
+ task: async (_, task) => {
51
+ const challengeData = await api.getChallenge(args.challenge);
52
+ // Remove fields that shouldn't be in the config file
53
+ const { id, creationTime, updateTime, creator, ...cleanChallenge } = challengeData;
54
+ // Remove quotes from keys
55
+ const configStr = JSON.stringify(cleanChallenge, null, 2).replace(/"([a-zA-Z0-9_]+)":/g, "$1:");
56
+ await fs.writeFile(challenge.configPath, `
57
+ export const config = ${configStr};
58
+ `.trim());
59
+ task.title = `Downloaded config to: ${challenge.configPath}`;
60
+ },
61
+ },
62
+ {
63
+ title: "Building initial datapack",
64
+ task: async (_, task) => {
65
+ await challenge.build(!flags.verbose);
66
+ task.title = `Built initial datapack`;
67
+ },
68
+ },
69
+ {
70
+ title: "Uploading initial datapack",
71
+ task: async (_, task) => {
72
+ await challenge.upload(api);
73
+ task.title = `Uploaded initial datapack`;
74
+ },
75
+ },
76
+ ]);
77
+ try {
78
+ await tasks.run();
79
+ this.log(pc.green(`\nāœ“ Challenge created: ${args.challenge}`));
80
+ this.log(pc.green(`↳ Run "kradle challenge watch ${args.challenge}" to watch your challenge and start testing!`));
81
+ this.log(pc.dim(`\nSee your challenge at: ${config.WEB_URL}/studio/challenges/${args.challenge}?tab=challenge-definition`));
82
+ }
83
+ catch (error) {
84
+ this.error(pc.red(`Create failed: ${error instanceof Error ? error.message : String(error)}`));
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Delete extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ challenge: import("@oclif/core/interfaces").Arg<string>;
7
+ };
8
+ static flags: {
9
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,99 @@
1
+ import fs from "node:fs/promises";
2
+ import { Command, Flags } from "@oclif/core";
3
+ import enquirer from "enquirer";
4
+ import pc from "picocolors";
5
+ import { ApiClient } from "../../lib/api-client.js";
6
+ import { getChallengeSlugArgument } from "../../lib/arguments.js";
7
+ import { Challenge } from "../../lib/challenge.js";
8
+ import { loadConfig } from "../../lib/config.js";
9
+ export default class Delete extends Command {
10
+ static description = "Delete a challenge locally and from the cloud";
11
+ static examples = [
12
+ "<%= config.bin %> <%= command.id %> my-challenge",
13
+ "<%= config.bin %> <%= command.id %> my-challenge --yes",
14
+ ];
15
+ static args = {
16
+ challenge: getChallengeSlugArgument({ description: "Challenge slug to delete" }),
17
+ };
18
+ static flags = {
19
+ yes: Flags.boolean({ char: "y", description: "Skip confirmation prompts", default: false }),
20
+ };
21
+ async run() {
22
+ 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);
26
+ // Check if challenge exists locally
27
+ const existsLocally = challenge.exists();
28
+ // Check if challenge exists in cloud
29
+ const existsInCloud = await api.challengeExists(challenge.shortSlug);
30
+ // If challenge doesn't exist anywhere, inform user and exit
31
+ if (!existsLocally && !existsInCloud) {
32
+ this.error(pc.red(`Challenge "${challenge.shortSlug}" does not exist locally or in the cloud.`));
33
+ }
34
+ // Show what will be deleted
35
+ this.log(pc.bold(`\nChallenge: ${pc.cyan(challenge.shortSlug)}`));
36
+ this.log(` šŸ’» Local: ${existsLocally ? pc.green("āœ“ exists") : pc.dim("āœ— not found")}`);
37
+ this.log(` ā˜ļø Cloud: ${existsInCloud ? pc.green("āœ“ exists") : pc.dim("āœ— not found")}`);
38
+ this.log("");
39
+ // Confirm deletion from cloud
40
+ if (existsInCloud) {
41
+ if (!flags.yes) {
42
+ try {
43
+ const response = await enquirer.prompt({
44
+ type: "confirm",
45
+ name: "confirm",
46
+ message: `Delete challenge from cloud? ${pc.red("This cannot be undone.")}`,
47
+ initial: false,
48
+ });
49
+ if (!response.confirm) {
50
+ this.log(pc.yellow("āœ— Cloud deletion cancelled"));
51
+ return;
52
+ }
53
+ }
54
+ catch (error) {
55
+ this.log(pc.yellow("\nāœ— Cloud deletion cancelled"));
56
+ return;
57
+ }
58
+ }
59
+ try {
60
+ this.log(pc.blue(">> Deleting from cloud..."));
61
+ await api.deleteChallenge(challenge.shortSlug);
62
+ this.log(pc.green("āœ“ Deleted from cloud"));
63
+ }
64
+ catch (error) {
65
+ this.error(pc.red(`Failed to delete from cloud: ${error instanceof Error ? error.message : String(error)}`));
66
+ }
67
+ }
68
+ // Confirm deletion locally
69
+ if (existsLocally) {
70
+ if (!flags.yes) {
71
+ try {
72
+ const response = await enquirer.prompt({
73
+ type: "confirm",
74
+ name: "confirm",
75
+ message: `Delete local challenge folder? ${pc.red("This cannot be undone.")}`,
76
+ initial: false,
77
+ });
78
+ if (!response.confirm) {
79
+ this.log(pc.yellow("āœ— Local deletion cancelled"));
80
+ return;
81
+ }
82
+ }
83
+ catch (error) {
84
+ this.log(pc.yellow("\nāœ— Local deletion cancelled"));
85
+ return;
86
+ }
87
+ }
88
+ try {
89
+ this.log(pc.blue(">> Deleting local folder..."));
90
+ await fs.rm(challenge.challengeDir, { recursive: true, force: true });
91
+ this.log(pc.green("āœ“ Deleted local folder"));
92
+ }
93
+ catch (error) {
94
+ this.error(pc.red(`Failed to delete local folder: ${error instanceof Error ? error.message : String(error)}`));
95
+ }
96
+ }
97
+ this.log(pc.green(`\nāœ“ Challenge "${challenge.shortSlug}" deleted successfully!`));
98
+ }
99
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class List extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,48 @@
1
+ import { Command } from "@oclif/core";
2
+ import pc from "picocolors";
3
+ import { ApiClient } from "../../lib/api-client.js";
4
+ import { Challenge } from "../../lib/challenge.js";
5
+ import { loadConfig } from "../../lib/config.js";
6
+ export default class List extends Command {
7
+ static description = "List all challenges (local and cloud)";
8
+ static examples = ["<%= config.bin %> <%= command.id %>"];
9
+ async run() {
10
+ const config = loadConfig();
11
+ await this.parse(List);
12
+ const api = new ApiClient(config);
13
+ this.log(pc.blue(">> Loading challenges..."));
14
+ const [cloudChallenges, localChallenges, human] = await Promise.all([
15
+ api.listChallenges(),
16
+ Challenge.getLocalChallenges(),
17
+ api.getHuman(),
18
+ ]);
19
+ // Create a map for easy lookup
20
+ 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
+ ]);
25
+ this.log(pc.bold("\nChallenges:\n"));
26
+ this.log(`${"Status".padEnd(15)} ${"Slug".padEnd(40)} ${"Name".padEnd(30)}`);
27
+ this.log("-".repeat(90));
28
+ for (const slug of Array.from(allSlugs).sort()) {
29
+ const challenge = new Challenge(slug, config);
30
+ const inCloud = cloudMap.has(slug);
31
+ const inLocal = localChallenges[challenge.shortSlug];
32
+ let status;
33
+ if (inCloud && inLocal) {
34
+ status = pc.green("āœ“ synced");
35
+ }
36
+ else if (inCloud) {
37
+ status = pc.yellow("☁ cloud only");
38
+ }
39
+ else {
40
+ status = pc.blue("⊔ local only");
41
+ }
42
+ const cloudChallenge = cloudMap.get(slug);
43
+ const name = cloudChallenge?.name || "-";
44
+ this.log(`${status.padEnd(24)} ${slug.padEnd(40)} ${name.padEnd(30)}`);
45
+ }
46
+ this.log(pc.dim(`\nTotal: ${allSlugs.size} challenges`));
47
+ }
48
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class MultiUpload extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }