@coderook/cli 0.34.11 → 0.36.0
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/.claude-plugin/plugin.json +1 -1
- package/README.md +71 -0
- package/dist/cbx/src/core/worktree.js +29 -2
- package/dist/cli/src/api.js +16 -0
- package/dist/cli/src/cli.js +97 -0
- package/dist/cli/src/github_bridge.js +403 -0
- package/dist/cli/src/mods_command.js +518 -0
- package/dist/cli/src/zip_writer.js +188 -0
- package/package.json +1 -1
- package/skills/coderook/SKILL.md +45 -1
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.commandMods = commandMods;
|
|
7
|
+
/**
|
|
8
|
+
* cbx mods — mod repositories for games.
|
|
9
|
+
*
|
|
10
|
+
* A game's developer sets one up (`cbx mods create`) and gets an address and
|
|
11
|
+
* a public key to build into the game; modders publish to it with their own
|
|
12
|
+
* CodeRook accounts (`cbx mods publish`), and nothing they send is listed
|
|
13
|
+
* until it passes the repository's checks and a malware scan. The game's own
|
|
14
|
+
* client reads the signed catalogue and downloads; that side is a protocol,
|
|
15
|
+
* described in the docs, not something this command does.
|
|
16
|
+
*/
|
|
17
|
+
const node_crypto_1 = require("node:crypto");
|
|
18
|
+
const node_fs_1 = require("node:fs");
|
|
19
|
+
const promises_1 = require("node:fs/promises");
|
|
20
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
21
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
23
|
+
const promises_2 = require("node:readline/promises");
|
|
24
|
+
const config_js_1 = require("./config.js");
|
|
25
|
+
const colour_js_1 = require("./colour.js");
|
|
26
|
+
const zip_writer_js_1 = require("./zip_writer.js");
|
|
27
|
+
const ROOT = "/v1/modrepo";
|
|
28
|
+
function modsOrigin() {
|
|
29
|
+
return (node_process_1.default.env.CODEROOK_MODS_URL ?? "https://mods.coderook.com").replace(/\/+$/, "");
|
|
30
|
+
}
|
|
31
|
+
const flag = (parsed, ...names) => {
|
|
32
|
+
for (const name of names) {
|
|
33
|
+
const value = parsed.flags.get(name);
|
|
34
|
+
if (typeof value === "string")
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
};
|
|
39
|
+
const has = (parsed, ...names) => names.some((name) => parsed.flags.has(name));
|
|
40
|
+
async function call(route, method = "GET", body, raw, auth = true) {
|
|
41
|
+
const headers = { accept: "application/json", "user-agent": "CodeRook-CLI/0.1" };
|
|
42
|
+
if (auth) {
|
|
43
|
+
const token = await (0, config_js_1.loadToken)();
|
|
44
|
+
if (!token)
|
|
45
|
+
throw new Error("Not signed in. Run: cbx sign-in");
|
|
46
|
+
headers.authorization = `Bearer ${token}`;
|
|
47
|
+
}
|
|
48
|
+
if (body !== undefined)
|
|
49
|
+
headers["content-type"] = "application/json";
|
|
50
|
+
if (raw)
|
|
51
|
+
headers["content-type"] = "application/octet-stream";
|
|
52
|
+
const response = await fetch(`${modsOrigin()}${route}`, {
|
|
53
|
+
method,
|
|
54
|
+
headers,
|
|
55
|
+
body: raw ?? (body === undefined ? undefined : JSON.stringify(body)),
|
|
56
|
+
});
|
|
57
|
+
const text = await response.text();
|
|
58
|
+
let parsed = {};
|
|
59
|
+
try {
|
|
60
|
+
parsed = text ? JSON.parse(text) : {};
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
parsed = { message: text.slice(0, 200) };
|
|
64
|
+
}
|
|
65
|
+
return { status: response.status, body: parsed };
|
|
66
|
+
}
|
|
67
|
+
function fail(answer, doing) {
|
|
68
|
+
if (answer.status === 401) {
|
|
69
|
+
console.error((0, colour_js_1.red)(`${doing}: not allowed. Sign in with cbx sign-in, and check you own this repository.`));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.error((0, colour_js_1.red)(`${doing}: ${answer.body.message ?? answer.body.error ?? `failed (${answer.status})`}`));
|
|
73
|
+
}
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
const list = (text) => text === undefined ? undefined : text.split(",").map((one) => one.trim()).filter(Boolean).map((e) => (e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
|
|
77
|
+
/** Rules named on the command line, for create and settings. */
|
|
78
|
+
function settingsFrom(parsed) {
|
|
79
|
+
const settings = {};
|
|
80
|
+
const allowed = list(flag(parsed, "allow"));
|
|
81
|
+
const blocked = list(flag(parsed, "block"));
|
|
82
|
+
const nested = list(flag(parsed, "nested"));
|
|
83
|
+
if (allowed)
|
|
84
|
+
settings.allowed = allowed;
|
|
85
|
+
if (blocked)
|
|
86
|
+
settings.blocked = blocked;
|
|
87
|
+
if (nested)
|
|
88
|
+
settings.nested = nested;
|
|
89
|
+
const manifest = flag(parsed, "manifest");
|
|
90
|
+
if (manifest !== undefined)
|
|
91
|
+
settings.manifest = manifest === "none" ? null : manifest;
|
|
92
|
+
const fields = flag(parsed, "fields");
|
|
93
|
+
if (fields !== undefined)
|
|
94
|
+
settings.manifestFields = fields.split(",").map((f) => f.trim()).filter(Boolean);
|
|
95
|
+
for (const [name, key] of [["max-files", "maxFiles"], ["max-ratio", "maxRatio"], ["max-parts", "maxParts"]]) {
|
|
96
|
+
const value = flag(parsed, name);
|
|
97
|
+
if (value !== undefined)
|
|
98
|
+
settings[key] = Number(value);
|
|
99
|
+
}
|
|
100
|
+
const maxPart = flag(parsed, "max-part");
|
|
101
|
+
if (maxPart !== undefined)
|
|
102
|
+
settings.maxPartBytes = parseSize(maxPart);
|
|
103
|
+
const uploads = flag(parsed, "uploads");
|
|
104
|
+
if (uploads !== undefined)
|
|
105
|
+
settings.uploads = uploads;
|
|
106
|
+
return settings;
|
|
107
|
+
}
|
|
108
|
+
function parseSize(text) {
|
|
109
|
+
const match = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/i.exec(text.trim());
|
|
110
|
+
if (!match)
|
|
111
|
+
return Number.NaN;
|
|
112
|
+
const unit = (match[2] ?? "b").toLowerCase();
|
|
113
|
+
const factor = unit === "gb" ? 1024 ** 3 : unit === "mb" ? 1024 ** 2 : unit === "kb" ? 1024 : 1;
|
|
114
|
+
return Math.round(Number(match[1]) * factor);
|
|
115
|
+
}
|
|
116
|
+
function bytes(n) {
|
|
117
|
+
if (n === null || n === undefined)
|
|
118
|
+
return "—";
|
|
119
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
120
|
+
let value = n;
|
|
121
|
+
let unit = 0;
|
|
122
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
123
|
+
value /= 1024;
|
|
124
|
+
unit += 1;
|
|
125
|
+
}
|
|
126
|
+
return `${value.toFixed(unit ? 1 : 0)} ${units[unit]}`;
|
|
127
|
+
}
|
|
128
|
+
function printRepo(view) {
|
|
129
|
+
console.log(`${(0, colour_js_1.bold)(view.name)} ${(0, colour_js_1.dim)(view.repo)}`);
|
|
130
|
+
if (view.about)
|
|
131
|
+
console.log((0, colour_js_1.dim)(view.about));
|
|
132
|
+
console.log(`Address ${(0, colour_js_1.accent)(view.base)}`);
|
|
133
|
+
console.log(`Public key ${view.publicKey}`);
|
|
134
|
+
console.log(`Uploads ${view.rules.uploads === "anyone" ? "any CodeRook account" : "the owner only"}`);
|
|
135
|
+
console.log(`Allowed ${view.rules.allowed.join(" ") || "(nothing)"}`);
|
|
136
|
+
if (view.rules.nested.length)
|
|
137
|
+
console.log(`Opened too ${view.rules.nested.join(" ")} (files inside may have no extension)`);
|
|
138
|
+
console.log(`Manifest ${view.rules.manifest ? `${view.rules.manifest}, matching ${view.rules.manifestFields.join(", ")}` : "none"}`);
|
|
139
|
+
console.log(`Limits ${view.rules.maxFiles.toLocaleString()} files, ${bytes(view.rules.maxPartBytes)} a part, ${view.rules.maxParts} parts, unpacked ≤ ${view.rules.maxRatio}× the zip`);
|
|
140
|
+
console.log((0, colour_js_1.dim)(`Never allowed: ${view.rules.blocked.join(" ")}`));
|
|
141
|
+
}
|
|
142
|
+
/* ------------------------------------------------------------------ subcommands */
|
|
143
|
+
async function create(parsed) {
|
|
144
|
+
const slug = parsed.positional[1];
|
|
145
|
+
const name = flag(parsed, "name");
|
|
146
|
+
if (!slug || !name) {
|
|
147
|
+
console.error((0, colour_js_1.red)('Usage: cbx mods create <repo> --name "Game name" [--allow .png,.json] [--nested .pak] [--manifest mod.json] [--uploads anyone|owner]'));
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
const answer = await call(ROOT, "POST", { repo: slug, name, about: flag(parsed, "about"), settings: settingsFrom(parsed) });
|
|
151
|
+
if (answer.status !== 201)
|
|
152
|
+
return fail(answer, "Creating the repository");
|
|
153
|
+
console.log((0, colour_js_1.green)(`Created ${answer.body.repo}.`));
|
|
154
|
+
printRepo(answer.body);
|
|
155
|
+
console.log((0, colour_js_1.dim)("\nBuild the public key into your game to check the catalogue's signature, and register your\n") +
|
|
156
|
+
(0, colour_js_1.dim)("game's build secret with: ") + (0, colour_js_1.accent)(`cbx mods builds ${answer.body.repo} --register <file>`));
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
async function repos() {
|
|
160
|
+
const answer = await call(`${ROOT}/mine`);
|
|
161
|
+
if (answer.status !== 200)
|
|
162
|
+
return fail(answer, "Listing your repositories");
|
|
163
|
+
if (!answer.body.repos.length) {
|
|
164
|
+
console.log((0, colour_js_1.dim)("You have no mod repositories. Create one with: ") + (0, colour_js_1.accent)('cbx mods create <repo> --name "Game"'));
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
for (const view of answer.body.repos)
|
|
168
|
+
console.log(`${(0, colour_js_1.accent)(view.repo.padEnd(24))} ${view.name} ${(0, colour_js_1.dim)(view.base)}`);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
async function info(parsed) {
|
|
172
|
+
const slug = parsed.positional[1];
|
|
173
|
+
if (!slug) {
|
|
174
|
+
console.error((0, colour_js_1.red)("Usage: cbx mods info <repo>"));
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
const answer = await call(`${ROOT}/${encodeURIComponent(slug)}/info`, "GET", undefined, undefined, false);
|
|
178
|
+
if (answer.status !== 200)
|
|
179
|
+
return fail(answer, `Reading ${slug}`);
|
|
180
|
+
printRepo(answer.body);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
async function settings(parsed) {
|
|
184
|
+
const slug = parsed.positional[1];
|
|
185
|
+
if (!slug) {
|
|
186
|
+
console.error((0, colour_js_1.red)("Usage: cbx mods settings <repo> [--allow …] [--uploads anyone|owner] …"));
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
const changes = settingsFrom(parsed);
|
|
190
|
+
const name = flag(parsed, "name");
|
|
191
|
+
const about = flag(parsed, "about");
|
|
192
|
+
const body = {};
|
|
193
|
+
if (Object.keys(changes).length)
|
|
194
|
+
body.settings = changes;
|
|
195
|
+
if (name !== undefined)
|
|
196
|
+
body.name = name;
|
|
197
|
+
if (about !== undefined)
|
|
198
|
+
body.about = about;
|
|
199
|
+
if (has(parsed, "disable"))
|
|
200
|
+
body.disabled = true;
|
|
201
|
+
if (has(parsed, "enable"))
|
|
202
|
+
body.disabled = false;
|
|
203
|
+
const route = `${ROOT}/${encodeURIComponent(slug)}/admin/settings`;
|
|
204
|
+
const answer = Object.keys(body).length ? await call(route, "PUT", body) : await call(route);
|
|
205
|
+
if (answer.status !== 200)
|
|
206
|
+
return fail(answer, `Settings for ${slug}`);
|
|
207
|
+
if (Object.keys(body).length)
|
|
208
|
+
console.log((0, colour_js_1.green)("Saved."));
|
|
209
|
+
printRepo(answer.body);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
async function builds(parsed) {
|
|
213
|
+
const slug = parsed.positional[1];
|
|
214
|
+
if (!slug) {
|
|
215
|
+
console.error((0, colour_js_1.red)("Usage: cbx mods builds <repo> [--register <file> | --new | --off <id>]"));
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
const base = `${ROOT}/${encodeURIComponent(slug)}/admin/builds`;
|
|
219
|
+
const off = flag(parsed, "off");
|
|
220
|
+
if (off) {
|
|
221
|
+
const answer = await call(`${base}/${encodeURIComponent(off)}`, "DELETE");
|
|
222
|
+
if (answer.status !== 200)
|
|
223
|
+
return fail(answer, `Turning off ${off}`);
|
|
224
|
+
console.log((0, colour_js_1.green)(`Build ${off} is off: its signatures are refused from now on.`));
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
const register = flag(parsed, "register");
|
|
228
|
+
const expires = flag(parsed, "expires");
|
|
229
|
+
if (register || has(parsed, "new")) {
|
|
230
|
+
let body = {};
|
|
231
|
+
if (register) {
|
|
232
|
+
/* The secret is read from a file and sent straight to the service; it is never printed or passed on a command line. */
|
|
233
|
+
const secret = (await (0, promises_1.readFile)(register, "utf8")).trim().toLowerCase();
|
|
234
|
+
const hexSecret = /^[0-9a-f]{64}$/.test(secret) ? secret : Buffer.from(await (0, promises_1.readFile)(register)).length === 32 ? Buffer.from(await (0, promises_1.readFile)(register)).toString("hex") : "";
|
|
235
|
+
if (!hexSecret) {
|
|
236
|
+
console.error((0, colour_js_1.red)("The file must hold the 32-byte build secret, raw or as 64 hex characters."));
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
body = { secret: hexSecret };
|
|
240
|
+
}
|
|
241
|
+
if (expires)
|
|
242
|
+
body.expires = expires;
|
|
243
|
+
const answer = await call(base, "POST", body);
|
|
244
|
+
if (answer.status !== 201)
|
|
245
|
+
return fail(answer, "Registering the build");
|
|
246
|
+
console.log((0, colour_js_1.green)(`Build ${answer.body.build} registered.`));
|
|
247
|
+
if (answer.body.secret) {
|
|
248
|
+
console.log((0, colour_js_1.bold)("Secret (shown once — compile it into this build and keep it out of any repository):"));
|
|
249
|
+
console.log(answer.body.secret);
|
|
250
|
+
}
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
const answer = await call(base);
|
|
254
|
+
if (answer.status !== 200)
|
|
255
|
+
return fail(answer, `Builds of ${slug}`);
|
|
256
|
+
if (!answer.body.builds.length)
|
|
257
|
+
console.log((0, colour_js_1.dim)("No builds registered yet."));
|
|
258
|
+
for (const b of answer.body.builds) {
|
|
259
|
+
console.log(`${b.disabled ? (0, colour_js_1.dim)(b.build) : (0, colour_js_1.accent)(b.build)} ${b.disabled ? (0, colour_js_1.red)("off") : (0, colour_js_1.green)("on ")} ${(0, colour_js_1.dim)(`since ${b.created.slice(0, 10)}${b.expires ? `, until ${b.expires.slice(0, 10)}` : ""}`)}`);
|
|
260
|
+
}
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
function printMod(mod) {
|
|
264
|
+
console.log(`${(0, colour_js_1.bold)(mod.entry.name)} ${(0, colour_js_1.dim)(mod.mod)}${mod.entry.version ? (0, colour_js_1.dim)(` v${mod.entry.version}`) : ""}${mod.hidden ? (0, colour_js_1.red)(" hidden") : ""}`);
|
|
265
|
+
for (const part of mod.parts) {
|
|
266
|
+
const state = part.check.state;
|
|
267
|
+
const colour = state === "live" ? colour_js_1.green : state === "rejected" || state === "error" ? colour_js_1.red : colour_js_1.accent;
|
|
268
|
+
const live = part.live ? (0, colour_js_1.dim)(` live: ${bytes(part.live.size)}, ${part.live.files.toLocaleString()} files`) : (0, colour_js_1.dim)(" not listed");
|
|
269
|
+
console.log(` ${part.part.padEnd(16)} ${colour(state)}${live}`);
|
|
270
|
+
for (const problem of part.check.problems ?? [])
|
|
271
|
+
console.log(` ${(0, colour_js_1.red)("·")} ${problem}`);
|
|
272
|
+
for (const found of part.check.infected ?? [])
|
|
273
|
+
console.log(` ${(0, colour_js_1.red)("✗")} ${found.path}: ${found.signature}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function mine(parsed) {
|
|
277
|
+
const slug = flag(parsed, "repo") ?? parsed.positional[1];
|
|
278
|
+
if (!slug) {
|
|
279
|
+
console.error((0, colour_js_1.red)("Usage: cbx mods list <repo>"));
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
const route = has(parsed, "all") ? `${ROOT}/${encodeURIComponent(slug)}/admin/mods` : `${ROOT}/${encodeURIComponent(slug)}/my/mods`;
|
|
283
|
+
const answer = await call(route);
|
|
284
|
+
if (answer.status !== 200)
|
|
285
|
+
return fail(answer, `Mods in ${slug}`);
|
|
286
|
+
if (!answer.body.mods.length)
|
|
287
|
+
console.log((0, colour_js_1.dim)("No mods yet. Publish one with: ") + (0, colour_js_1.accent)(`cbx mods publish <folder> --repo ${slug}`));
|
|
288
|
+
for (const mod of answer.body.mods)
|
|
289
|
+
printMod(mod);
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
async function sha256File(file) {
|
|
293
|
+
const hash = (0, node_crypto_1.createHash)("sha256");
|
|
294
|
+
for await (const chunk of (0, node_fs_1.createReadStream)(file))
|
|
295
|
+
hash.update(chunk);
|
|
296
|
+
return hash.digest("hex");
|
|
297
|
+
}
|
|
298
|
+
async function readChunk(file, start, length) {
|
|
299
|
+
const parts = [];
|
|
300
|
+
for await (const chunk of (0, node_fs_1.createReadStream)(file, { start, end: start + length - 1 }))
|
|
301
|
+
parts.push(chunk);
|
|
302
|
+
return new Uint8Array(Buffer.concat(parts));
|
|
303
|
+
}
|
|
304
|
+
async function publish(parsed) {
|
|
305
|
+
const target = parsed.positional[1];
|
|
306
|
+
const slug = flag(parsed, "repo");
|
|
307
|
+
if (!target || !slug) {
|
|
308
|
+
console.error((0, colour_js_1.red)("Usage: cbx mods publish <folder|zip> --repo <repo> [--part <id>] [--id <mod>] [--name <text>] [--version <v>]"));
|
|
309
|
+
return 1;
|
|
310
|
+
}
|
|
311
|
+
const source = node_path_1.default.resolve(target);
|
|
312
|
+
if (!(0, node_fs_1.existsSync)(source)) {
|
|
313
|
+
console.error((0, colour_js_1.red)(`${target} does not exist.`));
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
316
|
+
const base = `${ROOT}/${encodeURIComponent(slug)}`;
|
|
317
|
+
const rules = await call(`${base}/info`, "GET", undefined, undefined, false);
|
|
318
|
+
if (rules.status !== 200)
|
|
319
|
+
return fail(rules, `Reading ${slug}`);
|
|
320
|
+
const manifestName = rules.body.rules.manifest;
|
|
321
|
+
/* The mod's identity: flags first, then the manifest in the folder, then the folder's name. */
|
|
322
|
+
const isZip = (await (0, promises_1.stat)(source)).isFile();
|
|
323
|
+
let manifest = {};
|
|
324
|
+
if (!isZip && manifestName && (0, node_fs_1.existsSync)(node_path_1.default.join(source, manifestName))) {
|
|
325
|
+
try {
|
|
326
|
+
manifest = JSON.parse(await (0, promises_1.readFile)(node_path_1.default.join(source, manifestName), "utf8"));
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
console.error((0, colour_js_1.red)(`${manifestName} in that folder is not valid JSON.`));
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const folderName = node_path_1.default.basename(source).replace(/\.zip$/i, "");
|
|
334
|
+
const id = (flag(parsed, "id") ?? manifest.id ?? folderName.toLowerCase().replace(/[^a-z0-9._-]+/g, "-")).toLowerCase();
|
|
335
|
+
const name = flag(parsed, "name") ?? manifest.name ?? folderName;
|
|
336
|
+
const folder = flag(parsed, "folder") ?? manifest.folder ?? folderName.replace(/[^A-Za-z0-9 ._-]+/g, "-");
|
|
337
|
+
const version = flag(parsed, "version") ?? manifest.version;
|
|
338
|
+
const partId = (flag(parsed, "part") ?? "main").toLowerCase();
|
|
339
|
+
/* Keep the parts the mod already declares, and add this one. */
|
|
340
|
+
const existing = await call(`${base}/my/mods/${encodeURIComponent(id)}`);
|
|
341
|
+
const parts = existing.status === 200 ? [...existing.body.entry.parts] : [];
|
|
342
|
+
if (!parts.some((p) => p.id === partId))
|
|
343
|
+
parts.push({ id: partId, name: flag(parsed, "part-name") ?? partId });
|
|
344
|
+
const entry = {
|
|
345
|
+
id, name, folder, parts,
|
|
346
|
+
...(version ? { version } : {}),
|
|
347
|
+
...(flag(parsed, "about") ? { about: flag(parsed, "about") } : existing.status === 200 && existing.body.entry.about ? { about: existing.body.entry.about } : {}),
|
|
348
|
+
...(flag(parsed, "notice") ? { notice: flag(parsed, "notice") } : {}),
|
|
349
|
+
};
|
|
350
|
+
const saved = await call(`${base}/my/mods`, "POST", entry);
|
|
351
|
+
if (saved.status !== 200 && saved.status !== 201)
|
|
352
|
+
return fail(saved, "Saving the mod's entry");
|
|
353
|
+
/* A folder is zipped here, with the manifest written to match the entry; a zip is sent as it is. */
|
|
354
|
+
let zipPath = source;
|
|
355
|
+
let scratch = null;
|
|
356
|
+
try {
|
|
357
|
+
if (!isZip) {
|
|
358
|
+
scratch = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "cbx-mod-"));
|
|
359
|
+
zipPath = node_path_1.default.join(scratch, `${id}-${partId}.zip`);
|
|
360
|
+
const files = await (0, zip_writer_js_1.listFiles)(source, (relative) => relative === ".git" || relative === ".coderook" || relative.endsWith(".DS_Store"));
|
|
361
|
+
const extra = manifestName
|
|
362
|
+
? [{ path: manifestName, bytes: new TextEncoder().encode(JSON.stringify({ ...manifest, id, name, folder, ...(version ? { version } : {}) }, null, 2) + "\n") }]
|
|
363
|
+
: [];
|
|
364
|
+
node_process_1.default.stdout.write((0, colour_js_1.dim)(`Zipping ${files.length.toLocaleString()} files… `));
|
|
365
|
+
const zipped = await (0, zip_writer_js_1.writeZip)(source, files, zipPath, extra);
|
|
366
|
+
console.log((0, colour_js_1.dim)(bytes(zipped.bytes)));
|
|
367
|
+
}
|
|
368
|
+
const size = (await (0, promises_1.stat)(zipPath)).size;
|
|
369
|
+
if (size > rules.body.rules.maxPartBytes) {
|
|
370
|
+
console.error((0, colour_js_1.red)(`The part is ${bytes(size)}; ${slug} takes up to ${bytes(rules.body.rules.maxPartBytes)} a part. Split it with --part.`));
|
|
371
|
+
return 1;
|
|
372
|
+
}
|
|
373
|
+
const sha256 = await sha256File(zipPath);
|
|
374
|
+
const begun = await call(`${base}/my/mods/${encodeURIComponent(id)}/parts/${encodeURIComponent(partId)}`, "PUT", { size, sha256 });
|
|
375
|
+
if (begun.status !== 201)
|
|
376
|
+
return fail(begun, "Starting the upload");
|
|
377
|
+
for (let n = 1; n <= begun.body.chunks; n += 1) {
|
|
378
|
+
const start = (n - 1) * begun.body.chunkBytes;
|
|
379
|
+
const chunk = await readChunk(zipPath, start, Math.min(begun.body.chunkBytes, size - start));
|
|
380
|
+
let sent = null;
|
|
381
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
382
|
+
try {
|
|
383
|
+
sent = await call(`${base}/my/mods/${encodeURIComponent(id)}/parts/${encodeURIComponent(partId)}/chunks/${n}?upload=${encodeURIComponent(begun.body.upload)}`, "PUT", undefined, chunk);
|
|
384
|
+
if (sent.status === 200)
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
/* tried again below */
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!sent || sent.status !== 200)
|
|
392
|
+
return fail(sent ?? { status: 0, body: { message: "the connection kept failing" } }, `Sending chunk ${n}`);
|
|
393
|
+
node_process_1.default.stdout.write(`\r${(0, colour_js_1.dim)(`Sent ${n} of ${begun.body.chunks} (${bytes(Math.min(size, n * begun.body.chunkBytes))} of ${bytes(size)})`)} `);
|
|
394
|
+
}
|
|
395
|
+
console.log("");
|
|
396
|
+
const done = await call(`${base}/my/mods/${encodeURIComponent(id)}/parts/${encodeURIComponent(partId)}/complete`, "POST", { upload: begun.body.upload });
|
|
397
|
+
if (done.status !== 202)
|
|
398
|
+
return fail(done, "Finishing the upload");
|
|
399
|
+
console.log((0, colour_js_1.green)(`Uploaded ${id}/${partId}.`) + (0, colour_js_1.dim)(" It is listed once its checks pass."));
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
if (scratch)
|
|
403
|
+
await (0, promises_1.rm)(scratch, { recursive: true, force: true });
|
|
404
|
+
}
|
|
405
|
+
if (has(parsed, "no-wait")) {
|
|
406
|
+
console.log((0, colour_js_1.dim)("See how the checks went with: ") + (0, colour_js_1.accent)(`cbx mods list ${slug}`));
|
|
407
|
+
return 0;
|
|
408
|
+
}
|
|
409
|
+
node_process_1.default.stdout.write((0, colour_js_1.dim)("Checking (structure, file types, malware scan)… "));
|
|
410
|
+
const started = Date.now();
|
|
411
|
+
for (;;) {
|
|
412
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
413
|
+
const now = await call(`${base}/my/mods/${encodeURIComponent(id)}`);
|
|
414
|
+
const part = now.body.parts?.find((p) => p.part === partId);
|
|
415
|
+
const state = part?.check.state ?? "queued";
|
|
416
|
+
if (!["queued", "checking"].includes(state)) {
|
|
417
|
+
console.log("");
|
|
418
|
+
printMod(now.body);
|
|
419
|
+
return state === "live" ? 0 : 1;
|
|
420
|
+
}
|
|
421
|
+
if (Date.now() - started > 30 * 60 * 1000) {
|
|
422
|
+
console.log("");
|
|
423
|
+
console.log((0, colour_js_1.dim)("Still checking after 30 minutes; it carries on without you. Look later with ") + (0, colour_js_1.accent)(`cbx mods list ${slug}`));
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async function remove(parsed) {
|
|
429
|
+
const id = parsed.positional[1];
|
|
430
|
+
const slug = flag(parsed, "repo");
|
|
431
|
+
if (!id || !slug) {
|
|
432
|
+
console.error((0, colour_js_1.red)("Usage: cbx mods remove <mod> --repo <repo> [--yes]"));
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
if (!has(parsed, "yes", "y")) {
|
|
436
|
+
const prompt = (0, promises_2.createInterface)({ input: node_process_1.default.stdin, output: node_process_1.default.stdout });
|
|
437
|
+
const typed = (await prompt.question(`This takes ${id} out of ${slug} and deletes its files. Type its id to confirm: `)).trim();
|
|
438
|
+
prompt.close();
|
|
439
|
+
if (typed !== id) {
|
|
440
|
+
console.error((0, colour_js_1.red)("That did not match. Nothing was removed."));
|
|
441
|
+
return 1;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const owner = has(parsed, "as-owner") ? "admin" : "my";
|
|
445
|
+
const answer = await call(`${ROOT}/${encodeURIComponent(slug)}/${owner}/mods/${encodeURIComponent(id)}`, "DELETE");
|
|
446
|
+
if (answer.status !== 200)
|
|
447
|
+
return fail(answer, `Removing ${id}`);
|
|
448
|
+
console.log((0, colour_js_1.green)(`Removed ${id}.`));
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
async function block(parsed) {
|
|
452
|
+
const account = parsed.positional[1];
|
|
453
|
+
const slug = flag(parsed, "repo");
|
|
454
|
+
if (!account || !slug) {
|
|
455
|
+
console.error((0, colour_js_1.red)("Usage: cbx mods block <username> --repo <repo> [--take-down] | --undo"));
|
|
456
|
+
return 1;
|
|
457
|
+
}
|
|
458
|
+
const verb = has(parsed, "undo") ? "unblock" : "block";
|
|
459
|
+
const answer = await call(`${ROOT}/${encodeURIComponent(slug)}/admin/uploaders/${encodeURIComponent(account)}/${verb}`, "POST", { takeDown: has(parsed, "take-down") });
|
|
460
|
+
if (answer.status !== 200)
|
|
461
|
+
return fail(answer, `${verb === "block" ? "Blocking" : "Unblocking"} ${account}`);
|
|
462
|
+
console.log((0, colour_js_1.green)(verb === "block" ? `${account} can no longer upload to ${slug}${answer.body.hidden ? ", and their mods are hidden" : ""}.` : `${account} can upload to ${slug} again.`));
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
async function reports(parsed) {
|
|
466
|
+
const slug = parsed.positional[1];
|
|
467
|
+
if (!slug) {
|
|
468
|
+
console.error((0, colour_js_1.red)("Usage: cbx mods reports <repo>"));
|
|
469
|
+
return 1;
|
|
470
|
+
}
|
|
471
|
+
const answer = await call(`${ROOT}/${encodeURIComponent(slug)}/admin/reports`);
|
|
472
|
+
if (answer.status !== 200)
|
|
473
|
+
return fail(answer, `Reports for ${slug}`);
|
|
474
|
+
if (!answer.body.reports.length)
|
|
475
|
+
console.log((0, colour_js_1.dim)("No reports."));
|
|
476
|
+
for (const r of answer.body.reports) {
|
|
477
|
+
console.log(`${(0, colour_js_1.dim)(r.created.slice(0, 16).replace("T", " "))} ${(0, colour_js_1.accent)(r.modId)}${r.partId ? (0, colour_js_1.dim)(`/${r.partId}`) : ""} ${(0, colour_js_1.bold)(r.reason)}${r.detail ? ` ${r.detail}` : ""}`);
|
|
478
|
+
}
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
async function deleteRepo(parsed) {
|
|
482
|
+
const slug = parsed.positional[1];
|
|
483
|
+
if (!slug) {
|
|
484
|
+
console.error((0, colour_js_1.red)("Usage: cbx mods delete <repo>"));
|
|
485
|
+
return 1;
|
|
486
|
+
}
|
|
487
|
+
const prompt = (0, promises_2.createInterface)({ input: node_process_1.default.stdin, output: node_process_1.default.stdout });
|
|
488
|
+
const typed = has(parsed, "yes") ? slug : (await prompt.question(`This deletes ${slug}, every mod in it and every build registration, for good. Type ${slug} to confirm: `)).trim();
|
|
489
|
+
prompt.close();
|
|
490
|
+
if (typed !== slug) {
|
|
491
|
+
console.error((0, colour_js_1.red)("That did not match. Nothing was deleted."));
|
|
492
|
+
return 1;
|
|
493
|
+
}
|
|
494
|
+
const answer = await call(`${ROOT}/${encodeURIComponent(slug)}`, "DELETE", { confirm: slug });
|
|
495
|
+
if (answer.status !== 200)
|
|
496
|
+
return fail(answer, `Deleting ${slug}`);
|
|
497
|
+
console.log((0, colour_js_1.green)(`Deleted ${slug}.`));
|
|
498
|
+
return 0;
|
|
499
|
+
}
|
|
500
|
+
async function commandMods(parsed) {
|
|
501
|
+
const sub = parsed.positional[0];
|
|
502
|
+
switch (sub) {
|
|
503
|
+
case "create": return create(parsed);
|
|
504
|
+
case "repos": return repos();
|
|
505
|
+
case "info": return info(parsed);
|
|
506
|
+
case "settings": return settings(parsed);
|
|
507
|
+
case "builds": return builds(parsed);
|
|
508
|
+
case "list": return mine(parsed);
|
|
509
|
+
case "publish": return publish(parsed);
|
|
510
|
+
case "remove": return remove(parsed);
|
|
511
|
+
case "block": return block(parsed);
|
|
512
|
+
case "reports": return reports(parsed);
|
|
513
|
+
case "delete": return deleteRepo(parsed);
|
|
514
|
+
default:
|
|
515
|
+
console.error((0, colour_js_1.red)(sub ? `cbx mods has no "${sub}". ` : "") + "See: " + (0, colour_js_1.accent)("cbx help mods"));
|
|
516
|
+
return 1;
|
|
517
|
+
}
|
|
518
|
+
}
|