kradle 0.6.6 → 0.6.7

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 CHANGED
@@ -81,23 +81,24 @@ This creates a `challenges/<challenge-name>/` folder with:
81
81
 
82
82
  ### Build Challenge
83
83
 
84
- Build, validate, and upload challenge datapack:
84
+ Build and validate challenge datapacks locally, with optional upload:
85
85
 
86
86
  ```bash
87
87
  kradle challenge build <challenge-name>
88
+ kradle challenge build <challenge-name> --no-upload # Build locally only (no cloud upload)
88
89
  kradle challenge build <challenge-name> --no-validate # Skip validation
89
90
  kradle challenge build <challenge-name> --public # Upload as public
90
91
  kradle challenge build --all # Build all local challenges
91
92
  ```
92
93
 
93
94
  This command:
94
- 1. Creates the challenge in the cloud (if it doesn't already exist)
95
- 2. Builds the datapack by executing `challenge.ts`
96
- 3. Validates the datapack using Spyglass (blocks upload if errors found)
97
- 4. Uploads `config.ts` to cloud
98
- 5. Uploads the datapack to GCS
95
+ 1. Builds the datapack by executing `challenge.ts`
96
+ 2. Validates the datapack using Spyglass (unless `--no-validate`)
97
+ 3. If upload is enabled (default), creates the challenge in the cloud if needed
98
+ 4. If upload is enabled (default), uploads `config.ts` and datapack
99
99
 
100
- Validation checks `.mcfunction` files for syntax errors, invalid commands, and other issues. Use `--no-validate` to skip this step.
100
+ Use `--no-upload` to build locally only. Use `--no-validate` to skip validation.
101
+ `--public` is only valid when upload is enabled (default).
101
102
 
102
103
  ### Delete Challenge
103
104
 
@@ -478,7 +479,7 @@ Each challenge is a folder in `challenges/<slug>/` containing:
478
479
  1. `kradle challenge create <slug>` creates the folder with `challenge.ts`
479
480
  2. The create command automatically builds, uploads, and downloads the config from the cloud API
480
481
  3. The downloaded JSON is converted into a typed TypeScript `config.ts` file
481
- 4. `kradle challenge build <slug>` automatically uploads `config.ts` (if it exists) before building the datapack
482
+ 4. `kradle challenge build <slug>` builds datapacks locally and uploads `config.ts` + datapack by default (use `--no-upload` for local-only build)
482
483
  5. You can modify `config.ts` locally and run `build` to sync changes to the cloud
483
484
 
484
485
  ## Architecture
@@ -6,13 +6,15 @@ export default class Build extends Command {
6
6
  challengeSlug: import("@oclif/core/interfaces").Arg<string | undefined>;
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
9
  "challenges-path": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
10
  all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
11
  public: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
12
  "no-validate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ "no-upload": import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
16
  };
16
17
  static strict: boolean;
17
18
  run(): Promise<void>;
19
+ private requireApiKey;
18
20
  }
@@ -5,7 +5,7 @@ import { extractShortSlug, getChallengeSlugArgument } from "../../lib/arguments.
5
5
  import { Challenge } from "../../lib/challenge.js";
6
6
  import { getConfigFlags } from "../../lib/flags.js";
7
7
  export default class Build extends Command {
8
- static description = "Build and upload challenge datapack and config";
8
+ static description = "Build challenge datapack locally, with optional upload";
9
9
  static examples = [
10
10
  "<%= config.bin %> <%= command.id %> my-challenge",
11
11
  "<%= config.bin %> <%= command.id %> my-challenge my-other-challenge",
@@ -13,7 +13,7 @@ export default class Build extends Command {
13
13
  ];
14
14
  static args = {
15
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.",
16
+ description: "Challenge slug to build. Can be used multiple times to build multiple challenges at once. Incompatible with --all flag.",
17
17
  required: false,
18
18
  }),
19
19
  };
