@kradle/cli 0.2.5 → 0.2.6
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/dist/commands/challenge/pull.js +2 -2
- package/dist/commands/world/delete.d.ts +14 -0
- package/dist/commands/world/delete.js +80 -0
- package/dist/commands/world/import.d.ts +14 -0
- package/dist/commands/world/import.js +116 -0
- package/dist/commands/world/list.d.ts +10 -0
- package/dist/commands/world/list.js +46 -0
- package/dist/commands/world/pull.d.ts +14 -0
- package/dist/commands/world/pull.js +135 -0
- package/dist/commands/world/push.d.ts +13 -0
- package/dist/commands/world/push.js +64 -0
- package/dist/lib/api-client.d.ts +13 -8
- package/dist/lib/api-client.js +57 -8
- package/dist/lib/arguments.d.ts +5 -0
- package/dist/lib/arguments.js +18 -0
- package/dist/lib/challenge.d.ts +0 -3
- package/dist/lib/challenge.js +3 -13
- package/dist/lib/schemas.d.ts +46 -4
- package/dist/lib/schemas.js +24 -3
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +9 -0
- package/dist/lib/world.d.ts +21 -0
- package/dist/lib/world.js +102 -0
- package/oclif.manifest.json +271 -1
- package/package.json +4 -1
|
@@ -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
|
|
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
|
|
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,116 @@
|
|
|
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 config = {
|
|
68
|
+
name: slug,
|
|
69
|
+
description: "no description",
|
|
70
|
+
visibility: "private",
|
|
71
|
+
domain: "minecraft",
|
|
72
|
+
};
|
|
73
|
+
await World.createLocal(slug, config);
|
|
74
|
+
task.title = `Created worlds/${slug}/`;
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
title: isReimport ? "Updating tarball" : "Packaging world as tarball",
|
|
79
|
+
task: async (_ctx, task) => {
|
|
80
|
+
await World.createTarballFrom(sourcePath, slug);
|
|
81
|
+
task.title = isReimport ? `Updated worlds/${slug}/world.tar.gz` : `Created worlds/${slug}/world.tar.gz`;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
title: "Creating world in cloud",
|
|
86
|
+
enabled: () => !isReimport,
|
|
87
|
+
task: async (_ctx, task) => {
|
|
88
|
+
const exists = await api.worldExists(slug);
|
|
89
|
+
if (exists) {
|
|
90
|
+
task.title = "World already exists in cloud";
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const config = {
|
|
94
|
+
name: slug,
|
|
95
|
+
visibility: "private",
|
|
96
|
+
domain: "minecraft",
|
|
97
|
+
};
|
|
98
|
+
await api.createWorld(slug, config);
|
|
99
|
+
task.title = "Created world in cloud";
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
await tasks.run();
|
|
104
|
+
if (isReimport) {
|
|
105
|
+
this.log(pc.green(`\n✓ World re-imported: ${slug}`));
|
|
106
|
+
this.log(pc.dim(` → world.tar.gz updated (config.ts preserved)`));
|
|
107
|
+
this.log(pc.dim(`\nRun ${pc.cyan(`kradle world push ${slug}`)} to upload to cloud.`));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this.log(pc.green(`\n✓ World imported: ${slug}`));
|
|
111
|
+
this.log(pc.dim(` → config.ts: ${world.configPath}`));
|
|
112
|
+
this.log(pc.dim(` → world.tar.gz: ${world.tarballPath}`));
|
|
113
|
+
this.log(pc.dim(`\nRun ${pc.cyan(`kradle world push ${slug}`)} to upload to cloud.`));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type z from "zod";
|
|
2
|
-
import { type AgentSchemaType, type ChallengeConfigSchemaType, type ChallengeSchemaType, HumanSchema, type
|
|
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<
|
|
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
|
}
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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,
|
|
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`, {},
|
|
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, {},
|
|
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
|
}
|
package/dist/lib/arguments.d.ts
CHANGED
|
@@ -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>;
|
package/dist/lib/arguments.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/lib/challenge.d.ts
CHANGED
|
@@ -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.
|
package/dist/lib/challenge.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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(
|
|
117
|
+
return ChallengeConfigSchema.parse(config);
|
|
128
118
|
}
|
|
129
119
|
/**
|
|
130
120
|
* Build the challenge datapack and upload it to the cloud.
|
package/dist/lib/schemas.d.ts
CHANGED
|
@@ -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
|
|
190
|
-
|
|
191
|
-
|
|
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>;
|
package/dist/lib/schemas.js
CHANGED
|
@@ -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
|
|
90
|
-
|
|
91
|
-
|
|
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
|
});
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -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.
|
|
1142
|
+
"version": "0.2.6"
|
|
873
1143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kradle/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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
|
}
|