@kradle/cli 0.2.5 → 0.2.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.
@@ -100,7 +100,7 @@ export default class Pull extends Command {
100
100
  const tempTarballPath = path.join(flags["challenges-path"], `${challenge.shortSlug}-pull-temp.tar.gz`);
101
101
  const tasks = new Listr([
102
102
  {
103
- title: "Downloading challenge tarball",
103
+ title: "Downloading challenge",
104
104
  task: async (_, task) => {
105
105
  const { downloadUrl } = await api.getChallengeDownloadUrl(challengeSlug);
106
106
  const response = await fetch(downloadUrl);
@@ -110,7 +110,7 @@ export default class Pull extends Command {
110
110
  const buffer = await response.arrayBuffer();
111
111
  await fs.mkdir(path.dirname(tempTarballPath), { recursive: true });
112
112
  await fs.writeFile(tempTarballPath, Buffer.from(buffer));
113
- task.title = "Downloaded challenge tarball";
113
+ task.title = "Downloaded challenge";
114
114
  },
115
115
  },
116
116
  {
@@ -0,0 +1,14 @@
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
+ worldSlug: import("@oclif/core/interfaces").Arg<string>;
7
+ };
8
+ static flags: {
9
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,80 @@
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 { getWorldSlugArgument } from "../../lib/arguments.js";
7
+ import { getConfigFlags } from "../../lib/flags.js";
8
+ import { World } from "../../lib/world.js";
9
+ export default class Delete extends Command {
10
+ static description = "Delete a world locally, from the cloud, or both";
11
+ static examples = [
12
+ "<%= config.bin %> <%= command.id %> my-world",
13
+ "<%= config.bin %> <%= command.id %> my-world --yes",
14
+ ];
15
+ static args = {
16
+ worldSlug: getWorldSlugArgument({ description: "World slug to delete" }),
17
+ };
18
+ static flags = {
19
+ yes: Flags.boolean({ char: "y", description: "Skip confirmation prompts", default: false }),
20
+ ...getConfigFlags("api-key", "api-url"),
21
+ };
22
+ async run() {
23
+ const { args, flags } = await this.parse(Delete);
24
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
25
+ const world = new World(args.worldSlug);
26
+ const existsLocally = world.exists();
27
+ const existsInCloud = await api.worldExists(args.worldSlug);
28
+ if (!existsLocally && !existsInCloud) {
29
+ this.error(pc.red(`World "${args.worldSlug}" does not exist locally or in the cloud.`));
30
+ }
31
+ this.log(pc.bold(`\nWorld: ${pc.cyan(args.worldSlug)}`));
32
+ if (existsLocally) {
33
+ this.log(` 📁 Local: ${pc.green("exists")} at ${world.worldDir}`);
34
+ }
35
+ else {
36
+ this.log(` 📁 Local: ${pc.dim("not found")}`);
37
+ }
38
+ if (existsInCloud) {
39
+ this.log(` ☁ Cloud: ${pc.green("exists")}`);
40
+ }
41
+ else {
42
+ this.log(` ☁ Cloud: ${pc.dim("not found")}`);
43
+ }
44
+ this.log("");
45
+ let deleteLocal = existsLocally;
46
+ let deleteCloud = existsInCloud;
47
+ if (!flags.yes) {
48
+ if (existsLocally) {
49
+ const response = await enquirer.prompt({
50
+ type: "confirm",
51
+ name: "confirm",
52
+ message: `Delete local world files? ${pc.red("This cannot be undone.")}`,
53
+ initial: true,
54
+ });
55
+ deleteLocal = response.confirm;
56
+ }
57
+ if (existsInCloud) {
58
+ const response = await enquirer.prompt({
59
+ type: "confirm",
60
+ name: "confirm",
61
+ message: `Delete world from cloud? ${pc.red("This cannot be undone.")}`,
62
+ initial: true,
63
+ });
64
+ deleteCloud = response.confirm;
65
+ }
66
+ }
67
+ if (!deleteLocal && !deleteCloud) {
68
+ this.log(pc.yellow("✗ Nothing deleted"));
69
+ return;
70
+ }
71
+ if (deleteLocal) {
72
+ await fs.rm(world.worldDir, { recursive: true, force: true });
73
+ this.log(pc.green(`✓ Deleted local: ${world.worldDir}`));
74
+ }
75
+ if (deleteCloud) {
76
+ await api.deleteWorld(args.worldSlug);
77
+ this.log(pc.green(`✓ Deleted from cloud: ${args.worldSlug}`));
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Import extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ path: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ as: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,122 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { Args, Command, Flags } from "@oclif/core";
4
+ import enquirer from "enquirer";
5
+ import { Listr } from "listr2";
6
+ import pc from "picocolors";
7
+ import { ApiClient } from "../../lib/api-client.js";
8
+ import { getConfigFlags } from "../../lib/flags.js";
9
+ import { World } from "../../lib/world.js";
10
+ export default class Import extends Command {
11
+ static description = "Import a Minecraft world folder and package it as a tarball";
12
+ static examples = [
13
+ "<%= config.bin %> <%= command.id %> ~/minecraft/saves/MyWorld",
14
+ "<%= config.bin %> <%= command.id %> ~/minecraft/saves/MyWorld --as my-world",
15
+ ];
16
+ static args = {
17
+ path: Args.string({
18
+ description: "Path to the Minecraft world folder",
19
+ required: true,
20
+ }),
21
+ };
22
+ static flags = {
23
+ as: Flags.string({
24
+ description: "Slug for the world (defaults to folder name converted to slug)",
25
+ }),
26
+ ...getConfigFlags("api-key", "api-url"),
27
+ };
28
+ async run() {
29
+ const { args, flags } = await this.parse(Import);
30
+ const sourcePath = path.resolve(args.path);
31
+ if (!existsSync(sourcePath)) {
32
+ this.error(pc.red(`Source path does not exist: ${sourcePath}`));
33
+ }
34
+ const levelDatPath = path.join(sourcePath, "level.dat");
35
+ if (!existsSync(levelDatPath)) {
36
+ this.error(pc.red(`Invalid Minecraft world: level.dat not found in ${sourcePath}`));
37
+ }
38
+ let slug = flags.as;
39
+ if (!slug) {
40
+ const folderName = path.basename(sourcePath);
41
+ const suggestedSlug = World.toSlug(folderName);
42
+ const response = await enquirer.prompt({
43
+ type: "input",
44
+ name: "slug",
45
+ message: "World slug:",
46
+ initial: suggestedSlug,
47
+ validate: (input) => {
48
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input)) {
49
+ return "Slug must be lowercase alphanumeric with hyphens, not starting/ending with hyphen";
50
+ }
51
+ return true;
52
+ },
53
+ });
54
+ slug = response.slug;
55
+ }
56
+ const world = new World(slug);
57
+ const isReimport = world.exists();
58
+ if (isReimport) {
59
+ this.log(`World "${slug}" already exists. Re-import will update the tarball but preserve config.ts.`);
60
+ }
61
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
62
+ const tasks = new Listr([
63
+ {
64
+ title: "Creating world directory",
65
+ enabled: () => !isReimport,
66
+ task: async (_ctx, task) => {
67
+ const name = path.basename(sourcePath);
68
+ const config = {
69
+ name: name,
70
+ description: "no description",
71
+ visibility: "private",
72
+ domain: "minecraft",
73
+ };
74
+ await World.createLocal(slug, config);
75
+ task.title = `Created worlds/${slug}/`;
76
+ },
77
+ },
78
+ {
79
+ title: isReimport ? "Updating tarball" : "Packaging world as tarball",
80
+ task: async (_ctx, task) => {
81
+ await World.createTarballFrom(sourcePath, slug);
82
+ task.title = isReimport ? `Updated worlds/${slug}/world.tar.gz` : `Created worlds/${slug}/world.tar.gz`;
83
+ },
84
+ },
85
+ {
86
+ title: "Creating world in cloud",
87
+ enabled: () => !isReimport,
88
+ task: async (_ctx, task) => {
89
+ const exists = await api.worldExists(slug);
90
+ if (exists) {
91
+ task.title = "World already exists in cloud";
92
+ return;
93
+ }
94
+ const config = {
95
+ name: slug,
96
+ visibility: "private",
97
+ domain: "minecraft",
98
+ };
99
+ await api.createWorld(slug, config);
100
+ task.title = "Created world in cloud";
101
+ },
102
+ },
103
+ {
104
+ title: "Uploading world to cloud",
105
+ task: async (_ctx, task) => {
106
+ await api.uploadWorldFile(slug, world.tarballPath);
107
+ task.title = "Uploaded world to cloud";
108
+ },
109
+ },
110
+ ]);
111
+ await tasks.run();
112
+ if (isReimport) {
113
+ this.log(pc.green(`\n✓ World re-imported & uploaded: ${slug}`));
114
+ this.log(pc.dim(` → world.tar.gz updated (config.ts preserved)`));
115
+ }
116
+ else {
117
+ this.log(pc.green(`\n✓ World imported & uploaded: ${slug}`));
118
+ this.log(pc.dim(` → config.ts: ${world.configPath}`));
119
+ this.log(pc.dim(` → world.tar.gz: ${world.tarballPath}`));
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class List extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,46 @@
1
+ import { Command } from "@oclif/core";
2
+ import pc from "picocolors";
3
+ import { ApiClient } from "../../lib/api-client.js";
4
+ import { getConfigFlags } from "../../lib/flags.js";
5
+ import { World } from "../../lib/world.js";
6
+ export default class List extends Command {
7
+ static description = "List all worlds (local and cloud)";
8
+ static examples = ["<%= config.bin %> <%= command.id %>"];
9
+ static flags = {
10
+ ...getConfigFlags("api-key", "api-url"),
11
+ };
12
+ async run() {
13
+ const { flags } = await this.parse(List);
14
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
15
+ this.log(pc.blue(">> Loading worlds..."));
16
+ const [cloudWorlds, localWorlds, human] = await Promise.all([
17
+ api.listWorlds(),
18
+ World.getLocalWorlds(),
19
+ api.getHuman(),
20
+ ]);
21
+ const cloudMap = new Map(cloudWorlds.map((w) => [w.slug, w]));
22
+ const allSlugs = new Set([...cloudMap.keys(), ...localWorlds.map((slug) => `${human.username}:${slug}`)]);
23
+ this.log(pc.bold("\nWorlds:\n"));
24
+ this.log(`${"Status".padEnd(15)} ${"Slug".padEnd(40)} ${"Name".padEnd(30)}`);
25
+ this.log("-".repeat(90));
26
+ for (const slug of Array.from(allSlugs).sort()) {
27
+ const inCloud = cloudMap.has(slug);
28
+ const shortSlug = slug.includes(":") ? slug.split(":")[1] : slug;
29
+ const inLocal = localWorlds.includes(shortSlug);
30
+ let status;
31
+ if (inCloud && inLocal) {
32
+ status = pc.green("✓ synced");
33
+ }
34
+ else if (inCloud) {
35
+ status = pc.yellow("☁ cloud only");
36
+ }
37
+ else {
38
+ status = pc.blue("⊡ local only");
39
+ }
40
+ const cloudWorld = cloudMap.get(slug);
41
+ const name = cloudWorld?.name || "-";
42
+ this.log(`${status.padEnd(24)} ${slug.padEnd(40)} ${name.padEnd(30)}`);
43
+ }
44
+ this.log(pc.dim(`\nTotal: ${allSlugs.size} worlds`));
45
+ }
46
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Pull extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ worldSlug: import("@oclif/core/interfaces").Arg<string | undefined>;
7
+ };
8
+ static flags: {
9
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,135 @@
1
+ import { existsSync } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import { Command, Flags } from "@oclif/core";
4
+ import enquirer from "enquirer";
5
+ import { Listr } from "listr2";
6
+ import pc from "picocolors";
7
+ import { ApiClient } from "../../lib/api-client.js";
8
+ import { extractShortSlug, getWorldSlugArgument } from "../../lib/arguments.js";
9
+ import { getConfigFlags } from "../../lib/flags.js";
10
+ import { World } from "../../lib/world.js";
11
+ export default class Pull extends Command {
12
+ static description = "Download a world from the cloud";
13
+ static examples = [
14
+ "<%= config.bin %> <%= command.id %>",
15
+ "<%= config.bin %> <%= command.id %> my-world",
16
+ "<%= config.bin %> <%= command.id %> username:my-world",
17
+ "<%= config.bin %> <%= command.id %> my-world --yes",
18
+ ];
19
+ static args = {
20
+ worldSlug: getWorldSlugArgument({
21
+ description: "World slug to pull (interactive selection if omitted)",
22
+ required: false,
23
+ allowTeam: true,
24
+ }),
25
+ };
26
+ static flags = {
27
+ yes: Flags.boolean({ char: "y", description: "Skip confirmation prompts", default: false }),
28
+ ...getConfigFlags("api-key", "api-url"),
29
+ };
30
+ async run() {
31
+ const { args, flags } = await this.parse(Pull);
32
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
33
+ let worldSlug = args.worldSlug;
34
+ if (!worldSlug) {
35
+ const [kradleWorlds, cloudWorlds, localWorlds] = await Promise.all([
36
+ api.listKradleWorlds(),
37
+ api.listWorlds(),
38
+ World.getLocalWorlds(),
39
+ ]);
40
+ const allWorlds = [...kradleWorlds, ...cloudWorlds];
41
+ if (allWorlds.length === 0) {
42
+ this.error(pc.red("No worlds found in the cloud."));
43
+ }
44
+ const localSet = new Set(localWorlds);
45
+ const choices = allWorlds
46
+ .map((w) => w.slug)
47
+ .toSorted()
48
+ .map((slug) => {
49
+ const shortSlug = extractShortSlug(slug);
50
+ const isLocal = localSet.has(shortSlug);
51
+ const status = isLocal ? pc.yellow(" (local)") : "";
52
+ return {
53
+ name: slug,
54
+ message: `${slug}${status}`,
55
+ };
56
+ });
57
+ const response = await enquirer.prompt({
58
+ type: "select",
59
+ name: "world",
60
+ message: "Select a world to pull",
61
+ choices,
62
+ });
63
+ worldSlug = response.world;
64
+ }
65
+ const shortSlug = extractShortSlug(worldSlug);
66
+ const world = new World(shortSlug);
67
+ const existsInCloud = await api.worldExists(worldSlug);
68
+ if (!existsInCloud) {
69
+ this.error(pc.red(`World "${worldSlug}" does not exist in the cloud.`));
70
+ }
71
+ const existsLocally = world.exists();
72
+ if (existsLocally && !flags.yes) {
73
+ this.log(pc.bold(`\nWorld: ${pc.cyan(worldSlug)}`));
74
+ this.log(` 📁 Local folder exists: ${pc.yellow(world.worldDir)}`);
75
+ if (world.tarballExists()) {
76
+ this.log(` 📦 world.tar.gz: ${pc.yellow("exists (will be overwritten)")}`);
77
+ }
78
+ if (existsSync(world.configPath)) {
79
+ this.log(` 📄 config.ts: ${pc.yellow("exists (will be overwritten)")}`);
80
+ }
81
+ this.log("");
82
+ const confirmResponse = await enquirer.prompt({
83
+ type: "confirm",
84
+ name: "confirm",
85
+ message: `Overwrite local world files? ${pc.red("This cannot be undone.")}`,
86
+ initial: false,
87
+ });
88
+ if (!confirmResponse.confirm) {
89
+ this.log(pc.yellow("✗ Pull cancelled"));
90
+ return;
91
+ }
92
+ }
93
+ const cloudWorld = await api.getWorld(worldSlug);
94
+ const tasks = new Listr([
95
+ {
96
+ title: "Creating world directory",
97
+ task: async () => {
98
+ await fs.mkdir(world.worldDir, { recursive: true });
99
+ },
100
+ },
101
+ {
102
+ title: "Downloading world",
103
+ task: async (_ctx, task) => {
104
+ const { downloadUrl } = await api.getWorldDownloadUrl(worldSlug);
105
+ const response = await fetch(downloadUrl);
106
+ if (!response.ok) {
107
+ throw new Error(`Failed to download: ${response.status} ${response.statusText}`);
108
+ }
109
+ const buffer = await response.arrayBuffer();
110
+ await fs.writeFile(world.tarballPath, Buffer.from(buffer));
111
+ task.title = "Downloaded world";
112
+ },
113
+ },
114
+ {
115
+ title: "Creating config.ts",
116
+ task: async (_ctx, task) => {
117
+ const config = {
118
+ name: cloudWorld.name,
119
+ description: cloudWorld.description ?? "no description",
120
+ visibility: cloudWorld.visibility,
121
+ domain: "minecraft",
122
+ };
123
+ const configContent = `export const config = ${JSON.stringify(config, null, "\t").replace(/"([a-zA-Z_][a-zA-Z0-9_]*)"\s*:/g, "$1:")};
124
+ `;
125
+ await fs.writeFile(world.configPath, configContent);
126
+ task.title = "Created config.ts";
127
+ },
128
+ },
129
+ ]);
130
+ await tasks.run();
131
+ this.log(pc.green(`\n✓ World pulled: ${worldSlug}`));
132
+ this.log(pc.dim(` → config.ts: ${world.configPath}`));
133
+ this.log(pc.dim(` → world.tar.gz: ${world.tarballPath}`));
134
+ }
135
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Push extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ worldSlug: import("@oclif/core/interfaces").Arg<string>;
7
+ };
8
+ static flags: {
9
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,64 @@
1
+ import { Command } from "@oclif/core";
2
+ import { Listr } from "listr2";
3
+ import pc from "picocolors";
4
+ import { ApiClient } from "../../lib/api-client.js";
5
+ import { getWorldSlugArgument } from "../../lib/arguments.js";
6
+ import { getConfigFlags } from "../../lib/flags.js";
7
+ import { World } from "../../lib/world.js";
8
+ export default class Push extends Command {
9
+ static description = "Upload a world (config + tarball) to the cloud";
10
+ static examples = ["<%= config.bin %> <%= command.id %> my-world"];
11
+ static args = {
12
+ worldSlug: getWorldSlugArgument({ description: "World slug to push" }),
13
+ };
14
+ static flags = {
15
+ ...getConfigFlags("api-key", "api-url"),
16
+ };
17
+ async run() {
18
+ const { args, flags } = await this.parse(Push);
19
+ const world = new World(args.worldSlug);
20
+ if (!world.exists()) {
21
+ this.error(pc.red(`World "${args.worldSlug}" does not exist locally.`));
22
+ }
23
+ if (!world.tarballExists()) {
24
+ this.error(pc.red(`World tarball not found at ${world.tarballPath}`));
25
+ }
26
+ const isValid = await world.validateTarball();
27
+ if (!isValid) {
28
+ this.error(pc.red(`Invalid world tarball: level.dat not found in ${world.tarballPath}`));
29
+ }
30
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
31
+ const config = await world.loadConfig();
32
+ const tasks = new Listr([
33
+ {
34
+ title: "Ensuring world exists in cloud",
35
+ task: async (_ctx, task) => {
36
+ const exists = await api.worldExists(args.worldSlug);
37
+ if (!exists) {
38
+ await api.createWorld(args.worldSlug, config);
39
+ task.title = "Created world in cloud";
40
+ }
41
+ else {
42
+ task.title = "World exists in cloud";
43
+ }
44
+ },
45
+ },
46
+ {
47
+ title: "Uploading config",
48
+ task: async (_ctx, task) => {
49
+ await api.updateWorld(args.worldSlug, config);
50
+ task.title = "Config uploaded";
51
+ },
52
+ },
53
+ {
54
+ title: "Uploading world",
55
+ task: async (_ctx, task) => {
56
+ await api.uploadWorldFile(args.worldSlug, world.tarballPath);
57
+ task.title = "World uploaded";
58
+ },
59
+ },
60
+ ]);
61
+ await tasks.run();
62
+ this.log(pc.green(`\n✓ World pushed: ${args.worldSlug}`));
63
+ }
64
+ }
@@ -1,5 +1,5 @@
1
1
  import type z from "zod";
2
- import { type AgentSchemaType, type ChallengeConfigSchemaType, type ChallengeSchemaType, HumanSchema, type RecordingDownloadUrlResponse, type RecordingMetadata, type RunStatusSchemaType } from "./schemas.js";
2
+ import { type AgentSchemaType, type ChallengeConfigSchemaType, type ChallengeSchemaType, type DownloadUrlResponse, HumanSchema, type RecordingMetadata, type RunStatusSchemaType, type WorldConfigSchemaType, type WorldSchemaType } from "./schemas.js";
3
3
  export declare class ApiClient {
4
4
  private apiUrl;
5
5
  private kradleApiKey;
@@ -66,12 +66,7 @@ export declare class ApiClient {
66
66
  * @returns The upload URL.
67
67
  */
68
68
  getChallengeUploadUrl(slug: string): Promise<string>;
69
- /**
70
- * Get the download URL for a challenge datapack.
71
- * @param slug - The slug of the challenge.
72
- * @returns The download URL and expiration time.
73
- */
74
- getChallengeDownloadUrl(slug: string): Promise<RecordingDownloadUrlResponse>;
69
+ getChallengeDownloadUrl(slug: string): Promise<DownloadUrlResponse>;
75
70
  runChallenge(runData: {
76
71
  challenge: string;
77
72
  participants: unknown[];
@@ -112,5 +107,15 @@ export declare class ApiClient {
112
107
  * @param timestamp - The timestamp of the recording.
113
108
  * @returns Download URL and expiration time.
114
109
  */
115
- getRecordingDownloadUrl(runId: string, participantId: string, timestamp: string): Promise<RecordingDownloadUrlResponse>;
110
+ getRecordingDownloadUrl(runId: string, participantId: string, timestamp: string): Promise<DownloadUrlResponse>;
111
+ listWorlds(): Promise<WorldSchemaType[]>;
112
+ listKradleWorlds(): Promise<WorldSchemaType[]>;
113
+ getWorld(slug: string): Promise<WorldSchemaType>;
114
+ worldExists(slug: string): Promise<boolean>;
115
+ createWorld(slug: string, config: WorldConfigSchemaType): Promise<WorldSchemaType>;
116
+ updateWorld(slug: string, config: WorldConfigSchemaType): Promise<void>;
117
+ deleteWorld(slug: string): Promise<void>;
118
+ getWorldUploadUrl(slug: string): Promise<string>;
119
+ getWorldDownloadUrl(slug: string): Promise<DownloadUrlResponse>;
120
+ uploadWorldFile(slug: string, tarballPath: string): Promise<void>;
116
121
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import fs from "node:fs/promises";
3
- import { AgentsResponseSchema, ChallengeSchema, ChallengesResponseSchema, HumanSchema, JobResponseSchema, RecordingDownloadUrlResponseSchema, RecordingsListResponseSchema, RunStatusSchema, UploadUrlResponseSchema, } from "./schemas.js";
3
+ import { AgentsResponseSchema, ChallengeSchema, ChallengesResponseSchema, DownloadUrlResponseSchema, HumanSchema, JobResponseSchema, RecordingsListResponseSchema, RunStatusSchema, UploadUrlResponseSchema, WorldSchema, WorldsResponseSchema, } from "./schemas.js";
4
4
  const DEFAULT_PAGE_SIZE = 30;
5
5
  const DEFAULT_CHALLENGE_SCHEMA = {
6
6
  slug: "",
@@ -210,13 +210,8 @@ export class ApiClient {
210
210
  const response = await this.get(`challenges/${slug}/datapackUploadUrl`, {}, UploadUrlResponseSchema);
211
211
  return response.uploadUrl;
212
212
  }
213
- /**
214
- * Get the download URL for a challenge datapack.
215
- * @param slug - The slug of the challenge.
216
- * @returns The download URL and expiration time.
217
- */
218
213
  async getChallengeDownloadUrl(slug) {
219
- return this.get(`challenges/${slug}/datapackDownloadUrl`, {}, RecordingDownloadUrlResponseSchema);
214
+ return this.get(`challenges/${slug}/datapackDownloadUrl`, {}, DownloadUrlResponseSchema);
220
215
  }
221
216
  async runChallenge(runData) {
222
217
  const url = "jobs";
@@ -270,6 +265,60 @@ export class ApiClient {
270
265
  */
271
266
  async getRecordingDownloadUrl(runId, participantId, timestamp) {
272
267
  const url = `runs/${runId}/recordings/${participantId}/downloadUrl?timestamp=${encodeURIComponent(timestamp)}`;
273
- return this.get(url, {}, RecordingDownloadUrlResponseSchema);
268
+ return this.get(url, {}, DownloadUrlResponseSchema);
269
+ }
270
+ async listWorlds() {
271
+ return this.listResource("worlds", "worlds", WorldsResponseSchema);
272
+ }
273
+ async listKradleWorlds() {
274
+ return this.listResource("humans/team-kradle/worlds", "worlds", WorldsResponseSchema);
275
+ }
276
+ async getWorld(slug) {
277
+ return this.get(`worlds/${slug}`, {}, WorldSchema);
278
+ }
279
+ async worldExists(slug) {
280
+ try {
281
+ const res = await this.get(`worlds/${slug}`, {});
282
+ return res !== null && res !== undefined;
283
+ }
284
+ catch {
285
+ return false;
286
+ }
287
+ }
288
+ async createWorld(slug, config) {
289
+ return this.post("worlds", { body: JSON.stringify({ slug, ...config }) }, WorldSchema);
290
+ }
291
+ async updateWorld(slug, config) {
292
+ await this.put(`worlds/${slug}`, {
293
+ body: JSON.stringify({ slug, ...config }),
294
+ });
295
+ }
296
+ async deleteWorld(slug) {
297
+ await this.delete(`worlds/${slug}`);
298
+ }
299
+ async getWorldUploadUrl(slug) {
300
+ const response = await this.get(`worlds/${slug}/uploadUrl`, {}, UploadUrlResponseSchema);
301
+ return response.uploadUrl;
302
+ }
303
+ async getWorldDownloadUrl(slug) {
304
+ return this.get(`worlds/${slug}/downloadUrl`, {}, DownloadUrlResponseSchema);
305
+ }
306
+ async uploadWorldFile(slug, tarballPath) {
307
+ const uploadUrl = await this.getWorldUploadUrl(slug);
308
+ if (!existsSync(tarballPath)) {
309
+ throw new Error(`World tarball not found at ${tarballPath}`);
310
+ }
311
+ const fileBuffer = await fs.readFile(tarballPath);
312
+ const response = await fetch(uploadUrl, {
313
+ method: "PUT",
314
+ headers: {
315
+ "Content-Type": "application/gzip",
316
+ "Content-Length": fileBuffer.length.toString(),
317
+ },
318
+ body: fileBuffer,
319
+ });
320
+ if (!response.ok) {
321
+ throw new Error(`Failed to upload world: ${response.statusText}`);
322
+ }
274
323
  }
275
324
  }
@@ -5,3 +5,8 @@ export declare function getChallengeSlugArgument<R extends boolean = true>({ des
5
5
  required?: R;
6
6
  allowTeam?: boolean;
7
7
  }): Arg<R extends true ? string : string | undefined>;
8
+ export declare function getWorldSlugArgument<R extends boolean = true>({ description, required, allowTeam, }: {
9
+ description: string;
10
+ required?: R;
11
+ allowTeam?: boolean;
12
+ }): Arg<R extends true ? string : string | undefined>;
@@ -26,3 +26,21 @@ export function getChallengeSlugArgument({ description, required, allowTeam = fa
26
26
  // biome-ignore lint/suspicious/noExplicitAny: Typescript can't handle the conditional "required"
27
27
  return arg;
28
28
  }
29
+ export function getWorldSlugArgument({ description, required, allowTeam = false, }) {
30
+ const regex = allowTeam ? NAMESPACED_SLUG_REGEX : LOCAL_SLUG_REGEX;
31
+ const errorMessage = allowTeam
32
+ ? "World slugs must be lowercase alphanumeric characters and hyphens, and must not start or end with a hyphen. Optionally, a team prefix can be provided (e.g., 'team-name:my-world')."
33
+ : "World slugs must be lowercase alphanumeric characters and hyphens, and must not start or end with a hyphen.";
34
+ const arg = Args.string({
35
+ description,
36
+ required: required ?? true,
37
+ parse: async (input) => {
38
+ if (!regex.test(input)) {
39
+ throw new Error(`Invalid world slug: ${input}. ${errorMessage}`);
40
+ }
41
+ return input;
42
+ },
43
+ });
44
+ // biome-ignore lint/suspicious/noExplicitAny: Typescript can't handle the conditional "required"
45
+ return arg;
46
+ }
@@ -37,9 +37,6 @@ export declare class Challenge {
37
37
  * Build the challenge datapack
38
38
  */
39
39
  build(silent?: boolean): Promise<void>;
40
- /**
41
- * Load the challenge configuration from config.ts
42
- */
43
40
  loadConfig(): Promise<ChallengeConfigSchemaType>;
44
41
  /**
45
42
  * Build the challenge datapack and upload it to the cloud.
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import pc from "picocolors";
6
6
  import * as tar from "tar";
7
7
  import { ChallengeConfigSchema } from "./schemas.js";
8
- import { executeNodeCommand, executeTypescriptFile, getStaticResourcePath, readDirSorted } from "./utils.js";
8
+ import { executeTypescriptFile, getStaticResourcePath, loadTypescriptExport, readDirSorted } from "./utils.js";
9
9
  export const SOURCE_FOLDER = ".src";
10
10
  export class Challenge {
11
11
  shortSlug;
@@ -106,25 +106,15 @@ export class Challenge {
106
106
  strict: true,
107
107
  }, ["datapack", SOURCE_FOLDER]);
108
108
  }
109
- /**
110
- * Load the challenge configuration from config.ts
111
- */
112
109
  async loadConfig() {
113
110
  if (!existsSync(this.configPath)) {
114
111
  throw new Error(`Config file not found at ${this.configPath}`);
115
112
  }
116
- // We spawn a new NodeJS process to execute & log the config file.
117
- // We can't directly import the file because it would be cached, and import cache can't be invalidated.
118
- const stdout = await executeNodeCommand([
119
- "--experimental-transform-types",
120
- "--no-warnings",
121
- "-e",
122
- `console.log(JSON.stringify(require("${this.configPath}").config));`,
123
- ], {
113
+ const config = await loadTypescriptExport(this.configPath, "config", {
124
114
  KRADLE_CHALLENGES_PATH: this.kradleChallengesPath,
125
115
  NAMESPACE: "kradle",
126
116
  });
127
- return ChallengeConfigSchema.parse(JSON.parse(stdout));
117
+ return ChallengeConfigSchema.parse(config);
128
118
  }
129
119
  /**
130
120
  * Build the challenge datapack and upload it to the cloud.
@@ -143,6 +143,10 @@ export declare const UploadUrlResponseSchema: z.ZodObject<{
143
143
  uploadUrl: z.ZodString;
144
144
  expiresAt: z.ZodString;
145
145
  }, z.core.$strip>;
146
+ export declare const DownloadUrlResponseSchema: z.ZodObject<{
147
+ downloadUrl: z.ZodString;
148
+ expiresAt: z.ZodString;
149
+ }, z.core.$strip>;
146
150
  export declare const AgentSchema: z.ZodObject<{
147
151
  username: z.ZodOptional<z.ZodString>;
148
152
  name: z.ZodOptional<z.ZodString>;
@@ -186,9 +190,44 @@ export declare const RecordingsListResponseSchema: z.ZodObject<{
186
190
  sizeBytes: z.ZodNumber;
187
191
  }, z.core.$strip>>;
188
192
  }, z.core.$strip>;
189
- export declare const RecordingDownloadUrlResponseSchema: z.ZodObject<{
190
- downloadUrl: z.ZodString;
191
- expiresAt: z.ZodString;
193
+ export declare const WorldSchema: z.ZodObject<{
194
+ id: z.ZodString;
195
+ slug: z.ZodString;
196
+ name: z.ZodString;
197
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
198
+ visibility: z.ZodEnum<{
199
+ private: "private";
200
+ public: "public";
201
+ }>;
202
+ creationTime: z.ZodString;
203
+ updateTime: z.ZodString;
204
+ creator: z.ZodString;
205
+ }, z.core.$strip>;
206
+ export declare const WorldConfigSchema: z.ZodObject<{
207
+ name: z.ZodString;
208
+ description: z.ZodOptional<z.ZodString>;
209
+ visibility: z.ZodEnum<{
210
+ private: "private";
211
+ public: "public";
212
+ }>;
213
+ domain: z.ZodDefault<z.ZodLiteral<"minecraft">>;
214
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
215
+ }, z.core.$strip>;
216
+ export declare const WorldsResponseSchema: z.ZodObject<{
217
+ worlds: z.ZodArray<z.ZodObject<{
218
+ id: z.ZodString;
219
+ slug: z.ZodString;
220
+ name: z.ZodString;
221
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
222
+ visibility: z.ZodEnum<{
223
+ private: "private";
224
+ public: "public";
225
+ }>;
226
+ creationTime: z.ZodString;
227
+ updateTime: z.ZodString;
228
+ creator: z.ZodString;
229
+ }, z.core.$strip>>;
230
+ nextPageToken: z.ZodOptional<z.ZodString>;
192
231
  }, z.core.$strip>;
193
232
  export type ChallengeSchemaType = z.infer<typeof ChallengeSchema>;
194
233
  export type ChallengeConfigSchemaType = z.infer<typeof ChallengeConfigSchema>;
@@ -200,5 +239,8 @@ export type AgentSchemaType = z.infer<typeof AgentSchema>;
200
239
  export type AgentsResponseType = z.infer<typeof AgentsResponseSchema>;
201
240
  export type RecordingMetadata = z.infer<typeof RecordingMetadataSchema>;
202
241
  export type RecordingsListResponse = z.infer<typeof RecordingsListResponseSchema>;
203
- export type RecordingDownloadUrlResponse = z.infer<typeof RecordingDownloadUrlResponseSchema>;
204
242
  export type RunParticipant = z.infer<typeof RunParticipantSchema>;
243
+ export type DownloadUrlResponse = z.infer<typeof DownloadUrlResponseSchema>;
244
+ export type WorldSchemaType = z.infer<typeof WorldSchema>;
245
+ export type WorldConfigSchemaType = z.infer<typeof WorldConfigSchema>;
246
+ export type WorldsResponseType = z.infer<typeof WorldsResponseSchema>;
@@ -64,6 +64,10 @@ export const UploadUrlResponseSchema = z.object({
64
64
  uploadUrl: z.string(),
65
65
  expiresAt: z.string(),
66
66
  });
67
+ export const DownloadUrlResponseSchema = z.object({
68
+ downloadUrl: z.string(),
69
+ expiresAt: z.string(),
70
+ });
67
71
  export const AgentSchema = z.object({
68
72
  username: z.string().optional(),
69
73
  name: z.string().optional(),
@@ -86,7 +90,24 @@ export const RecordingMetadataSchema = z.object({
86
90
  export const RecordingsListResponseSchema = z.object({
87
91
  recordings: z.array(RecordingMetadataSchema),
88
92
  });
89
- export const RecordingDownloadUrlResponseSchema = z.object({
90
- downloadUrl: z.string(),
91
- expiresAt: z.string(),
93
+ export const WorldSchema = z.object({
94
+ id: z.string(),
95
+ slug: z.string(),
96
+ name: z.string(),
97
+ description: z.string().nullish(),
98
+ visibility: z.enum(["private", "public"]),
99
+ creationTime: z.string(),
100
+ updateTime: z.string(),
101
+ creator: z.string(),
102
+ });
103
+ export const WorldConfigSchema = z.object({
104
+ name: z.string(),
105
+ description: z.string().optional(),
106
+ visibility: z.enum(["private", "public"]),
107
+ domain: z.literal("minecraft").default("minecraft"),
108
+ metadata: z.record(z.string(), z.string()).optional(),
109
+ });
110
+ export const WorldsResponseSchema = z.object({
111
+ worlds: z.array(WorldSchema),
112
+ nextPageToken: z.string().optional(),
92
113
  });
@@ -86,6 +86,7 @@ export declare function executeCommand(command: string, args: string[], options?
86
86
  * @returns A promise that resolves with the stdout of the command.
87
87
  */
88
88
  export declare function executeNodeCommand(args: string[], env: Record<string, string>): Promise<string>;
89
+ export declare function loadTypescriptExport(filePath: string, exportName: string, env?: Record<string, string>): Promise<unknown>;
89
90
  /**
90
91
  * Open a URL in the default browser.
91
92
  * This is fire-and-forget, so we don't wait for it to complete.
package/dist/lib/utils.js CHANGED
@@ -168,6 +168,15 @@ export async function executeCommand(command, args, options) {
168
168
  export async function executeNodeCommand(args, env) {
169
169
  return executeCommand(process.execPath, args, { env });
170
170
  }
171
+ export async function loadTypescriptExport(filePath, exportName, env = {}) {
172
+ const stdout = await executeNodeCommand([
173
+ "--experimental-transform-types",
174
+ "--no-warnings",
175
+ "-e",
176
+ `console.log(JSON.stringify(require("${filePath}").${exportName}));`,
177
+ ], env);
178
+ return JSON.parse(stdout);
179
+ }
171
180
  /**
172
181
  * Open a URL in the default browser.
173
182
  * This is fire-and-forget, so we don't wait for it to complete.
@@ -0,0 +1,21 @@
1
+ import { type WorldConfigSchemaType } from "./schemas.js";
2
+ /**
3
+ * Represents a Minecraft world stored locally as:
4
+ * worlds/<slug>/config.ts - metadata (name, visibility, etc.)
5
+ * worlds/<slug>/world.tar.gz - the packaged world files
6
+ */
7
+ export declare class World {
8
+ readonly shortSlug: string;
9
+ constructor(shortSlug: string);
10
+ get worldDir(): string;
11
+ get configPath(): string;
12
+ get tarballPath(): string;
13
+ exists(): boolean;
14
+ tarballExists(): boolean;
15
+ validateTarball(): Promise<boolean>;
16
+ loadConfig(): Promise<WorldConfigSchemaType>;
17
+ static getLocalWorlds(): Promise<string[]>;
18
+ static createLocal(slug: string, config: WorldConfigSchemaType): Promise<void>;
19
+ static createTarballFrom(sourcePath: string, slug: string): Promise<void>;
20
+ static toSlug(folderName: string): string;
21
+ }
@@ -0,0 +1,102 @@
1
+ import { existsSync } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import * as tar from "tar";
5
+ import { WorldConfigSchema } from "./schemas.js";
6
+ import { loadTypescriptExport, readDirSorted } from "./utils.js";
7
+ /**
8
+ * Represents a Minecraft world stored locally as:
9
+ * worlds/<slug>/config.ts - metadata (name, visibility, etc.)
10
+ * worlds/<slug>/world.tar.gz - the packaged world files
11
+ */
12
+ export class World {
13
+ shortSlug;
14
+ constructor(shortSlug) {
15
+ this.shortSlug = shortSlug;
16
+ }
17
+ get worldDir() {
18
+ return path.resolve(process.cwd(), "worlds", this.shortSlug);
19
+ }
20
+ get configPath() {
21
+ return path.join(this.worldDir, "config.ts");
22
+ }
23
+ get tarballPath() {
24
+ return path.join(this.worldDir, "world.tar.gz");
25
+ }
26
+ exists() {
27
+ return existsSync(this.worldDir) && existsSync(this.configPath);
28
+ }
29
+ tarballExists() {
30
+ return existsSync(this.tarballPath);
31
+ }
32
+ // Validates that the tarball contains level.dat (required for valid Minecraft worlds)
33
+ async validateTarball() {
34
+ if (!this.tarballExists()) {
35
+ return false;
36
+ }
37
+ const entries = [];
38
+ await tar.list({
39
+ file: this.tarballPath,
40
+ onReadEntry: (entry) => {
41
+ entries.push(entry.path);
42
+ },
43
+ });
44
+ return entries.some((entry) => entry === "level.dat" || entry.endsWith("/level.dat"));
45
+ }
46
+ async loadConfig() {
47
+ if (!existsSync(this.configPath)) {
48
+ throw new Error(`Config file not found at ${this.configPath}`);
49
+ }
50
+ const config = await loadTypescriptExport(this.configPath, "config");
51
+ return WorldConfigSchema.parse(config);
52
+ }
53
+ static async getLocalWorlds() {
54
+ const worldsDir = path.resolve(process.cwd(), "worlds");
55
+ if (!existsSync(worldsDir)) {
56
+ return [];
57
+ }
58
+ const entries = await readDirSorted(worldsDir);
59
+ const worlds = [];
60
+ for (const entry of entries) {
61
+ if (entry.isDirectory() && entry.parentPath === worldsDir) {
62
+ const configPath = path.join(worldsDir, entry.name, "config.ts");
63
+ if (existsSync(configPath)) {
64
+ worlds.push(entry.name);
65
+ }
66
+ }
67
+ }
68
+ return worlds;
69
+ }
70
+ static async createLocal(slug, config) {
71
+ const world = new World(slug);
72
+ await fs.mkdir(world.worldDir, { recursive: true });
73
+ const configContent = `export const config = ${JSON.stringify(config, null, "\t").replace(/"([a-zA-Z_][a-zA-Z0-9_]*)"\s*:/g, "$1:")};
74
+ `;
75
+ await fs.writeFile(world.configPath, configContent);
76
+ }
77
+ static async createTarballFrom(sourcePath, slug) {
78
+ const resolvedSource = path.resolve(sourcePath);
79
+ if (!existsSync(resolvedSource)) {
80
+ throw new Error(`Source path does not exist: ${resolvedSource}`);
81
+ }
82
+ const levelDatPath = path.join(resolvedSource, "level.dat");
83
+ if (!existsSync(levelDatPath)) {
84
+ throw new Error(`Invalid Minecraft world: level.dat not found in ${resolvedSource}`);
85
+ }
86
+ const world = new World(slug);
87
+ await fs.mkdir(world.worldDir, { recursive: true });
88
+ await tar.create({
89
+ file: world.tarballPath,
90
+ gzip: true,
91
+ cwd: resolvedSource,
92
+ }, ["."]);
93
+ }
94
+ // Converts folder name to valid slug: "My World 2" -> "my-world-2"
95
+ static toSlug(folderName) {
96
+ return folderName
97
+ .toLowerCase()
98
+ .replace(/[^a-z0-9]+/g, "-")
99
+ .replace(/^-+|-+$/g, "")
100
+ .replace(/-+/g, "-");
101
+ }
102
+ }
@@ -867,7 +867,277 @@
867
867
  "experiment",
868
868
  "run.js"
869
869
  ]
870
+ },
871
+ "world:delete": {
872
+ "aliases": [],
873
+ "args": {
874
+ "worldSlug": {
875
+ "description": "World slug to delete",
876
+ "name": "worldSlug",
877
+ "required": true
878
+ }
879
+ },
880
+ "description": "Delete a world locally, from the cloud, or both",
881
+ "examples": [
882
+ "<%= config.bin %> <%= command.id %> my-world",
883
+ "<%= config.bin %> <%= command.id %> my-world --yes"
884
+ ],
885
+ "flags": {
886
+ "yes": {
887
+ "char": "y",
888
+ "description": "Skip confirmation prompts",
889
+ "name": "yes",
890
+ "allowNo": false,
891
+ "type": "boolean"
892
+ },
893
+ "api-key": {
894
+ "description": "Kradle API key",
895
+ "env": "KRADLE_API_KEY",
896
+ "name": "api-key",
897
+ "required": true,
898
+ "hasDynamicHelp": false,
899
+ "multiple": false,
900
+ "type": "option"
901
+ },
902
+ "api-url": {
903
+ "description": "Kradle Web API URL",
904
+ "env": "KRADLE_API_URL",
905
+ "name": "api-url",
906
+ "required": true,
907
+ "default": "https://api.kradle.ai/v0",
908
+ "hasDynamicHelp": false,
909
+ "multiple": false,
910
+ "type": "option"
911
+ }
912
+ },
913
+ "hasDynamicHelp": false,
914
+ "hiddenAliases": [],
915
+ "id": "world:delete",
916
+ "pluginAlias": "@kradle/cli",
917
+ "pluginName": "@kradle/cli",
918
+ "pluginType": "core",
919
+ "strict": true,
920
+ "enableJsonFlag": false,
921
+ "isESM": true,
922
+ "relativePath": [
923
+ "dist",
924
+ "commands",
925
+ "world",
926
+ "delete.js"
927
+ ]
928
+ },
929
+ "world:import": {
930
+ "aliases": [],
931
+ "args": {
932
+ "path": {
933
+ "description": "Path to the Minecraft world folder",
934
+ "name": "path",
935
+ "required": true
936
+ }
937
+ },
938
+ "description": "Import a Minecraft world folder and package it as a tarball",
939
+ "examples": [
940
+ "<%= config.bin %> <%= command.id %> ~/minecraft/saves/MyWorld",
941
+ "<%= config.bin %> <%= command.id %> ~/minecraft/saves/MyWorld --as my-world"
942
+ ],
943
+ "flags": {
944
+ "as": {
945
+ "description": "Slug for the world (defaults to folder name converted to slug)",
946
+ "name": "as",
947
+ "hasDynamicHelp": false,
948
+ "multiple": false,
949
+ "type": "option"
950
+ },
951
+ "api-key": {
952
+ "description": "Kradle API key",
953
+ "env": "KRADLE_API_KEY",
954
+ "name": "api-key",
955
+ "required": true,
956
+ "hasDynamicHelp": false,
957
+ "multiple": false,
958
+ "type": "option"
959
+ },
960
+ "api-url": {
961
+ "description": "Kradle Web API URL",
962
+ "env": "KRADLE_API_URL",
963
+ "name": "api-url",
964
+ "required": true,
965
+ "default": "https://api.kradle.ai/v0",
966
+ "hasDynamicHelp": false,
967
+ "multiple": false,
968
+ "type": "option"
969
+ }
970
+ },
971
+ "hasDynamicHelp": false,
972
+ "hiddenAliases": [],
973
+ "id": "world:import",
974
+ "pluginAlias": "@kradle/cli",
975
+ "pluginName": "@kradle/cli",
976
+ "pluginType": "core",
977
+ "strict": true,
978
+ "enableJsonFlag": false,
979
+ "isESM": true,
980
+ "relativePath": [
981
+ "dist",
982
+ "commands",
983
+ "world",
984
+ "import.js"
985
+ ]
986
+ },
987
+ "world:list": {
988
+ "aliases": [],
989
+ "args": {},
990
+ "description": "List all worlds (local and cloud)",
991
+ "examples": [
992
+ "<%= config.bin %> <%= command.id %>"
993
+ ],
994
+ "flags": {
995
+ "api-key": {
996
+ "description": "Kradle API key",
997
+ "env": "KRADLE_API_KEY",
998
+ "name": "api-key",
999
+ "required": true,
1000
+ "hasDynamicHelp": false,
1001
+ "multiple": false,
1002
+ "type": "option"
1003
+ },
1004
+ "api-url": {
1005
+ "description": "Kradle Web API URL",
1006
+ "env": "KRADLE_API_URL",
1007
+ "name": "api-url",
1008
+ "required": true,
1009
+ "default": "https://api.kradle.ai/v0",
1010
+ "hasDynamicHelp": false,
1011
+ "multiple": false,
1012
+ "type": "option"
1013
+ }
1014
+ },
1015
+ "hasDynamicHelp": false,
1016
+ "hiddenAliases": [],
1017
+ "id": "world:list",
1018
+ "pluginAlias": "@kradle/cli",
1019
+ "pluginName": "@kradle/cli",
1020
+ "pluginType": "core",
1021
+ "strict": true,
1022
+ "enableJsonFlag": false,
1023
+ "isESM": true,
1024
+ "relativePath": [
1025
+ "dist",
1026
+ "commands",
1027
+ "world",
1028
+ "list.js"
1029
+ ]
1030
+ },
1031
+ "world:pull": {
1032
+ "aliases": [],
1033
+ "args": {
1034
+ "worldSlug": {
1035
+ "description": "World slug to pull (interactive selection if omitted)",
1036
+ "name": "worldSlug",
1037
+ "required": false
1038
+ }
1039
+ },
1040
+ "description": "Download a world from the cloud",
1041
+ "examples": [
1042
+ "<%= config.bin %> <%= command.id %>",
1043
+ "<%= config.bin %> <%= command.id %> my-world",
1044
+ "<%= config.bin %> <%= command.id %> username:my-world",
1045
+ "<%= config.bin %> <%= command.id %> my-world --yes"
1046
+ ],
1047
+ "flags": {
1048
+ "yes": {
1049
+ "char": "y",
1050
+ "description": "Skip confirmation prompts",
1051
+ "name": "yes",
1052
+ "allowNo": false,
1053
+ "type": "boolean"
1054
+ },
1055
+ "api-key": {
1056
+ "description": "Kradle API key",
1057
+ "env": "KRADLE_API_KEY",
1058
+ "name": "api-key",
1059
+ "required": true,
1060
+ "hasDynamicHelp": false,
1061
+ "multiple": false,
1062
+ "type": "option"
1063
+ },
1064
+ "api-url": {
1065
+ "description": "Kradle Web API URL",
1066
+ "env": "KRADLE_API_URL",
1067
+ "name": "api-url",
1068
+ "required": true,
1069
+ "default": "https://api.kradle.ai/v0",
1070
+ "hasDynamicHelp": false,
1071
+ "multiple": false,
1072
+ "type": "option"
1073
+ }
1074
+ },
1075
+ "hasDynamicHelp": false,
1076
+ "hiddenAliases": [],
1077
+ "id": "world:pull",
1078
+ "pluginAlias": "@kradle/cli",
1079
+ "pluginName": "@kradle/cli",
1080
+ "pluginType": "core",
1081
+ "strict": true,
1082
+ "enableJsonFlag": false,
1083
+ "isESM": true,
1084
+ "relativePath": [
1085
+ "dist",
1086
+ "commands",
1087
+ "world",
1088
+ "pull.js"
1089
+ ]
1090
+ },
1091
+ "world:push": {
1092
+ "aliases": [],
1093
+ "args": {
1094
+ "worldSlug": {
1095
+ "description": "World slug to push",
1096
+ "name": "worldSlug",
1097
+ "required": true
1098
+ }
1099
+ },
1100
+ "description": "Upload a world (config + tarball) to the cloud",
1101
+ "examples": [
1102
+ "<%= config.bin %> <%= command.id %> my-world"
1103
+ ],
1104
+ "flags": {
1105
+ "api-key": {
1106
+ "description": "Kradle API key",
1107
+ "env": "KRADLE_API_KEY",
1108
+ "name": "api-key",
1109
+ "required": true,
1110
+ "hasDynamicHelp": false,
1111
+ "multiple": false,
1112
+ "type": "option"
1113
+ },
1114
+ "api-url": {
1115
+ "description": "Kradle Web API URL",
1116
+ "env": "KRADLE_API_URL",
1117
+ "name": "api-url",
1118
+ "required": true,
1119
+ "default": "https://api.kradle.ai/v0",
1120
+ "hasDynamicHelp": false,
1121
+ "multiple": false,
1122
+ "type": "option"
1123
+ }
1124
+ },
1125
+ "hasDynamicHelp": false,
1126
+ "hiddenAliases": [],
1127
+ "id": "world:push",
1128
+ "pluginAlias": "@kradle/cli",
1129
+ "pluginName": "@kradle/cli",
1130
+ "pluginType": "core",
1131
+ "strict": true,
1132
+ "enableJsonFlag": false,
1133
+ "isESM": true,
1134
+ "relativePath": [
1135
+ "dist",
1136
+ "commands",
1137
+ "world",
1138
+ "push.js"
1139
+ ]
870
1140
  }
871
1141
  },
872
- "version": "0.2.5"
1142
+ "version": "0.2.7"
873
1143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kradle/cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Kradle's CLI. Manage challenges, experiments, agents and more!",
5
5
  "keywords": [
6
6
  "cli"
@@ -84,6 +84,9 @@
84
84
  },
85
85
  "ai-docs": {
86
86
  "description": "Output LLM reference documentation"
87
+ },
88
+ "world": {
89
+ "description": "Manage worlds"
87
90
  }
88
91
  }
89
92
  }