@@ -21,10 +21,23 @@ export default class Build extends Command {
21
21
  all: Flags.boolean({ char: "a", description: "Build all challenges in the challenges directory", default: false }),
22
22
  public: Flags.boolean({ char: "p", description: "Upload challenges as public.", default: false }),
23
23
  "no-validate": Flags.boolean({
24
- description: "Skip datapack validation before upload",
24
+ description: "Skip datapack validation",
25
25
  default: false,
26
26
  }),
27
- ...getConfigFlags("api-key", "api-url", "challenges-path"),
27
+ "no-upload": Flags.boolean({
28
+ description: "Build datapack locally only (skip cloud config/datapack upload)",
29
+ default: false,
30
+ }),
31
+ "api-url": Flags.string({
32
+ description: "Kradle Web API URL",
33
+ env: "KRADLE_API_URL",
34
+ default: "https://api.kradle.ai/v0",
35
+ }),
36
+ "api-key": Flags.string({
37
+ description: "Kradle API key",
38
+ env: "KRADLE_API_KEY",
39
+ }),
40
+ ...getConfigFlags("challenges-path"),
28
41
  };
29
42
  static strict = false;
30
43
  async run() {
@@ -32,6 +45,9 @@ export default class Build extends Command {
32
45
  if (flags.all && argv.length > 0) {
33
46
  this.error(pc.red("Cannot use --all flag with challenge slugs"));
34
47
  }
48
+ if (flags["no-upload"] && flags.public) {
49
+ this.error(pc.red("Cannot use --public with --no-upload"));
50
+ }
35
51
  if (!flags.all && argv.length === 0) {
36
52
  // Show help if no challenge slugs are provided - https://github.com/oclif/oclif/issues/183#issuecomment-1933104981
37
53
  await new (await loadHelpClass(this.config))(this.config).showHelp([Build.id]);
@@ -41,7 +57,8 @@ export default class Build extends Command {
41
57
  this.log(pc.blue("Building all challenges"));
42
58
  }
43
59
  const challengeSlugs = flags.all ? await Challenge.getLocalChallenges() : argv;
44
- const api = new ApiClient(flags["api-url"], flags["api-key"]);
60
+ const shouldUpload = !flags["no-upload"];
61
+ const api = shouldUpload ? new ApiClient(flags["api-url"], this.requireApiKey(flags["api-key"])) : null;
45
62
  for (const challengeSlug of challengeSlugs) {
46
63
  // Validate that the challenge exists locally
47
64
  const validation = await Challenge.validateForLocalOperation(challengeSlug, flags["challenges-path"]);
@@ -51,10 +68,17 @@ export default class Build extends Command {
51
68
  const challenge = new Challenge(extractShortSlug(challengeSlug), flags["challenges-path"]);
52
69
  this.log(pc.blue(`==== Building challenge: ${challenge.shortSlug} ====`));
53
70
  try {
54
- await challenge.buildAndUpload(api, {
55
- asPublic: flags.public,
56
- validate: !flags["no-validate"],
57
- });
71
+ if (shouldUpload && api) {
72
+ await challenge.buildAndUpload(api, {
73
+ asPublic: flags.public,
74
+ validate: !flags["no-validate"],
75
+ });
76
+ }
77
+ else {
78
+ await challenge.buildLocal({
79
+ validate: !flags["no-validate"],
80
+ });
81
+ }
58
82
  }
59
83
  catch (error) {
60
84
  this.error(pc.red(`Build failed: ${error instanceof Error ? error.message : String(error)}`));
@@ -62,4 +86,11 @@ export default class Build extends Command {
62
86
  this.log();
63
87
  }
64
88
  }
89
+ requireApiKey(apiKey) {
90
+ if (apiKey) {
91
+ return apiKey;
92
+ }
93
+ this.error(pc.red("Missing required flag --api-key (or KRADLE_API_KEY) unless using --no-upload"));
94
+ throw new Error("Unreachable");
95
+ }
65
96
  }
@@ -51,6 +51,18 @@ export declare class Challenge {
51
51
  */
52
52
  build(silent?: boolean, config?: ChallengeConfigSchemaType): Promise<void>;
53
53
  loadConfig(): Promise<ChallengeConfigSchemaType>;
54
+ /**
55
+ * Build the challenge datapack locally.
56
+ * @param options - Build options
57
+ * @param options.validate - Whether to validate the datapack after build (default: true).
58
+ * @returns The loaded config and datapack hash.
59
+ */
60
+ buildLocal(options?: {
61
+ validate?: boolean;
62
+ }): Promise<{
63
+ config: ChallengeConfigSchemaType;
64
+ datapackHash: string;
65
+ }>;
54
66
  /**
55
67
  * Build the challenge datapack and upload it to the cloud.
56
68
  * @param api - The API client to use.
@@ -162,17 +162,15 @@ export class Challenge {
162
162
  }
163
163
  }
164
164
  /**
165
- * Build the challenge datapack and upload it to the cloud.
166
- * @param api - The API client to use.
165
+ * Build the challenge datapack locally.
167
166
  * @param options - Build options
168
- * @param options.asPublic - Whether the challenge should be uploaded as public.
169
- * @param options.validate - Whether to validate the datapack before upload (default: true).
170
- * @returns The config and datapack hash.
167
+ * @param options.validate - Whether to validate the datapack after build (default: true).
168
+ * @returns The loaded config and datapack hash.
171
169
  */
172
- async buildAndUpload(api, options = { asPublic: false, validate: true }) {
173
- const { asPublic, validate = true } = options;
170
+ async buildLocal(options = { validate: true }) {
171
+ const { validate = true } = options;
174
172
  // Start validation service initialization in parallel with build (if validation enabled)
175
- // This overlaps the ~1.5s init time with the build process
173
+ // This overlaps the init time with the build process.
176
174
  let validationService = null;
177
175
  if (validate) {
178
176
  validationService = ValidationService.create(this.builtDatapackPath);
@@ -182,19 +180,12 @@ export class Challenge {
182
180
  await validationService?.close();
183
181
  throw new Error(`Challenge "${this.shortSlug}" does not exist locally. Make sure both the challenge.ts file and the config.ts file exist.`);
184
182
  }
185
- // Ensure challenge exists in the cloud
186
- if (!(await api.challengeExists(this.shortSlug))) {
187
- console.log(pc.yellow(`Challenge not found in cloud: ${this.shortSlug}`));
188
- console.log(pc.yellow(`Creating challenge: ${this.shortSlug}`));
189
- await api.createChallenge(this.shortSlug);
190
- console.log(pc.green(`✓ Challenge created in cloud`));
191
- }
192
183
  const config = await this.loadConfig();
193
184
  // Build datapack (validation service initializes in parallel)
194
185
  console.log(pc.blue(`>> Building datapack: ${this.shortSlug}`));
195
186
  await this.build(false, config);
196
187
  console.log(pc.green(`✓ Datapack built\n`));
197
- // Validate datapack before uploading (if enabled)
188
+ // Validate datapack after building (if enabled)
198
189
  if (validate && validationService) {
199
190
  console.log(pc.blue(`>> Validating datapack: ${this.shortSlug}`));
200
191
  try {
@@ -202,13 +193,34 @@ export class Challenge {
202
193
  console.log(formatValidationResult(validationResult));
203
194
  console.log();
204
195
  if (validationResult.hasErrors) {
205
- throw new Error(`Validation failed with ${validationResult.errors.length} error(s). Upload aborted.`);
196
+ throw new Error(`Validation failed with ${validationResult.errors.length} error(s). Build aborted.`);
206
197
  }
207
198
  }
208
199
  finally {
209
200
  await validationService.close();
210
201
  }
211
202
  }
203
+ const datapackHash = await this.getDatapackHash();
204
+ return { config, datapackHash };
205
+ }
206
+ /**
207
+ * Build the challenge datapack and upload it to the cloud.
208
+ * @param api - The API client to use.
209
+ * @param options - Build options
210
+ * @param options.asPublic - Whether the challenge should be uploaded as public.
211
+ * @param options.validate - Whether to validate the datapack before upload (default: true).
212
+ * @returns The config and datapack hash.
213
+ */
214
+ async buildAndUpload(api, options = { asPublic: false, validate: true }) {
215
+ const { asPublic, validate = true } = options;
216
+ const { config, datapackHash } = await this.buildLocal({ validate });
217
+ // Ensure challenge exists in the cloud
218
+ if (!(await api.challengeExists(this.shortSlug))) {
219
+ console.log(pc.yellow(`Challenge not found in cloud: ${this.shortSlug}`));
220
+ console.log(pc.yellow(`Creating challenge: ${this.shortSlug}`));
221
+ await api.createChallenge(this.shortSlug);
222
+ console.log(pc.green(`✓ Challenge created in cloud`));
223
+ }
212
224
  // Ensure challenge's visibility is set to private - else, temporarily set it to private
213
225
  const cloudChallengeVisibility = (await api.getChallenge(this.shortSlug, ["visibility"])).visibility;
214
226
  if (cloudChallengeVisibility === "public") {
@@ -230,7 +242,6 @@ export class Challenge {
230
242
  await api.updateChallengeVisibility(this.shortSlug, "public");
231
243
  console.log(pc.green(`✓ Challenge visibility set to public\n`));
232
244
  }
233
- const datapackHash = await this.getDatapackHash();
234
245
  return { config, datapackHash };
235
246
  }
236
247
  /**
@@ -12,6 +12,8 @@ import pc from "picocolors";
12
12
  */
13
13
  const FILTERED_ERROR_MESSAGES = [
14
14
  "Fake names cannot be longer than 40 characters", // Actually they can be longer
15
+ "Objective names cannot be longer than 16 characters", // Actually they can be longer
16
+ "Player names cannot be longer than 16 characters", // Actually they can be longer
15
17
  ];
16
18
  /**
17
19
  * Get a human-readable severity label with color
@@ -215,12 +215,12 @@
215
215
  "aliases": [],
216
216
  "args": {
217
217
  "challengeSlug": {
218
- "description": "Challenge slug to build and upload. Can be used multiple times to build and upload multiple challenges at once. Incompatible with --all flag.",
218
+ "description": "Challenge slug to build. Can be used multiple times to build multiple challenges at once. Incompatible with --all flag.",
219
219
  "name": "challengeSlug",
220
220
  "required": false
221
221
  }
222
222
  },
223
- "description": "Build and upload challenge datapack and config",
223
+ "description": "Build challenge datapack locally, with optional upload",
224
224
  "examples": [
225
225
  "<%= config.bin %> <%= command.id %> my-challenge",
226
226
  "<%= config.bin %> <%= command.id %> my-challenge my-other-challenge",
@@ -242,30 +242,34 @@
242
242
  "type": "boolean"
243
243
  },
244
244
  "no-validate": {
245
- "description": "Skip datapack validation before upload",
245
+ "description": "Skip datapack validation",
246
246
  "name": "no-validate",
247
247
  "allowNo": false,
248
248
  "type": "boolean"
249
249
  },
250
- "api-key": {
251
- "description": "Kradle API key",
252
- "env": "KRADLE_API_KEY",
253
- "name": "api-key",
254
- "required": true,
255
- "hasDynamicHelp": false,
256
- "multiple": false,
257
- "type": "option"
250
+ "no-upload": {
251
+ "description": "Build datapack locally only (skip cloud config/datapack upload)",
252
+ "name": "no-upload",
253
+ "allowNo": false,
254
+ "type": "boolean"
258
255
  },
259
256
  "api-url": {
260
257
  "description": "Kradle Web API URL",
261
258
  "env": "KRADLE_API_URL",
262
259
  "name": "api-url",
263
- "required": true,
264
260
  "default": "https://api.kradle.ai/v0",
265
261
  "hasDynamicHelp": false,
266
262
  "multiple": false,
267
263
  "type": "option"
268
264
  },
265
+ "api-key": {
266
+ "description": "Kradle API key",
267
+ "env": "KRADLE_API_KEY",
268
+ "name": "api-key",
269
+ "hasDynamicHelp": false,
270
+ "multiple": false,
271
+ "type": "option"
272
+ },
269
273
  "challenges-path": {
270
274
  "description": "Absolute path to the challenges directory",
271
275
  "env": "KRADLE_CHALLENGES_PATH",
@@ -1524,5 +1528,5 @@
1524
1528
  ]
1525
1529
  }
1526
1530
  },
1527
- "version": "0.6.6"
1531
+ "version": "0.6.7"
1528
1532
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kradle",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Kradle's CLI. Manage challenges, experiments, agents and more!",
5
5
  "keywords": [
6
6
  "cli"
@@ -119,11 +119,12 @@ kradle challenge create my-first-challenge
119
119
 
120
120
  ### `kradle challenge build <name>`
121
121
 
122
- Builds a challenge datapack, validates it, and uploads everything to the cloud.
122
+ Builds a challenge datapack locally, validates it, and optionally uploads to the cloud.
123
123
 
124
124
  **Usage:**
125
125
  ```bash
126
126
  kradle challenge build <challenge-name>
127
+ kradle challenge build <challenge-name> --no-upload
127
128
  kradle challenge build <challenge-name> --public
128
129
  kradle challenge build <challenge-name> --no-validate
129
130
  kradle challenge build --all
@@ -140,21 +141,24 @@ kradle challenge build --all --public
140
141
  |------|-------|-------------|---------|
141
142
  | `--all` | `-a` | Build all local challenges | false |
142
143
  | `--public` | `-p` | Set visibility to public after upload | false |
143
- | `--no-validate` | | Skip datapack validation before upload | false |
144
+ | `--no-validate` | | Skip datapack validation | false |
145
+ | `--no-upload` | | Build locally only (skip cloud config/datapack upload) | false |
146
+
147
+ `--public` is incompatible with `--no-upload`.
144
148
 
145
149
  **What it does:**
146
- 1. Creates the challenge in cloud if it doesn't exist
147
- 2. Executes `challenge.ts` to generate the datapack
150
+ 1. Executes `challenge.ts` to generate the datapack
148
151
  - Passes `KRADLE_CHALLENGE_END_STATES` env var containing a JSON array of end state keys from `config.endStates` (e.g., `["victory", "defeat"]`)
149
152
  - Passes `KRADLE_CHALLENGE_ROLES` env var containing a JSON array of role names from `config.roles` (e.g., `["attacker", "defender"]`)
150
153
  - Passes `KRADLE_CHALLENGE_LOCATIONS` env var containing a JSON array of location keys from `config.challengeConfig.locations` (e.g., `["spawn", "goal"]`)
151
154
  - The `@kradle/challenges-sdk` uses these to register valid end states, roles, and locations at build time
152
- 3. Validates the datapack using Spyglass engine (unless `--no-validate`)
155
+ 2. Validates the datapack using Spyglass engine (unless `--no-validate`)
153
156
  - Checks `.mcfunction` files for syntax errors, invalid commands, JSON text components
154
- - **Errors block the upload** - fix them before the challenge can be uploaded
157
+ - **Errors block command completion** - fix them before the build can finish
155
158
  - Auto-detects Minecraft version from `pack.mcmeta` `pack_format`
156
- 4. Uploads `config.ts` metadata to cloud
157
- 5. Compresses and uploads datapack to cloud storage
159
+ 3. If upload is enabled (default), creates challenge in cloud if needed
160
+ 4. If upload is enabled (default), uploads `config.ts` metadata to cloud
161
+ 5. If upload is enabled (default), compresses and uploads datapack to cloud storage
158
162
 
159
163
  **Validation Output:**
160
164
  ```
@@ -164,7 +168,7 @@ data/kradle/functions/init.mcfunction:5:1 warning: Unused objective
164
168
 
165
169
  Found 1 error, 1 warning
166
170
 
167
- Error: Validation failed with 1 error(s). Upload aborted.
171
+ Error: Validation failed with 1 error(s). Build aborted.
168
172
  ```
169
173
 
170
174
  **Examples:**
@@ -172,6 +176,9 @@ Error: Validation failed with 1 error(s). Upload aborted.
172
176
  # Build single challenge (with validation)
173
177
  kradle challenge build my-challenge
174
178
 
179
+ # Build local datapack only (no cloud upload)
180
+ kradle challenge build my-challenge --no-upload
181
+
175
182
  # Build and make public
176
183
  kradle challenge build my-challenge --public
177
184
 
@@ -1274,7 +1281,7 @@ kradle challenge build --all --public
1274
1281
  |---------|-------------|
1275
1282
  | `kradle init` | Initialize new project |
1276
1283
  | `kradle challenge create <name>` | Create new challenge |
1277
- | `kradle challenge build <name>` | Build and upload challenge |
1284
+ | `kradle challenge build <name>` | Build challenge (uploads by default) |
1278
1285
  | `kradle challenge build --all` | Build all challenges |
1279
1286
  | `kradle challenge delete <name>` | Delete challenge |
1280
1287
  | `kradle challenge list` | List all challenges |