kradle 0.6.5 → 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 +16 -7
- package/dist/commands/challenge/build.d.ts +5 -2
- package/dist/commands/challenge/build.js +43 -5
- package/dist/commands/challenge/watch.d.ts +1 -0
- package/dist/commands/challenge/watch.js +55 -3
- package/dist/lib/challenge.d.ts +30 -2
- package/dist/lib/challenge.js +67 -11
- package/dist/lib/validator.d.ts +62 -0
- package/dist/lib/validator.js +285 -0
- package/oclif.manifest.json +28 -12
- package/package.json +4 -1
- package/static/ai_docs/LLM_CLI_REFERENCE.md +60 -19
package/README.md
CHANGED
|
@@ -81,17 +81,24 @@ This creates a `challenges/<challenge-name>/` folder with:
|
|
|
81
81
|
|
|
82
82
|
### Build Challenge
|
|
83
83
|
|
|
84
|
-
Build
|
|
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)
|
|
89
|
+
kradle challenge build <challenge-name> --no-validate # Skip validation
|
|
90
|
+
kradle challenge build <challenge-name> --public # Upload as public
|
|
91
|
+
kradle challenge build --all # Build all local challenges
|
|
88
92
|
```
|
|
89
93
|
|
|
90
94
|
This command:
|
|
91
|
-
1.
|
|
92
|
-
2.
|
|
93
|
-
3.
|
|
94
|
-
4.
|
|
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
|
+
|
|
100
|
+
Use `--no-upload` to build locally only. Use `--no-validate` to skip validation.
|
|
101
|
+
`--public` is only valid when upload is enabled (default).
|
|
95
102
|
|
|
96
103
|
### Delete Challenge
|
|
97
104
|
|
|
@@ -132,9 +139,11 @@ Watch a challenge for changes and auto-rebuild/upload:
|
|
|
132
139
|
|
|
133
140
|
```bash
|
|
134
141
|
kradle challenge watch <challenge-name>
|
|
142
|
+
kradle challenge watch <challenge-name> --no-validate # Skip validation
|
|
143
|
+
kradle challenge watch <challenge-name> --verbose # Verbose output
|
|
135
144
|
```
|
|
136
145
|
|
|
137
|
-
Uses file watching with debouncing (
|
|
146
|
+
Uses file watching with debouncing (1 second) and hash comparison to minimize unnecessary rebuilds. Validation runs after each build but doesn't block uploads in watch mode (errors are displayed but the upload continues).
|
|
138
147
|
|
|
139
148
|
### Run Challenge
|
|
140
149
|
|
|
@@ -470,7 +479,7 @@ Each challenge is a folder in `challenges/<slug>/` containing:
|
|
|
470
479
|
1. `kradle challenge create <slug>` creates the folder with `challenge.ts`
|
|
471
480
|
2. The create command automatically builds, uploads, and downloads the config from the cloud API
|
|
472
481
|
3. The downloaded JSON is converted into a typed TypeScript `config.ts` file
|
|
473
|
-
4. `kradle challenge build <slug>`
|
|
482
|
+
4. `kradle challenge build <slug>` builds datapacks locally and uploads `config.ts` + datapack by default (use `--no-upload` for local-only build)
|
|
474
483
|
5. You can modify `config.ts` locally and run `build` to sync changes to the cloud
|
|
475
484
|
|
|
476
485
|
## Architecture
|
|
@@ -6,12 +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>;
|
|
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>;
|
|
14
16
|
};
|
|
15
17
|
static strict: boolean;
|
|
16
18
|
run(): Promise<void>;
|
|
19
|
+
private requireApiKey;
|
|
17
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
|
|
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,14 +13,31 @@ export default class Build extends Command {
|
|
|
13
13
|
];
|
|
14
14
|
static args = {
|
|
15
15
|
challengeSlug: getChallengeSlugArgument({
|
|
16
|
-
description: "Challenge slug to build
|
|
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
|
};
|
|
20
20
|
static flags = {
|
|
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",
|
|
25
|
+
default: false,
|
|
26
|
+
}),
|
|
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"),
|
|
24
41
|
};
|
|
25
42
|
static strict = false;
|
|
26
43
|
async run() {
|
|
@@ -28,6 +45,9 @@ export default class Build extends Command {
|
|
|
28
45
|
if (flags.all && argv.length > 0) {
|
|
29
46
|
this.error(pc.red("Cannot use --all flag with challenge slugs"));
|
|
30
47
|
}
|
|
48
|
+
if (flags["no-upload"] && flags.public) {
|
|
49
|
+
this.error(pc.red("Cannot use --public with --no-upload"));
|
|
50
|
+
}
|
|
31
51
|
if (!flags.all && argv.length === 0) {
|
|
32
52
|
// Show help if no challenge slugs are provided - https://github.com/oclif/oclif/issues/183#issuecomment-1933104981
|
|
33
53
|
await new (await loadHelpClass(this.config))(this.config).showHelp([Build.id]);
|
|
@@ -37,7 +57,8 @@ export default class Build extends Command {
|
|
|
37
57
|
this.log(pc.blue("Building all challenges"));
|
|
38
58
|
}
|
|
39
59
|
const challengeSlugs = flags.all ? await Challenge.getLocalChallenges() : argv;
|
|
40
|
-
const
|
|
60
|
+
const shouldUpload = !flags["no-upload"];
|
|
61
|
+
const api = shouldUpload ? new ApiClient(flags["api-url"], this.requireApiKey(flags["api-key"])) : null;
|
|
41
62
|
for (const challengeSlug of challengeSlugs) {
|
|
42
63
|
// Validate that the challenge exists locally
|
|
43
64
|
const validation = await Challenge.validateForLocalOperation(challengeSlug, flags["challenges-path"]);
|
|
@@ -47,7 +68,17 @@ export default class Build extends Command {
|
|
|
47
68
|
const challenge = new Challenge(extractShortSlug(challengeSlug), flags["challenges-path"]);
|
|
48
69
|
this.log(pc.blue(`==== Building challenge: ${challenge.shortSlug} ====`));
|
|
49
70
|
try {
|
|
50
|
-
|
|
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
|
+
}
|
|
51
82
|
}
|
|
52
83
|
catch (error) {
|
|
53
84
|
this.error(pc.red(`Build failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
@@ -55,4 +86,11 @@ export default class Build extends Command {
|
|
|
55
86
|
this.log();
|
|
56
87
|
}
|
|
57
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
|
+
}
|
|
58
96
|
}
|
|
@@ -7,6 +7,7 @@ export default class Watch extends Command {
|
|
|
7
7
|
"api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
8
|
"challenges-path": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
9
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
"no-validate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
};
|
|
11
12
|
static args: {
|
|
12
13
|
challengeSlug: import("@oclif/core/interfaces").Arg<string>;
|
|
@@ -8,11 +8,16 @@ import { extractShortSlug, getChallengeSlugArgument } from "../../lib/arguments.
|
|
|
8
8
|
import { Challenge, SOURCE_FOLDER } from "../../lib/challenge.js";
|
|
9
9
|
import { getConfigFlags } from "../../lib/flags.js";
|
|
10
10
|
import { clearScreen, debounced } from "../../lib/utils.js";
|
|
11
|
+
import { formatValidationResult, ValidationService } from "../../lib/validator.js";
|
|
11
12
|
export default class Watch extends Command {
|
|
12
13
|
static description = "Watch a challenge for changes and auto-rebuild/upload";
|
|
13
14
|
static examples = ["<%= config.bin %> <%= command.id %> my-challenge"];
|
|
14
15
|
static flags = {
|
|
15
16
|
verbose: Flags.boolean({ description: "Enable verbose output", default: false }),
|
|
17
|
+
"no-validate": Flags.boolean({
|
|
18
|
+
description: "Skip datapack validation",
|
|
19
|
+
default: false,
|
|
20
|
+
}),
|
|
16
21
|
...getConfigFlags("api-key", "api-url", "challenges-path"),
|
|
17
22
|
};
|
|
18
23
|
static args = {
|
|
@@ -31,8 +36,28 @@ export default class Watch extends Command {
|
|
|
31
36
|
let building = false;
|
|
32
37
|
// Perform the initial build and upload
|
|
33
38
|
this.log(pc.blue(`Performing initial build and upload...`));
|
|
34
|
-
let { config: lastConfig, datapackHash: lastHash } = await challenge.buildAndUpload(api,
|
|
39
|
+
let { config: lastConfig, datapackHash: lastHash } = await challenge.buildAndUpload(api, {
|
|
40
|
+
asPublic: false,
|
|
41
|
+
validate: !flags["no-validate"],
|
|
42
|
+
});
|
|
35
43
|
this.log(pc.green(`✓ Initial build and upload complete`));
|
|
44
|
+
// Create a reusable validation service for watch mode (42ms per validation vs 1.8s)
|
|
45
|
+
// The service is initialized once and reused for all subsequent validations
|
|
46
|
+
let validationService = null;
|
|
47
|
+
if (!flags["no-validate"]) {
|
|
48
|
+
validationService = ValidationService.create(challenge.builtDatapackPath);
|
|
49
|
+
// Wait for initial initialization
|
|
50
|
+
await validationService.waitUntilReady();
|
|
51
|
+
}
|
|
52
|
+
// Clean up validation service on exit
|
|
53
|
+
const cleanup = async () => {
|
|
54
|
+
if (validationService) {
|
|
55
|
+
await validationService.close();
|
|
56
|
+
}
|
|
57
|
+
process.exit(0);
|
|
58
|
+
};
|
|
59
|
+
process.on("SIGINT", cleanup);
|
|
60
|
+
process.on("SIGTERM", cleanup);
|
|
36
61
|
const executeRebuild = async () => {
|
|
37
62
|
building = true;
|
|
38
63
|
// Clear screen before subsequent rebuilds for clean output
|
|
@@ -66,8 +91,29 @@ export default class Watch extends Command {
|
|
|
66
91
|
task.title = "Datapack built";
|
|
67
92
|
},
|
|
68
93
|
},
|
|
94
|
+
{
|
|
95
|
+
title: "Validating datapack",
|
|
96
|
+
skip: () => (flags["no-validate"] || !validationService ? "Validation skipped" : false),
|
|
97
|
+
task: async (ctx, task) => {
|
|
98
|
+
// Use the reusable validation service (42ms vs 1.8s for new service each time)
|
|
99
|
+
const validationResult = await validationService.validate();
|
|
100
|
+
ctx.validationFailed = validationResult.hasErrors;
|
|
101
|
+
ctx.validationOutput = formatValidationResult(validationResult);
|
|
102
|
+
if (validationResult.hasErrors) {
|
|
103
|
+
throw new Error(`${validationResult.errors.length} error${validationResult.errors.length !== 1 ? "s" : ""}`);
|
|
104
|
+
}
|
|
105
|
+
else if (validationResult.warnings.length > 0) {
|
|
106
|
+
task.title = `Validation complete (${validationResult.warnings.length} warning${validationResult.warnings.length !== 1 ? "s" : ""})`;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
task.title = "Validation passed";
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
exitOnError: false,
|
|
113
|
+
},
|
|
69
114
|
{
|
|
70
115
|
title: "Checking datapack changes",
|
|
116
|
+
skip: (ctx) => (ctx.validationFailed ? "Upload skipped due to validation errors" : false),
|
|
71
117
|
task: async (_ctx, task) => {
|
|
72
118
|
const newHash = await challenge.getDatapackHash();
|
|
73
119
|
// Upload if datapack changed OR if config changed (since tarball includes source files)
|
|
@@ -87,8 +133,14 @@ export default class Watch extends Command {
|
|
|
87
133
|
renderer: flags.verbose ? "verbose" : "default",
|
|
88
134
|
});
|
|
89
135
|
try {
|
|
90
|
-
await tasks.run();
|
|
91
|
-
|
|
136
|
+
const ctx = await tasks.run();
|
|
137
|
+
if (ctx.validationFailed) {
|
|
138
|
+
this.log(`\n${ctx.validationOutput}`);
|
|
139
|
+
this.log(pc.red(`\n✗ Upload skipped due to validation errors`));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.log(pc.green(`\n✓ Rebuild complete`));
|
|
143
|
+
}
|
|
92
144
|
}
|
|
93
145
|
catch (error) {
|
|
94
146
|
this.log(pc.red(`\n✗ Error: ${error instanceof Error ? error.message : String(error)}`));
|
package/dist/lib/challenge.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ApiClient } from "./api-client.js";
|
|
2
2
|
import { type ChallengeConfigSchemaType, type ChallengeSchemaType } from "./schemas.js";
|
|
3
|
+
import { type ValidationResult } from "./validator.js";
|
|
3
4
|
export declare const SOURCE_FOLDER = ".src";
|
|
4
5
|
export declare class Challenge {
|
|
5
6
|
readonly shortSlug: string;
|
|
@@ -25,6 +26,16 @@ export declare class Challenge {
|
|
|
25
26
|
* Get the path to the tarball file
|
|
26
27
|
*/
|
|
27
28
|
get tarballPath(): string;
|
|
29
|
+
/**
|
|
30
|
+
* Get the path to the built datapack directory (inside datapackPath)
|
|
31
|
+
*/
|
|
32
|
+
get builtDatapackPath(): string;
|
|
33
|
+
/**
|
|
34
|
+
* Validate the built datapack using Spyglass
|
|
35
|
+
* @returns ValidationResult with categorized issues
|
|
36
|
+
* @throws Error if datapack doesn't exist
|
|
37
|
+
*/
|
|
38
|
+
validate(): Promise<ValidationResult>;
|
|
28
39
|
/**
|
|
29
40
|
* Make sure the challenge exists locally and is valid by ensuring the challenge.ts file and the config.ts file exist.
|
|
30
41
|
*/
|
|
@@ -40,13 +51,30 @@ export declare class Challenge {
|
|
|
40
51
|
*/
|
|
41
52
|
build(silent?: boolean, config?: ChallengeConfigSchemaType): Promise<void>;
|
|
42
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
|
+
}>;
|
|
43
66
|
/**
|
|
44
67
|
* Build the challenge datapack and upload it to the cloud.
|
|
45
68
|
* @param api - The API client to use.
|
|
46
|
-
* @param
|
|
69
|
+
* @param options - Build options
|
|
70
|
+
* @param options.asPublic - Whether the challenge should be uploaded as public.
|
|
71
|
+
* @param options.validate - Whether to validate the datapack before upload (default: true).
|
|
47
72
|
* @returns The config and datapack hash.
|
|
48
73
|
*/
|
|
49
|
-
buildAndUpload(api: ApiClient,
|
|
74
|
+
buildAndUpload(api: ApiClient, options?: {
|
|
75
|
+
asPublic: boolean;
|
|
76
|
+
validate?: boolean;
|
|
77
|
+
}): Promise<{
|
|
50
78
|
config: ChallengeConfigSchemaType;
|
|
51
79
|
datapackHash: string;
|
|
52
80
|
}>;
|
package/dist/lib/challenge.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as tar from "tar";
|
|
|
7
7
|
import { extractShortSlug, isNamespacedSlug } from "./arguments.js";
|
|
8
8
|
import { ChallengeConfigSchema } from "./schemas.js";
|
|
9
9
|
import { executeTypescriptFile, findSimilarStrings, getStaticResourcePath, loadTypescriptExport, readDirSorted, } from "./utils.js";
|
|
10
|
+
import { formatValidationResult, ValidationService, validateDatapack } from "./validator.js";
|
|
10
11
|
import { formatError } from "./zod-errors.js";
|
|
11
12
|
export const SOURCE_FOLDER = ".src";
|
|
12
13
|
export class Challenge {
|
|
@@ -46,6 +47,23 @@ export class Challenge {
|
|
|
46
47
|
get tarballPath() {
|
|
47
48
|
return path.join(this.kradleChallengesPath, `${this.shortSlug}.tar.gz`);
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Get the path to the built datapack directory (inside datapackPath)
|
|
52
|
+
*/
|
|
53
|
+
get builtDatapackPath() {
|
|
54
|
+
return path.join(this.datapackPath, "datapack");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate the built datapack using Spyglass
|
|
58
|
+
* @returns ValidationResult with categorized issues
|
|
59
|
+
* @throws Error if datapack doesn't exist
|
|
60
|
+
*/
|
|
61
|
+
async validate() {
|
|
62
|
+
if (!existsSync(this.builtDatapackPath)) {
|
|
63
|
+
throw new Error(`Datapack not found at ${this.builtDatapackPath}. Run build first.`);
|
|
64
|
+
}
|
|
65
|
+
return validateDatapack(this.builtDatapackPath);
|
|
66
|
+
}
|
|
49
67
|
/**
|
|
50
68
|
* Make sure the challenge exists locally and is valid by ensuring the challenge.ts file and the config.ts file exist.
|
|
51
69
|
*/
|
|
@@ -144,16 +162,58 @@ export class Challenge {
|
|
|
144
162
|
}
|
|
145
163
|
}
|
|
146
164
|
/**
|
|
147
|
-
* Build the challenge datapack
|
|
148
|
-
* @param
|
|
149
|
-
* @param
|
|
150
|
-
* @returns The config and datapack hash.
|
|
165
|
+
* Build the challenge datapack locally.
|
|
166
|
+
* @param options - Build options
|
|
167
|
+
* @param options.validate - Whether to validate the datapack after build (default: true).
|
|
168
|
+
* @returns The loaded config and datapack hash.
|
|
151
169
|
*/
|
|
152
|
-
async
|
|
170
|
+
async buildLocal(options = { validate: true }) {
|
|
171
|
+
const { validate = true } = options;
|
|
172
|
+
// Start validation service initialization in parallel with build (if validation enabled)
|
|
173
|
+
// This overlaps the init time with the build process.
|
|
174
|
+
let validationService = null;
|
|
175
|
+
if (validate) {
|
|
176
|
+
validationService = ValidationService.create(this.builtDatapackPath);
|
|
177
|
+
}
|
|
153
178
|
// Ensure challenge exists locally
|
|
154
179
|
if (!this.exists()) {
|
|
180
|
+
await validationService?.close();
|
|
155
181
|
throw new Error(`Challenge "${this.shortSlug}" does not exist locally. Make sure both the challenge.ts file and the config.ts file exist.`);
|
|
156
182
|
}
|
|
183
|
+
const config = await this.loadConfig();
|
|
184
|
+
// Build datapack (validation service initializes in parallel)
|
|
185
|
+
console.log(pc.blue(`>> Building datapack: ${this.shortSlug}`));
|
|
186
|
+
await this.build(false, config);
|
|
187
|
+
console.log(pc.green(`✓ Datapack built\n`));
|
|
188
|
+
// Validate datapack after building (if enabled)
|
|
189
|
+
if (validate && validationService) {
|
|
190
|
+
console.log(pc.blue(`>> Validating datapack: ${this.shortSlug}`));
|
|
191
|
+
try {
|
|
192
|
+
const validationResult = await validationService.validate();
|
|
193
|
+
console.log(formatValidationResult(validationResult));
|
|
194
|
+
console.log();
|
|
195
|
+
if (validationResult.hasErrors) {
|
|
196
|
+
throw new Error(`Validation failed with ${validationResult.errors.length} error(s). Build aborted.`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await validationService.close();
|
|
201
|
+
}
|
|
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 });
|
|
157
217
|
// Ensure challenge exists in the cloud
|
|
158
218
|
if (!(await api.challengeExists(this.shortSlug))) {
|
|
159
219
|
console.log(pc.yellow(`Challenge not found in cloud: ${this.shortSlug}`));
|
|
@@ -161,7 +221,6 @@ export class Challenge {
|
|
|
161
221
|
await api.createChallenge(this.shortSlug);
|
|
162
222
|
console.log(pc.green(`✓ Challenge created in cloud`));
|
|
163
223
|
}
|
|
164
|
-
const config = await this.loadConfig();
|
|
165
224
|
// Ensure challenge's visibility is set to private - else, temporarily set it to private
|
|
166
225
|
const cloudChallengeVisibility = (await api.getChallenge(this.shortSlug, ["visibility"])).visibility;
|
|
167
226
|
if (cloudChallengeVisibility === "public") {
|
|
@@ -174,18 +233,15 @@ export class Challenge {
|
|
|
174
233
|
// We have to set it to private because we cannot update public challenges
|
|
175
234
|
await api.updateChallenge(this.shortSlug, config, "private");
|
|
176
235
|
console.log(pc.green(`✓ Config uploaded\n`));
|
|
177
|
-
//
|
|
178
|
-
console.log(pc.blue(`>> Building datapack: ${this.shortSlug}`));
|
|
179
|
-
await this.build(false, config);
|
|
236
|
+
// Upload datapack
|
|
180
237
|
console.log(pc.blue(`>> Uploading datapack: ${this.shortSlug}`));
|
|
181
238
|
await api.uploadChallengeDatapack(this.shortSlug, this.tarballPath);
|
|
182
|
-
console.log(pc.green(`✓
|
|
239
|
+
console.log(pc.green(`✓ Datapack uploaded\n`));
|
|
183
240
|
if (asPublic) {
|
|
184
241
|
console.log(pc.green(`👀 Setting challenge visibility to public...`));
|
|
185
242
|
await api.updateChallengeVisibility(this.shortSlug, "public");
|
|
186
243
|
console.log(pc.green(`✓ Challenge visibility set to public\n`));
|
|
187
244
|
}
|
|
188
|
-
const datapackHash = await this.getDatapackHash();
|
|
189
245
|
return { config, datapackHash };
|
|
190
246
|
}
|
|
191
247
|
/**
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ErrorSeverity } from "@spyglassmc/core";
|
|
2
|
+
export interface ValidationError {
|
|
3
|
+
uri: string;
|
|
4
|
+
filePath: string;
|
|
5
|
+
line: number;
|
|
6
|
+
character: number;
|
|
7
|
+
endLine: number;
|
|
8
|
+
endCharacter: number;
|
|
9
|
+
message: string;
|
|
10
|
+
severity: ErrorSeverity;
|
|
11
|
+
}
|
|
12
|
+
export interface ValidationResult {
|
|
13
|
+
errors: ValidationError[];
|
|
14
|
+
warnings: ValidationError[];
|
|
15
|
+
infos: ValidationError[];
|
|
16
|
+
hints: ValidationError[];
|
|
17
|
+
hasErrors: boolean;
|
|
18
|
+
totalIssues: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Format validation errors for CLI output
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatValidationResult(result: ValidationResult): string;
|
|
24
|
+
/**
|
|
25
|
+
* Reusable validation service that keeps Spyglass initialized.
|
|
26
|
+
* Use this for watch mode or when validating multiple times.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ValidationService {
|
|
29
|
+
private service;
|
|
30
|
+
private rootUri;
|
|
31
|
+
private errorMap;
|
|
32
|
+
private readyPromise;
|
|
33
|
+
private constructor();
|
|
34
|
+
private initialize;
|
|
35
|
+
/**
|
|
36
|
+
* Create a new validation service for a datapack.
|
|
37
|
+
* Returns immediately - call waitUntilReady() or validate() to wait for initialization.
|
|
38
|
+
*/
|
|
39
|
+
static create(datapackPath: string): ValidationService;
|
|
40
|
+
/**
|
|
41
|
+
* Wait until the service is fully initialized.
|
|
42
|
+
*/
|
|
43
|
+
waitUntilReady(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Validate all mcfunction files in the datapack.
|
|
46
|
+
* Waits for initialization if not already complete.
|
|
47
|
+
*/
|
|
48
|
+
validate(): Promise<ValidationResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Notify the service that a file has changed (for watch mode).
|
|
51
|
+
*/
|
|
52
|
+
notifyFileChanged(filePath: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Close the service and clean up resources.
|
|
55
|
+
*/
|
|
56
|
+
close(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Validate a Minecraft datapack using Spyglass (simple one-shot API).
|
|
60
|
+
* For repeated validations, use ValidationService instead.
|
|
61
|
+
*/
|
|
62
|
+
export declare function validateDatapack(datapackPath: string): Promise<ValidationResult>;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { ConfigService, ErrorSeverity, Logger, Service, VanillaConfig } from "@spyglassmc/core";
|
|
6
|
+
import { getNodeJsExternals } from "@spyglassmc/core/lib/nodejs.js";
|
|
7
|
+
import { initialize as initializeJavaEdition } from "@spyglassmc/java-edition";
|
|
8
|
+
import { initialize as initializeMcdoc } from "@spyglassmc/mcdoc";
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
/**
|
|
11
|
+
* List of error messages to filter out (Spyglass false positives)
|
|
12
|
+
*/
|
|
13
|
+
const FILTERED_ERROR_MESSAGES = [
|
|
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
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Get a human-readable severity label with color
|
|
20
|
+
*/
|
|
21
|
+
function getSeverityLabel(severity) {
|
|
22
|
+
switch (severity) {
|
|
23
|
+
case ErrorSeverity.Error:
|
|
24
|
+
return pc.red("error");
|
|
25
|
+
case ErrorSeverity.Warning:
|
|
26
|
+
return pc.yellow("warning");
|
|
27
|
+
case ErrorSeverity.Information:
|
|
28
|
+
return pc.blue("info");
|
|
29
|
+
case ErrorSeverity.Hint:
|
|
30
|
+
return pc.dim("hint");
|
|
31
|
+
default:
|
|
32
|
+
return pc.dim("unknown");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read a specific line from a file
|
|
37
|
+
*/
|
|
38
|
+
function getSourceLine(uri, lineNumber) {
|
|
39
|
+
try {
|
|
40
|
+
const filePath = uri.startsWith("file://") ? fileURLToPath(uri) : uri;
|
|
41
|
+
const content = readFileSync(filePath, "utf-8");
|
|
42
|
+
const lines = content.split("\n");
|
|
43
|
+
if (lineNumber >= 0 && lineNumber < lines.length) {
|
|
44
|
+
return lines[lineNumber];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Ignore file read errors
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Format validation errors for CLI output
|
|
54
|
+
*/
|
|
55
|
+
export function formatValidationResult(result) {
|
|
56
|
+
if (result.totalIssues === 0) {
|
|
57
|
+
return pc.green("No validation issues found");
|
|
58
|
+
}
|
|
59
|
+
const lines = [];
|
|
60
|
+
const allIssues = [...result.errors, ...result.warnings, ...result.infos, ...result.hints];
|
|
61
|
+
// Group by file
|
|
62
|
+
const byFile = new Map();
|
|
63
|
+
for (const issue of allIssues) {
|
|
64
|
+
const existing = byFile.get(issue.filePath) || [];
|
|
65
|
+
existing.push(issue);
|
|
66
|
+
byFile.set(issue.filePath, existing);
|
|
67
|
+
}
|
|
68
|
+
// Sort files and output
|
|
69
|
+
const sortedFiles = [...byFile.keys()].sort();
|
|
70
|
+
for (const filePath of sortedFiles) {
|
|
71
|
+
const fileIssues = byFile.get(filePath);
|
|
72
|
+
if (!fileIssues)
|
|
73
|
+
continue;
|
|
74
|
+
// Sort by line, then character
|
|
75
|
+
fileIssues.sort((a, b) => a.line - b.line || a.character - b.character);
|
|
76
|
+
for (const issue of fileIssues) {
|
|
77
|
+
// Line numbers are 0-indexed from Spyglass, display as 1-indexed
|
|
78
|
+
const location = pc.cyan(`${filePath}:${issue.line + 1}:${issue.character + 1}`);
|
|
79
|
+
const severity = getSeverityLabel(issue.severity);
|
|
80
|
+
lines.push(`${location} ${severity}: ${issue.message}`);
|
|
81
|
+
// Show source line with caret marker
|
|
82
|
+
const sourceLine = getSourceLine(issue.uri, issue.line);
|
|
83
|
+
if (sourceLine !== null) {
|
|
84
|
+
lines.push(pc.dim(` ${sourceLine}`));
|
|
85
|
+
// Add caret marker pointing to the error column
|
|
86
|
+
const caretPadding = " ".repeat(issue.character + 2); // +2 for the " " prefix
|
|
87
|
+
lines.push(pc.red(`${caretPadding}^`));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Summary
|
|
92
|
+
const summary = [];
|
|
93
|
+
if (result.errors.length > 0) {
|
|
94
|
+
summary.push(pc.red(`${result.errors.length} error${result.errors.length !== 1 ? "s" : ""}`));
|
|
95
|
+
}
|
|
96
|
+
if (result.warnings.length > 0) {
|
|
97
|
+
summary.push(pc.yellow(`${result.warnings.length} warning${result.warnings.length !== 1 ? "s" : ""}`));
|
|
98
|
+
}
|
|
99
|
+
if (result.infos.length > 0) {
|
|
100
|
+
summary.push(pc.blue(`${result.infos.length} info`));
|
|
101
|
+
}
|
|
102
|
+
if (result.hints.length > 0) {
|
|
103
|
+
summary.push(pc.dim(`${result.hints.length} hint${result.hints.length !== 1 ? "s" : ""}`));
|
|
104
|
+
}
|
|
105
|
+
lines.push("");
|
|
106
|
+
lines.push(`Found ${summary.join(", ")}`);
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Reusable validation service that keeps Spyglass initialized.
|
|
111
|
+
* Use this for watch mode or when validating multiple times.
|
|
112
|
+
*/
|
|
113
|
+
export class ValidationService {
|
|
114
|
+
service;
|
|
115
|
+
rootUri;
|
|
116
|
+
errorMap = new Map();
|
|
117
|
+
readyPromise;
|
|
118
|
+
constructor(service, rootUri) {
|
|
119
|
+
this.service = service;
|
|
120
|
+
this.rootUri = rootUri;
|
|
121
|
+
// Listen for document errors
|
|
122
|
+
this.service.project.on("documentErrored", (event) => {
|
|
123
|
+
const fileUri = event.uri;
|
|
124
|
+
// Skip archive files (vanilla data)
|
|
125
|
+
if (fileUri.startsWith("archive://")) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Convert URI back to relative path
|
|
129
|
+
const filePath = fileUri.startsWith(this.rootUri)
|
|
130
|
+
? fileUri.slice(this.rootUri.length)
|
|
131
|
+
: fileUri.replace("file://", "");
|
|
132
|
+
for (const error of event.errors) {
|
|
133
|
+
const key = `${fileUri}:${error.posRange.start.line}:${error.posRange.start.character}:${error.message}`;
|
|
134
|
+
if (!this.errorMap.has(key)) {
|
|
135
|
+
this.errorMap.set(key, {
|
|
136
|
+
uri: fileUri,
|
|
137
|
+
filePath,
|
|
138
|
+
line: error.posRange.start.line,
|
|
139
|
+
character: error.posRange.start.character,
|
|
140
|
+
endLine: error.posRange.end.line,
|
|
141
|
+
endCharacter: error.posRange.end.character,
|
|
142
|
+
message: error.message,
|
|
143
|
+
severity: error.severity,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
// Start initialization immediately
|
|
149
|
+
this.readyPromise = this.initialize();
|
|
150
|
+
}
|
|
151
|
+
async initialize() {
|
|
152
|
+
await this.service.project.init();
|
|
153
|
+
await this.service.project.ready();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Create a new validation service for a datapack.
|
|
157
|
+
* Returns immediately - call waitUntilReady() or validate() to wait for initialization.
|
|
158
|
+
*/
|
|
159
|
+
static create(datapackPath) {
|
|
160
|
+
// Convert to file URI format required by Spyglass (must end with /)
|
|
161
|
+
let rootUriStr = pathToFileURL(datapackPath).href;
|
|
162
|
+
if (!rootUriStr.endsWith("/")) {
|
|
163
|
+
rootUriStr += "/";
|
|
164
|
+
}
|
|
165
|
+
const rootUri = rootUriStr;
|
|
166
|
+
// Cache directory for Spyglass - use temp directory (no persistent cache for now)
|
|
167
|
+
const cacheDir = path.join(os.tmpdir(), "kradle-spyglass-cache");
|
|
168
|
+
const cacheRoot = `${pathToFileURL(cacheDir).href}/`;
|
|
169
|
+
const service = new Service({
|
|
170
|
+
isDebugging: false,
|
|
171
|
+
logger: Logger.noop(),
|
|
172
|
+
project: {
|
|
173
|
+
defaultConfig: ConfigService.merge(VanillaConfig, {
|
|
174
|
+
env: {
|
|
175
|
+
// Only load @vanilla-mcdoc - sufficient for mcfunction syntax validation
|
|
176
|
+
// Skips @vanilla-datapack (6835 files) and @vanilla-resourcepack (8473 files)
|
|
177
|
+
dependencies: ["@vanilla-mcdoc"],
|
|
178
|
+
// Disable features we don't need for CLI validation
|
|
179
|
+
feature: {
|
|
180
|
+
codeActions: false,
|
|
181
|
+
colors: false,
|
|
182
|
+
completions: false,
|
|
183
|
+
documentHighlighting: false,
|
|
184
|
+
documentLinks: false,
|
|
185
|
+
foldingRanges: false,
|
|
186
|
+
formatting: false,
|
|
187
|
+
hover: false,
|
|
188
|
+
inlayHint: false,
|
|
189
|
+
semanticColoring: false,
|
|
190
|
+
selectionRanges: false,
|
|
191
|
+
signatures: false,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
cacheRoot,
|
|
196
|
+
externals: getNodeJsExternals({ cacheRoot }),
|
|
197
|
+
// IMPORTANT: mcdoc.initialize must come BEFORE je.initialize
|
|
198
|
+
initializers: [initializeMcdoc, initializeJavaEdition],
|
|
199
|
+
projectRoots: [rootUri],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return new ValidationService(service, rootUri);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Wait until the service is fully initialized.
|
|
206
|
+
*/
|
|
207
|
+
async waitUntilReady() {
|
|
208
|
+
await this.readyPromise;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Validate all mcfunction files in the datapack.
|
|
212
|
+
* Waits for initialization if not already complete.
|
|
213
|
+
*/
|
|
214
|
+
async validate() {
|
|
215
|
+
await this.readyPromise;
|
|
216
|
+
// Clear previous errors
|
|
217
|
+
this.errorMap.clear();
|
|
218
|
+
// Get all tracked mcfunction files in the project (not from archives)
|
|
219
|
+
const trackedFiles = this.service.project
|
|
220
|
+
.getTrackedFiles()
|
|
221
|
+
.filter((uri) => uri.startsWith(this.rootUri) && uri.endsWith(".mcfunction"));
|
|
222
|
+
// For full JSON text component validation, we need to explicitly open and check each file
|
|
223
|
+
for (const fileUri of trackedFiles) {
|
|
224
|
+
try {
|
|
225
|
+
const contentBytes = await this.service.project.externals.fs.readFile(fileUri);
|
|
226
|
+
if (contentBytes !== undefined) {
|
|
227
|
+
const content = new TextDecoder().decode(contentBytes);
|
|
228
|
+
await this.service.project.onDidOpen(fileUri, "mcfunction", 1, content);
|
|
229
|
+
await this.service.project.ensureClientManagedChecked(fileUri);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Ignore file read errors
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Get all collected errors - only keep actual errors (not warnings) and filter out false positives
|
|
237
|
+
const errors = Array.from(this.errorMap.values()).filter((e) => e.severity === ErrorSeverity.Error && !FILTERED_ERROR_MESSAGES.includes(e.message));
|
|
238
|
+
return {
|
|
239
|
+
errors,
|
|
240
|
+
warnings: [],
|
|
241
|
+
infos: [],
|
|
242
|
+
hints: [],
|
|
243
|
+
hasErrors: errors.length > 0,
|
|
244
|
+
totalIssues: errors.length,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Notify the service that a file has changed (for watch mode).
|
|
249
|
+
*/
|
|
250
|
+
async notifyFileChanged(filePath) {
|
|
251
|
+
await this.readyPromise;
|
|
252
|
+
const fileUri = pathToFileURL(filePath).href;
|
|
253
|
+
try {
|
|
254
|
+
const contentBytes = await this.service.project.externals.fs.readFile(fileUri);
|
|
255
|
+
if (contentBytes !== undefined) {
|
|
256
|
+
const content = new TextDecoder().decode(contentBytes);
|
|
257
|
+
// Increment version to signal change
|
|
258
|
+
await this.service.project.onDidChange(fileUri, [{ text: content }], 2);
|
|
259
|
+
await this.service.project.ensureClientManagedChecked(fileUri);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Ignore file read errors
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Close the service and clean up resources.
|
|
268
|
+
*/
|
|
269
|
+
async close() {
|
|
270
|
+
await this.service.project.close();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Validate a Minecraft datapack using Spyglass (simple one-shot API).
|
|
275
|
+
* For repeated validations, use ValidationService instead.
|
|
276
|
+
*/
|
|
277
|
+
export async function validateDatapack(datapackPath) {
|
|
278
|
+
const service = ValidationService.create(datapackPath);
|
|
279
|
+
try {
|
|
280
|
+
return await service.validate();
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
await service.close();
|
|
284
|
+
}
|
|
285
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -215,12 +215,12 @@
|
|
|
215
215
|
"aliases": [],
|
|
216
216
|
"args": {
|
|
217
217
|
"challengeSlug": {
|
|
218
|
-
"description": "Challenge slug to build
|
|
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
|
|
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",
|
|
@@ -241,25 +241,35 @@
|
|
|
241
241
|
"allowNo": false,
|
|
242
242
|
"type": "boolean"
|
|
243
243
|
},
|
|
244
|
-
"
|
|
245
|
-
"description": "
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"
|
|
244
|
+
"no-validate": {
|
|
245
|
+
"description": "Skip datapack validation",
|
|
246
|
+
"name": "no-validate",
|
|
247
|
+
"allowNo": false,
|
|
248
|
+
"type": "boolean"
|
|
249
|
+
},
|
|
250
|
+
"no-upload": {
|
|
251
|
+
"description": "Build datapack locally only (skip cloud config/datapack upload)",
|
|
252
|
+
"name": "no-upload",
|
|
253
|
+
"allowNo": false,
|
|
254
|
+
"type": "boolean"
|
|
252
255
|
},
|
|
253
256
|
"api-url": {
|
|
254
257
|
"description": "Kradle Web API URL",
|
|
255
258
|
"env": "KRADLE_API_URL",
|
|
256
259
|
"name": "api-url",
|
|
257
|
-
"required": true,
|
|
258
260
|
"default": "https://api.kradle.ai/v0",
|
|
259
261
|
"hasDynamicHelp": false,
|
|
260
262
|
"multiple": false,
|
|
261
263
|
"type": "option"
|
|
262
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
|
+
},
|
|
263
273
|
"challenges-path": {
|
|
264
274
|
"description": "Absolute path to the challenges directory",
|
|
265
275
|
"env": "KRADLE_CHALLENGES_PATH",
|
|
@@ -696,6 +706,12 @@
|
|
|
696
706
|
"allowNo": false,
|
|
697
707
|
"type": "boolean"
|
|
698
708
|
},
|
|
709
|
+
"no-validate": {
|
|
710
|
+
"description": "Skip datapack validation",
|
|
711
|
+
"name": "no-validate",
|
|
712
|
+
"allowNo": false,
|
|
713
|
+
"type": "boolean"
|
|
714
|
+
},
|
|
699
715
|
"api-key": {
|
|
700
716
|
"description": "Kradle API key",
|
|
701
717
|
"env": "KRADLE_API_KEY",
|
|
@@ -1512,5 +1528,5 @@
|
|
|
1512
1528
|
]
|
|
1513
1529
|
}
|
|
1514
1530
|
},
|
|
1515
|
-
"version": "0.6.
|
|
1531
|
+
"version": "0.6.7"
|
|
1516
1532
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kradle",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "Kradle's CLI. Manage challenges, experiments, agents and more!",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli"
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
"@oclif/core": "^4.8.0",
|
|
38
38
|
"@oclif/plugin-autocomplete": "^3.2.39",
|
|
39
39
|
"@oclif/plugin-help": "^6.2.35",
|
|
40
|
+
"@spyglassmc/core": "^0.4.43",
|
|
41
|
+
"@spyglassmc/java-edition": "^0.3.55",
|
|
42
|
+
"@spyglassmc/mcfunction": "^0.2.46",
|
|
40
43
|
"chokidar": "^4.0.3",
|
|
41
44
|
"dotenv": "^17.2.3",
|
|
42
45
|
"enquirer": "^2.4.1",
|
|
@@ -119,14 +119,16 @@ kradle challenge create my-first-challenge
|
|
|
119
119
|
|
|
120
120
|
### `kradle challenge build <name>`
|
|
121
121
|
|
|
122
|
-
Builds a challenge datapack and uploads
|
|
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> --
|
|
127
|
+
kradle challenge build <challenge-name> --no-upload
|
|
128
|
+
kradle challenge build <challenge-name> --public
|
|
129
|
+
kradle challenge build <challenge-name> --no-validate
|
|
128
130
|
kradle challenge build --all
|
|
129
|
-
kradle challenge build --all --
|
|
131
|
+
kradle challenge build --all --public
|
|
130
132
|
```
|
|
131
133
|
|
|
132
134
|
**Arguments:**
|
|
@@ -135,28 +137,53 @@ kradle challenge build --all --visibility public
|
|
|
135
137
|
| `challenge-name` | Challenge to build | Yes (unless `--all`) |
|
|
136
138
|
|
|
137
139
|
**Flags:**
|
|
138
|
-
| Flag | Description | Default |
|
|
139
|
-
|
|
140
|
-
| `--all` | Build all local challenges | false |
|
|
141
|
-
| `--
|
|
140
|
+
| Flag | Short | Description | Default |
|
|
141
|
+
|------|-------|-------------|---------|
|
|
142
|
+
| `--all` | `-a` | Build all local challenges | false |
|
|
143
|
+
| `--public` | `-p` | Set visibility to public after 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`.
|
|
142
148
|
|
|
143
149
|
**What it does:**
|
|
144
|
-
1.
|
|
145
|
-
2. Uploads `config.ts` metadata to cloud
|
|
146
|
-
3. Executes `challenge.ts` to generate the datapack
|
|
150
|
+
1. Executes `challenge.ts` to generate the datapack
|
|
147
151
|
- Passes `KRADLE_CHALLENGE_END_STATES` env var containing a JSON array of end state keys from `config.endStates` (e.g., `["victory", "defeat"]`)
|
|
148
152
|
- Passes `KRADLE_CHALLENGE_ROLES` env var containing a JSON array of role names from `config.roles` (e.g., `["attacker", "defender"]`)
|
|
149
153
|
- Passes `KRADLE_CHALLENGE_LOCATIONS` env var containing a JSON array of location keys from `config.challengeConfig.locations` (e.g., `["spawn", "goal"]`)
|
|
150
154
|
- The `@kradle/challenges-sdk` uses these to register valid end states, roles, and locations at build time
|
|
151
|
-
|
|
155
|
+
2. Validates the datapack using Spyglass engine (unless `--no-validate`)
|
|
156
|
+
- Checks `.mcfunction` files for syntax errors, invalid commands, JSON text components
|
|
157
|
+
- **Errors block command completion** - fix them before the build can finish
|
|
158
|
+
- Auto-detects Minecraft version from `pack.mcmeta` `pack_format`
|
|
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
|
|
162
|
+
|
|
163
|
+
**Validation Output:**
|
|
164
|
+
```
|
|
165
|
+
>> Validating datapack: my-challenge
|
|
166
|
+
data/kradle/functions/tick.mcfunction:1:23 error: Unknown function 'kradle:undefined'
|
|
167
|
+
data/kradle/functions/init.mcfunction:5:1 warning: Unused objective
|
|
168
|
+
|
|
169
|
+
Found 1 error, 1 warning
|
|
170
|
+
|
|
171
|
+
Error: Validation failed with 1 error(s). Build aborted.
|
|
172
|
+
```
|
|
152
173
|
|
|
153
174
|
**Examples:**
|
|
154
175
|
```bash
|
|
155
|
-
# Build single challenge
|
|
176
|
+
# Build single challenge (with validation)
|
|
156
177
|
kradle challenge build my-challenge
|
|
157
178
|
|
|
179
|
+
# Build local datapack only (no cloud upload)
|
|
180
|
+
kradle challenge build my-challenge --no-upload
|
|
181
|
+
|
|
158
182
|
# Build and make public
|
|
159
|
-
kradle challenge build my-challenge --
|
|
183
|
+
kradle challenge build my-challenge --public
|
|
184
|
+
|
|
185
|
+
# Build without validation (for quick iteration)
|
|
186
|
+
kradle challenge build my-challenge --no-validate
|
|
160
187
|
|
|
161
188
|
# Build all challenges
|
|
162
189
|
kradle challenge build --all
|
|
@@ -289,6 +316,8 @@ Watches a challenge for file changes and automatically rebuilds/uploads.
|
|
|
289
316
|
**Usage:**
|
|
290
317
|
```bash
|
|
291
318
|
kradle challenge watch <challenge-name>
|
|
319
|
+
kradle challenge watch <challenge-name> --no-validate
|
|
320
|
+
kradle challenge watch <challenge-name> --verbose
|
|
292
321
|
```
|
|
293
322
|
|
|
294
323
|
**Arguments:**
|
|
@@ -296,17 +325,29 @@ kradle challenge watch <challenge-name>
|
|
|
296
325
|
|----------|-------------|----------|
|
|
297
326
|
| `challenge-name` | Challenge to watch | Yes |
|
|
298
327
|
|
|
328
|
+
**Flags:**
|
|
329
|
+
| Flag | Description | Default |
|
|
330
|
+
|------|-------------|---------|
|
|
331
|
+
| `--no-validate` | Skip datapack validation | false |
|
|
332
|
+
| `--verbose` | Enable verbose output | false |
|
|
333
|
+
|
|
299
334
|
**Behavior:**
|
|
300
335
|
- Monitors `challenges/<name>/` directory for changes
|
|
301
|
-
- Debounces rebuilds (
|
|
302
|
-
-
|
|
336
|
+
- Debounces rebuilds (1 second delay)
|
|
337
|
+
- Validates datapack after each build (unless `--no-validate`)
|
|
338
|
+
- In watch mode, validation errors are shown but **don't block upload** (allows iteration)
|
|
339
|
+
- Warnings and hints are displayed for awareness
|
|
340
|
+
- Only uploads if datapack hash changed or config changed
|
|
303
341
|
- Press Ctrl+C to stop watching
|
|
304
342
|
|
|
305
343
|
**Example:**
|
|
306
344
|
```bash
|
|
307
345
|
kradle challenge watch my-challenge
|
|
308
346
|
# Now edit challenges/my-challenge/challenge.ts
|
|
309
|
-
# Changes will automatically rebuild and upload
|
|
347
|
+
# Changes will automatically rebuild, validate, and upload
|
|
348
|
+
|
|
349
|
+
# Skip validation for faster iteration
|
|
350
|
+
kradle challenge watch my-challenge --no-validate
|
|
310
351
|
```
|
|
311
352
|
|
|
312
353
|
---
|
|
@@ -1204,10 +1245,10 @@ kradle experiment recordings agent-benchmark --all
|
|
|
1204
1245
|
|
|
1205
1246
|
```bash
|
|
1206
1247
|
# Build with public visibility
|
|
1207
|
-
kradle challenge build my-challenge --
|
|
1248
|
+
kradle challenge build my-challenge --public
|
|
1208
1249
|
|
|
1209
1250
|
# Or build all challenges as public
|
|
1210
|
-
kradle challenge build --all --
|
|
1251
|
+
kradle challenge build --all --public
|
|
1211
1252
|
```
|
|
1212
1253
|
|
|
1213
1254
|
---
|
|
@@ -1240,7 +1281,7 @@ kradle challenge build --all --visibility public
|
|
|
1240
1281
|
|---------|-------------|
|
|
1241
1282
|
| `kradle init` | Initialize new project |
|
|
1242
1283
|
| `kradle challenge create <name>` | Create new challenge |
|
|
1243
|
-
| `kradle challenge build <name>` | Build
|
|
1284
|
+
| `kradle challenge build <name>` | Build challenge (uploads by default) |
|
|
1244
1285
|
| `kradle challenge build --all` | Build all challenges |
|
|
1245
1286
|
| `kradle challenge delete <name>` | Delete challenge |
|
|
1246
1287
|
| `kradle challenge list` | List all challenges |
|