@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.
- package/README.md +224 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +14 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +14 -0
- package/dist/commands/agent/list.d.ts +6 -0
- package/dist/commands/agent/list.js +20 -0
- package/dist/commands/challenge/build.d.ts +9 -0
- package/dist/commands/challenge/build.js +25 -0
- package/dist/commands/challenge/create.d.ts +12 -0
- package/dist/commands/challenge/create.js +87 -0
- package/dist/commands/challenge/delete.d.ts +12 -0
- package/dist/commands/challenge/delete.js +99 -0
- package/dist/commands/challenge/list.d.ts +6 -0
- package/dist/commands/challenge/list.js +48 -0
- package/dist/commands/challenge/multi-upload.d.ts +6 -0
- package/dist/commands/challenge/multi-upload.js +80 -0
- package/dist/commands/challenge/run.d.ts +12 -0
- package/dist/commands/challenge/run.js +47 -0
- package/dist/commands/challenge/watch.d.ts +12 -0
- package/dist/commands/challenge/watch.js +113 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.js +161 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/api-client.d.ts +55 -0
- package/dist/lib/api-client.js +162 -0
- package/dist/lib/arguments.d.ts +7 -0
- package/dist/lib/arguments.js +17 -0
- package/dist/lib/challenge.d.ts +67 -0
- package/dist/lib/challenge.js +203 -0
- package/dist/lib/config.d.ts +13 -0
- package/dist/lib/config.js +51 -0
- package/dist/lib/schemas.d.ts +127 -0
- package/dist/lib/schemas.js +55 -0
- package/dist/lib/utils.d.ts +89 -0
- package/dist/lib/utils.js +170 -0
- package/oclif.manifest.json +310 -0
- package/package.json +78 -0
- package/static/challenge.ts +32 -0
- package/static/project_template/dev.env +6 -0
- package/static/project_template/package.json +17 -0
- package/static/project_template/prod.env +6 -0
- package/static/project_template/template-run.json +10 -0
- 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
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
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,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,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,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
|
+
}
|