@coderook/cli 0.33.1 → 0.34.1

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.
@@ -0,0 +1,932 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.DEFAULT_TARGETS = exports.ALL_TARGETS = void 0;
40
+ exports.parseTargets = parseTargets;
41
+ exports.thisHost = thisHost;
42
+ exports.detectTools = detectTools;
43
+ exports.describeProject = describeProject;
44
+ exports.wslPath = wslPath;
45
+ exports.planBuild = planBuild;
46
+ exports.markExecutables = markExecutables;
47
+ exports.fileFromTar = fileFromTar;
48
+ exports.outputsOf = outputsOf;
49
+ exports.commandBuild = commandBuild;
50
+ /**
51
+ * `cbx build` — make a project's executables for Windows, Linux and macOS on
52
+ * the machine you are sitting at.
53
+ *
54
+ * Actions could already run a command, on CodeRook's Linux sandbox or on a
55
+ * machine that ran `cbx runner` — and each of those built for whatever it was.
56
+ * Shipping one app for three systems meant three machines. Most toolchains do
57
+ * not need that: Go, Bun, Deno and .NET produce every platform's binary from
58
+ * any one of them, Rust does with zig, and Electron does Windows and Linux
59
+ * through a container. What they lacked was something that knows which is
60
+ * which on *this* machine and says plainly what it cannot do.
61
+ *
62
+ * So this plans first and builds second. Every target gets a route — native,
63
+ * cross-compiled, in Docker, or in WSL — chosen from what is actually
64
+ * installed, or a sentence saying why it cannot be built here and where it
65
+ * can. Nothing is uploaded unless `--ship` is given; the output lands in
66
+ * `.coderook/build/<target>/` — beside the folder's link, which CodeRook never
67
+ * uploads and git is told to ignore. Not `dist/`: Electron apps commonly
68
+ * package `dist/**`, and every installer would then carry the last ones.
69
+ *
70
+ * A project can describe itself in `coderook.build.json` at its root, which is
71
+ * saved with the project like any other file. Without one, the toolchain is
72
+ * worked out from the files that are there.
73
+ */
74
+ const node_child_process_1 = require("node:child_process");
75
+ const node_crypto_1 = require("node:crypto");
76
+ const node_zlib_1 = require("node:zlib");
77
+ const node_os_1 = __importDefault(require("node:os"));
78
+ const node_fs_1 = require("node:fs");
79
+ const promises_1 = require("node:fs/promises");
80
+ const node_path_1 = __importDefault(require("node:path"));
81
+ const node_process_1 = __importDefault(require("node:process"));
82
+ const portable_python_js_1 = require("./portable_python.js");
83
+ const ESC = String.fromCharCode(27);
84
+ const colour = node_process_1.default.stdout.isTTY && !node_process_1.default.env.NO_COLOR;
85
+ const paint = (code) => (value) => (colour ? `${ESC}[${code}m${value}${ESC}[0m` : value);
86
+ const dim = paint("2");
87
+ const bold = paint("1");
88
+ const red = paint("31");
89
+ const green = paint("32");
90
+ const accent = paint("33");
91
+ exports.ALL_TARGETS = [
92
+ "windows-x64", "windows-arm64", "linux-x64", "linux-arm64", "macos-x64", "macos-arm64",
93
+ ];
94
+ exports.DEFAULT_TARGETS = ["windows-x64", "linux-x64", "macos-arm64"];
95
+ const ALIASES = {
96
+ all: ["windows-x64", "linux-x64", "macos-x64", "macos-arm64"],
97
+ win: ["windows-x64"], windows: ["windows-x64"],
98
+ linux: ["linux-x64"],
99
+ mac: ["macos-arm64"], macos: ["macos-arm64"], darwin: ["macos-arm64"],
100
+ };
101
+ /** `win,linux,mac`, `windows-x64`, `all`: what was asked for, as targets. */
102
+ function parseTargets(text) {
103
+ const found = [];
104
+ for (const word of text.split(/[\s,]+/).map((one) => one.trim().toLowerCase()).filter(Boolean)) {
105
+ const normal = word.replace(/^win-/, "windows-").replace(/^mac-|^darwin-/, "macos-").replace(/-amd64$/, "-x64").replace(/-aarch64$/, "-arm64");
106
+ const named = ALIASES[normal] ?? (exports.ALL_TARGETS.includes(normal) ? [normal] : null);
107
+ if (!named)
108
+ throw new Error(`"${word}" is not a target. Use ${exports.ALL_TARGETS.join(", ")}, or win, linux, mac, all.`);
109
+ for (const target of named)
110
+ if (!found.includes(target))
111
+ found.push(target);
112
+ }
113
+ return found;
114
+ }
115
+ const osOf = (target) => target.split("-")[0];
116
+ const archOf = (target) => target.split("-")[1];
117
+ const exe = (target) => (osOf(target) === "windows" ? ".exe" : "");
118
+ function thisHost() {
119
+ const platform = node_process_1.default.platform === "win32" ? "windows" : node_process_1.default.platform === "darwin" ? "macos" : "linux";
120
+ return { os: platform, arch: node_process_1.default.arch === "arm64" ? "arm64" : "x64" };
121
+ }
122
+ /** Whether a command answers within a few seconds, without a shell. */
123
+ function answers(program, args, timeoutMs = 8000) {
124
+ return new Promise((done) => {
125
+ let child;
126
+ try {
127
+ child = (0, node_child_process_1.spawn)(program, args, { stdio: "ignore", windowsHide: true });
128
+ }
129
+ catch {
130
+ done(false);
131
+ return;
132
+ }
133
+ const timer = setTimeout(() => {
134
+ child.kill();
135
+ done(false);
136
+ }, timeoutMs);
137
+ child.on("error", () => {
138
+ clearTimeout(timer);
139
+ done(false);
140
+ });
141
+ child.on("exit", (code) => {
142
+ clearTimeout(timer);
143
+ done(code === 0);
144
+ });
145
+ });
146
+ }
147
+ /** What this machine has, asked of each tool rather than guessed from PATH. */
148
+ async function detectTools(host = thisHost()) {
149
+ const checks = [
150
+ ["go", answers("go", ["version"])],
151
+ ["bun", answers("bun", ["--version"])],
152
+ ["deno", answers("deno", ["--version"])],
153
+ ["cargo", answers("cargo", ["--version"])],
154
+ ["rustup", answers("rustup", ["--version"])],
155
+ ["zigbuild", answers("cargo", ["zigbuild", "--help"])],
156
+ ["cross", answers("cross", ["--version"])],
157
+ ["dotnet", answers("dotnet", ["--version"])],
158
+ /* The daemon, not the client: a stopped Docker Desktop answers --version. */
159
+ ["docker", answers("docker", ["info", "--format", "{{.ServerVersion}}"], 15000)],
160
+ ["pyinstaller", answers("pyinstaller", ["--version"], 15000)],
161
+ ["uv", answers("uv", ["--version"])],
162
+ ["python", answers(host.os === "windows" ? "python" : "python3", ["-m", "pip", "--version"], 15000)],
163
+ ["wine", host.os === "windows" ? Promise.resolve(false) : answers("wine", ["--version"])],
164
+ ["node", answers(node_process_1.default.execPath, ["--version"])],
165
+ ];
166
+ if (host.os === "windows") {
167
+ checks.push(["wsl", answers("wsl", ["-e", "true"], 20000)]);
168
+ checks.push(["wslPython", answers("wsl", ["-e", "python3", "-m", "PyInstaller", "--version"], 30000)]);
169
+ }
170
+ const tools = {};
171
+ await Promise.all(checks.map(async ([name, check]) => {
172
+ tools[name] = await check;
173
+ }));
174
+ return tools;
175
+ }
176
+ const CONFIG_FILE = "coderook.build.json";
177
+ async function readJson(file) {
178
+ try {
179
+ return JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
185
+ const safeName = (name) => name.replace(/^@[^/]+\//, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-|-$/g, "") || "app";
186
+ /** The preset, name and entry point, from the config and the files present. */
187
+ async function describeProject(folder) {
188
+ const here = (file) => (0, node_fs_1.existsSync)(node_path_1.default.join(folder, file));
189
+ let config = {};
190
+ if (here(CONFIG_FILE)) {
191
+ const raw = await readJson(node_path_1.default.join(folder, CONFIG_FILE));
192
+ if (!raw)
193
+ throw new Error(`${CONFIG_FILE} is not valid JSON.`);
194
+ config = raw;
195
+ }
196
+ const pkg = await readJson(node_path_1.default.join(folder, "package.json"));
197
+ const entries = (await (0, promises_1.readdir)(folder).catch(() => []));
198
+ const csproj = entries.find((file) => file.endsWith(".csproj"));
199
+ const spec = entries.find((file) => file.endsWith(".spec"));
200
+ const dependencies = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
201
+ let preset = config.preset ?? (config.command ? "custom" : null);
202
+ if (!preset) {
203
+ if (here("go.mod"))
204
+ preset = "go";
205
+ else if (here("Cargo.toml"))
206
+ preset = "rust";
207
+ else if (csproj)
208
+ preset = "dotnet";
209
+ else if (dependencies["electron-builder"] || dependencies["electron"])
210
+ preset = "electron";
211
+ else if (here("deno.json") || here("deno.jsonc"))
212
+ preset = "deno";
213
+ else if (here("bun.lock") || here("bun.lockb") || here("bunfig.toml"))
214
+ preset = "bun";
215
+ else if (spec || (await (0, promises_1.readFile)(node_path_1.default.join(folder, "requirements.txt"), "utf8").catch(() => "")).match(/pyinstaller/i))
216
+ preset = "pyinstaller";
217
+ else if (here("requirements.txt") || here("pyproject.toml") || here("main.py") || here("app.py"))
218
+ preset = "python";
219
+ }
220
+ let name = config.name ?? "";
221
+ let entry = config.entry ?? null;
222
+ let windowsOnly = false;
223
+ if (preset === "go" && !name) {
224
+ const module = (await (0, promises_1.readFile)(node_path_1.default.join(folder, "go.mod"), "utf8").catch(() => "")).match(/^module\s+(\S+)/m)?.[1];
225
+ name = module?.split("/").pop() ?? "";
226
+ entry ??= ".";
227
+ }
228
+ if (preset === "rust" && !name) {
229
+ const cargo = await (0, promises_1.readFile)(node_path_1.default.join(folder, "Cargo.toml"), "utf8").catch(() => "");
230
+ name = cargo.match(/\[package\][^[]*?\bname\s*=\s*"([^"]+)"/)?.[1] ?? "";
231
+ }
232
+ if (preset === "dotnet") {
233
+ entry ??= csproj ?? null;
234
+ if (entry) {
235
+ const text = await (0, promises_1.readFile)(node_path_1.default.join(folder, entry), "utf8").catch(() => "");
236
+ windowsOnly = /<UseWindowsForms>\s*true|<UseWPF>\s*true|<TargetFrameworks?>[^<]*-windows/i.test(text);
237
+ name ||= text.match(/<AssemblyName>([^<]+)</)?.[1] ?? node_path_1.default.basename(entry, ".csproj");
238
+ }
239
+ }
240
+ if (preset === "bun" || preset === "deno") {
241
+ const bin = pkg?.bin;
242
+ entry ??=
243
+ (typeof bin === "string" ? bin : bin && typeof bin === "object" ? String(Object.values(bin)[0]) : null) ??
244
+ (typeof pkg?.module === "string" ? pkg.module : null) ??
245
+ (typeof pkg?.main === "string" ? pkg.main : null) ??
246
+ ["main.ts", "mod.ts", "index.ts", "src/main.ts", "src/index.ts", "index.js"].find(here) ??
247
+ null;
248
+ }
249
+ if (preset === "pyinstaller" || preset === "python") {
250
+ entry ??= spec ?? ["main.py", "app.py", "__main__.py", "src/main.py"].find(here) ?? null;
251
+ }
252
+ if (!name && typeof pkg?.name === "string")
253
+ name = pkg.name;
254
+ /*
255
+ An Electron app is usually compiled before it is packaged, and
256
+ electron-builder packages whatever is there: without this the installer
257
+ holds last week's build, or none. A `build` script that is itself
258
+ electron-builder is the packaging, not the compiling, and is left alone.
259
+ */
260
+ const scripts = (pkg?.scripts ?? {});
261
+ if (preset === "electron" && config.before === undefined && scripts.build && !/electron-builder/.test(scripts.build)) {
262
+ config = { ...config, before: "npm run build" };
263
+ }
264
+ if (preset === "pyinstaller" && !name && entry?.endsWith(".spec"))
265
+ name = node_path_1.default.basename(entry, ".spec");
266
+ const hasBuilder = preset === "electron" ? (0, node_fs_1.existsSync)(node_path_1.default.join(folder, "node_modules", "electron-builder", "cli.js")) : undefined;
267
+ return {
268
+ folder, preset, name: safeName(name || node_path_1.default.basename(folder)), entry, windowsOnly, config,
269
+ ...(hasBuilder === undefined ? {} : { hasBuilder }),
270
+ };
271
+ }
272
+ /** Where fetched toolchain pieces are kept between builds. */
273
+ function cacheDirectory() {
274
+ const base = node_process_1.default.platform === "win32"
275
+ ? node_process_1.default.env.LOCALAPPDATA ?? node_path_1.default.join(node_os_1.default.homedir(), "AppData", "Local")
276
+ : node_process_1.default.env.XDG_CACHE_HOME ?? node_path_1.default.join(node_os_1.default.homedir(), ".cache");
277
+ return node_path_1.default.join(base, "coderook", "build");
278
+ }
279
+ /** A command that runs inside a throwaway container with the project mounted. */
280
+ function inDocker(project, image, inner, volumes = []) {
281
+ return {
282
+ program: "docker",
283
+ args: [
284
+ "run", "--rm",
285
+ "-v", `${project.folder}:/project`,
286
+ ...volumes.flatMap((volume) => ["-v", volume]),
287
+ "-w", "/project",
288
+ image, "sh", "-lc", inner,
289
+ ],
290
+ };
291
+ }
292
+ /** A path inside WSL for a Windows path: `A:\x\y` is `/mnt/a/x/y`. */
293
+ function wslPath(windowsPath) {
294
+ const match = windowsPath.match(/^([A-Za-z]):[\\/](.*)$/);
295
+ if (!match)
296
+ return windowsPath.replace(/\\/g, "/");
297
+ return `/mnt/${match[1].toLowerCase()}/${match[2].replace(/\\/g, "/")}`;
298
+ }
299
+ const sq = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
300
+ const GO_OS = { windows: "windows", linux: "linux", macos: "darwin" };
301
+ const GO_ARCH = { x64: "amd64", arm64: "arm64" };
302
+ const DENO = {
303
+ "windows-x64": "x86_64-pc-windows-msvc", "windows-arm64": "",
304
+ "linux-x64": "x86_64-unknown-linux-gnu", "linux-arm64": "aarch64-unknown-linux-gnu",
305
+ "macos-x64": "x86_64-apple-darwin", "macos-arm64": "aarch64-apple-darwin",
306
+ };
307
+ const DOTNET = {
308
+ "windows-x64": "win-x64", "windows-arm64": "win-arm64", "linux-x64": "linux-x64",
309
+ "linux-arm64": "linux-arm64", "macos-x64": "osx-x64", "macos-arm64": "osx-arm64",
310
+ };
311
+ const RUST = {
312
+ "windows-x64": "x86_64-pc-windows-msvc", "windows-arm64": "aarch64-pc-windows-msvc",
313
+ "linux-x64": "x86_64-unknown-linux-gnu", "linux-arm64": "aarch64-unknown-linux-gnu",
314
+ "macos-x64": "x86_64-apple-darwin", "macos-arm64": "aarch64-apple-darwin",
315
+ };
316
+ const NEEDS = (os) => `run cbx build --target ${os === "macos" ? "mac" : os} on a ${os === "macos" ? "Mac" : os === "windows" ? "Windows PC" : "Linux machine"}, or a cbx runner on one`;
317
+ /**
318
+ * Every target, with how this machine would build it or why it cannot.
319
+ *
320
+ * Pure: the same project, host and tools always give the same plan, which is
321
+ * what lets the choices be tested for machines this one is not.
322
+ */
323
+ function planBuild(project, host, tools, targets) {
324
+ const outRoot = project.config.out ?? node_path_1.default.join(".coderook", "build");
325
+ const extra = project.config.args ?? [];
326
+ const before = project.config.before;
327
+ return targets.map((target) => {
328
+ const out = node_path_1.default.join(outRoot, target);
329
+ const outAbs = node_path_1.default.join(project.folder, out);
330
+ const os_ = osOf(target);
331
+ const arch = archOf(target);
332
+ const native = host.os === os_ && host.arch === arch;
333
+ const sameOs = host.os === os_;
334
+ const file = `${project.name}-${target}${exe(target)}`;
335
+ const pre = before ? [{ shell: before }] : [];
336
+ /* Found by asking the PATH, so a tool installed but not on it reads as absent: say both. */
337
+ const missing = (tool, why = "") => ({ target, ok: false, why: `needs ${tool}${why}, on the PATH this command sees` });
338
+ /*
339
+ Python for any system from any system: the target's own CPython, its
340
+ wheels and the program, in one archive. Needs uv, or Python with pip to
341
+ fetch uv; a dependency with no wheel for the target is the one thing it
342
+ cannot do, and it says which.
343
+ */
344
+ const portable = () => tools.uv || tools.python
345
+ ? {
346
+ target, ok: true, route: native ? "native" : "cross", out,
347
+ how: `portable CPython for ${target} + its wheels (uv), as a tar.gz with a launcher`,
348
+ steps: [...pre, { portablePython: { target, out: outAbs } }],
349
+ }
350
+ : null;
351
+ switch (project.preset) {
352
+ case "go": {
353
+ if (!tools.go)
354
+ return missing("Go (https://go.dev/dl)");
355
+ return {
356
+ target, ok: true, route: native ? "native" : "cross", out,
357
+ how: native ? "go build" : `go build for ${GO_OS[os_]}/${GO_ARCH[arch]}, cgo off`,
358
+ steps: [...pre, {
359
+ program: "go",
360
+ args: ["build", ...extra, "-o", node_path_1.default.join(outAbs, file), project.entry ?? "."],
361
+ env: { GOOS: GO_OS[os_], GOARCH: GO_ARCH[arch], CGO_ENABLED: "0" },
362
+ }],
363
+ };
364
+ }
365
+ case "bun": {
366
+ if (!tools.bun)
367
+ return missing("Bun (https://bun.sh)");
368
+ if (target === "windows-arm64")
369
+ return { target, ok: false, why: "Bun cannot compile for Windows on Arm" };
370
+ if (!project.entry)
371
+ return { target, ok: false, why: `no entry point; set "entry" in ${CONFIG_FILE}` };
372
+ const bunOs = os_ === "macos" ? "darwin" : os_;
373
+ /*
374
+ Bun fetches another system's runtime itself, and on Windows 1.3 fails
375
+ to unpack it ("Failed to extract executable"). Fetching it here, from
376
+ the same npm package and checked against npm's own digest, and
377
+ handing Bun the file works on every host.
378
+ */
379
+ const runtime = node_path_1.default.join(cacheDirectory(), "bun", target, `bun${exe(target)}`);
380
+ return {
381
+ target, ok: true, route: native ? "native" : "cross", out,
382
+ how: `bun build --compile --target=bun-${bunOs}-${arch}`,
383
+ steps: [
384
+ ...pre,
385
+ ...(native ? [] : [{ bunRuntime: { target, to: runtime } }]),
386
+ {
387
+ program: "bun",
388
+ args: [
389
+ "build", "--compile", `--target=bun-${bunOs}-${arch}`,
390
+ ...(native ? [] : [`--compile-executable-path=${runtime}`]),
391
+ ...extra, project.entry, "--outfile", node_path_1.default.join(outAbs, file),
392
+ ],
393
+ },
394
+ ],
395
+ };
396
+ }
397
+ case "deno": {
398
+ if (!tools.deno)
399
+ return missing("Deno (https://deno.com)");
400
+ if (!DENO[target])
401
+ return { target, ok: false, why: "Deno cannot compile for Windows on Arm" };
402
+ if (!project.entry)
403
+ return { target, ok: false, why: `no entry point; set "entry" in ${CONFIG_FILE}` };
404
+ return {
405
+ target, ok: true, route: native ? "native" : "cross", out,
406
+ how: `deno compile --target ${DENO[target]}`,
407
+ steps: [...pre, {
408
+ program: "deno",
409
+ args: ["compile", ...extra, "--target", DENO[target], "--output", node_path_1.default.join(outAbs, file), project.entry],
410
+ }],
411
+ };
412
+ }
413
+ case "dotnet": {
414
+ if (!tools.dotnet)
415
+ return missing(".NET SDK (https://dot.net)");
416
+ if (project.windowsOnly && os_ !== "windows") {
417
+ return { target, ok: false, why: "a WinForms or WPF app runs only on Windows" };
418
+ }
419
+ return {
420
+ target, ok: true, route: native ? "native" : "cross", out,
421
+ how: `dotnet publish -r ${DOTNET[target]}, self-contained, single file`,
422
+ steps: [...pre, {
423
+ program: "dotnet",
424
+ args: [
425
+ "publish", ...(project.entry ? [project.entry] : []), "-c", "Release", "-r", DOTNET[target],
426
+ "--self-contained", "true", "-p:PublishSingleFile=true", ...extra, "-o", outAbs,
427
+ ],
428
+ }],
429
+ };
430
+ }
431
+ case "rust": {
432
+ if (!tools.cargo)
433
+ return missing("Rust (https://rustup.rs)");
434
+ const triple = RUST[target];
435
+ const built = (tripleUsed) => ({
436
+ copy: {
437
+ from: node_path_1.default.join(project.folder, "target", tripleUsed, "release", `${project.name}${exe(target)}`),
438
+ to: node_path_1.default.join(outAbs, file),
439
+ },
440
+ });
441
+ const addTarget = (tripleUsed) => tools.rustup ? [{ program: "rustup", args: ["target", "add", tripleUsed] }] : [];
442
+ /*
443
+ Plain cargo for this machine's own target, and for the other Mac
444
+ architecture on a Mac, which Xcode links. Anything else needs a
445
+ linker for the other system: zig, or a container.
446
+ */
447
+ if (native || (host.os === "macos" && os_ === "macos")) {
448
+ return {
449
+ target, ok: true, route: native ? "native" : "cross", out,
450
+ how: `cargo build --target ${triple}`,
451
+ steps: [...pre, ...addTarget(triple), { program: "cargo", args: ["build", "--release", "--target", triple, ...extra] }, built(triple)],
452
+ };
453
+ }
454
+ const gnu = os_ === "windows" ? triple.replace("-msvc", "-gnu") : triple;
455
+ if (tools.zigbuild && !(os_ === "windows" && arch === "arm64")) {
456
+ return {
457
+ target, ok: true, route: "cross", out,
458
+ how: `cargo zigbuild --target ${gnu}${os_ === "macos" ? " (pure-Rust crates; system frameworks need a Mac)" : ""}`,
459
+ steps: [...pre, ...addTarget(gnu), { program: "cargo", args: ["zigbuild", "--release", "--target", gnu, ...extra] }, built(gnu)],
460
+ };
461
+ }
462
+ if (tools.cross && tools.docker && os_ !== "macos") {
463
+ return {
464
+ target, ok: true, route: "docker", out,
465
+ how: `cross build --target ${gnu}`,
466
+ steps: [...pre, { program: "cross", args: ["build", "--release", "--target", gnu, ...extra] }, built(gnu)],
467
+ };
468
+ }
469
+ return {
470
+ target, ok: false,
471
+ why: os_ === "macos"
472
+ ? `needs cargo-zigbuild (cargo install cargo-zigbuild, plus zig) to cross-compile, or ${NEEDS("macos")}`
473
+ : "needs cargo-zigbuild (cargo install cargo-zigbuild, plus zig), or cross with Docker running",
474
+ };
475
+ }
476
+ case "electron": {
477
+ /* Only the container installs its own; this machine's must already be there. */
478
+ const local = os_ === "macos" || sameOs || (os_ === "windows" && tools.wine);
479
+ if (project.hasBuilder === false && local) {
480
+ return { target, ok: false, why: "electron-builder is not installed here; run npm install first" };
481
+ }
482
+ const flag = os_ === "windows" ? "--win" : os_ === "macos" ? "--mac" : "--linux";
483
+ const builder = ["electron-builder", flag, `--${arch}`, "--publish", "never", ...extra];
484
+ const outputFlag = (dir) => `--config.directories.output=${dir}`;
485
+ if (os_ === "macos") {
486
+ if (host.os !== "macos") {
487
+ return { target, ok: false, why: `macOS apps are packaged and signed with tools only macOS has; ${NEEDS("macos")}` };
488
+ }
489
+ return {
490
+ target, ok: true, route: native ? "native" : "cross", out, how: `electron-builder ${flag} --${arch}`,
491
+ steps: [...pre, { program: node_process_1.default.execPath, args: [node_path_1.default.join(project.folder, "node_modules", "electron-builder", "cli.js"), ...builder.slice(1), outputFlag(out)] }],
492
+ };
493
+ }
494
+ /*
495
+ Linux from Windows needs no container. electron-builder packages a
496
+ Linux app on Windows by itself; what it cannot do is mark anything
497
+ executable, because Windows has no such bit, so every program in
498
+ the archive arrives as a plain file. The archive is fixed up after.
499
+ A tar.gz only: AppImage needs symlinks and Linux tools, deb needs fpm.
500
+ */
501
+ if (os_ === "linux" && host.os === "windows") {
502
+ return {
503
+ target, ok: true, route: "cross", out,
504
+ how: `electron-builder --linux tar.gz --${arch} on Windows, executables marked afterwards (AppImage and deb need Linux)`,
505
+ steps: [
506
+ ...pre,
507
+ {
508
+ program: node_process_1.default.execPath,
509
+ args: [node_path_1.default.join(project.folder, "node_modules", "electron-builder", "cli.js"), "--linux", "tar.gz", `--${arch}`, "--publish", "never", ...extra, outputFlag(out)],
510
+ },
511
+ { fixTarModes: outAbs },
512
+ ],
513
+ };
514
+ }
515
+ const nativeOk = sameOs || (os_ === "windows" && host.os !== "windows" && tools.wine);
516
+ if (nativeOk) {
517
+ return {
518
+ target, ok: true, route: sameOs ? (native ? "native" : "cross") : "cross", out,
519
+ how: `electron-builder ${flag} --${arch}${!sameOs ? " with wine" : ""}`,
520
+ steps: [...pre, { program: node_process_1.default.execPath, args: [node_path_1.default.join(project.folder, "node_modules", "electron-builder", "cli.js"), ...builder.slice(1), outputFlag(out)] }],
521
+ };
522
+ }
523
+ if (!tools.docker) {
524
+ return { target, ok: false, why: `needs Docker running (electronuserland/builder${os_ === "windows" ? ":wine" : ""}), or ${NEEDS(os_)}` };
525
+ }
526
+ const image = os_ === "windows" ? "electronuserland/builder:wine" : "electronuserland/builder";
527
+ const inner = [
528
+ "npm ci",
529
+ before,
530
+ `npx --no-install ${builder.join(" ")} ${outputFlag(out.replace(/\\/g, "/"))}`,
531
+ ].filter(Boolean).join(" && ");
532
+ return {
533
+ target, ok: true, route: "docker", out, how: `electron-builder ${flag} --${arch} in ${image}`,
534
+ steps: [inDocker(project, image, inner, [
535
+ /* This machine's node_modules are for this machine; the container keeps its own. */
536
+ `coderook-build-${project.name}-node-modules:/project/node_modules`,
537
+ "coderook-build-electron-cache:/root/.cache/electron",
538
+ "coderook-build-electron-builder-cache:/root/.cache/electron-builder",
539
+ ])],
540
+ };
541
+ }
542
+ case "pyinstaller": {
543
+ if (!project.entry)
544
+ return { target, ok: false, why: `no entry point; set "entry" in ${CONFIG_FILE}` };
545
+ const work = node_path_1.default.join("build", "coderook", target);
546
+ const flags = (dist, workDir, specDir) => project.entry.endsWith(".spec")
547
+ ? ["--noconfirm", "--distpath", dist, "--workpath", workDir, ...extra, project.entry]
548
+ : ["--noconfirm", "--onefile", "--name", `${project.name}-${target}`, "--distpath", dist, "--workpath", workDir, "--specpath", specDir, ...extra, project.entry];
549
+ if (native) {
550
+ if (!tools.pyinstaller)
551
+ return portable() ?? missing("PyInstaller (pip install pyinstaller)");
552
+ return {
553
+ target, ok: true, route: "native", out, how: "pyinstaller",
554
+ steps: [...pre, { program: "pyinstaller", args: flags(outAbs, node_path_1.default.join(project.folder, work), node_path_1.default.join(project.folder, "build", "coderook")) }],
555
+ };
556
+ }
557
+ /* PyInstaller cannot cross-compile; Linux on x64 can still be reached from here. */
558
+ if (target === "linux-x64" && host.arch === "x64") {
559
+ const unix = (value) => value.replace(/\\/g, "/");
560
+ const requirements = (0, node_fs_1.existsSync)(node_path_1.default.join(project.folder, "requirements.txt")) ? "pip install -q -r requirements.txt && " : "";
561
+ if (tools.docker) {
562
+ const inner = `${before ? `${before} && ` : ""}pip install -q pyinstaller && ${requirements}pyinstaller ${flags(unix(out), unix(work), "build/coderook").map(sq).join(" ")}`;
563
+ return {
564
+ target, ok: true, route: "docker", out, how: "pyinstaller in python:3.12 (Debian)",
565
+ steps: [inDocker(project, "python:3.12", inner, ["coderook-build-pip-cache:/root/.cache/pip"])],
566
+ };
567
+ }
568
+ if (tools.wslPython) {
569
+ const inner = `cd ${sq(wslPath(project.folder))} && ${before ? `${before} && ` : ""}python3 -m PyInstaller ${flags(unix(out), unix(work), "build/coderook").map(sq).join(" ")}`;
570
+ return {
571
+ target, ok: true, route: "wsl", out, how: "pyinstaller in WSL",
572
+ steps: [{ program: "wsl", args: ["-e", "sh", "-lc", inner] }],
573
+ };
574
+ }
575
+ return portable() ?? { target, ok: false, why: `PyInstaller builds only for the system it runs on; needs Docker running, or WSL with PyInstaller, or ${NEEDS("linux")}` };
576
+ }
577
+ return portable() ?? { target, ok: false, why: `PyInstaller builds only for the system it runs on; ${NEEDS(os_)}` };
578
+ }
579
+ case "python": {
580
+ if (!project.entry)
581
+ return { target, ok: false, why: `no entry point; set "entry" in ${CONFIG_FILE}` };
582
+ return portable() ?? missing("uv, or Python with pip (https://docs.astral.sh/uv)");
583
+ }
584
+ case "custom": {
585
+ if (!project.config.command)
586
+ return { target, ok: false, why: `"custom" needs a "command" in ${CONFIG_FILE}` };
587
+ const fill = (text, outPath) => text.replace(/\{target\}/g, target).replace(/\{os\}/g, os_).replace(/\{arch\}/g, arch).replace(/\{out\}/g, outPath);
588
+ const image = project.config.docker?.[target];
589
+ if (image) {
590
+ if (!tools.docker)
591
+ return { target, ok: false, why: `needs Docker running for ${image}` };
592
+ const unixOut = out.replace(/\\/g, "/");
593
+ return {
594
+ target, ok: true, route: "docker", out, how: `${project.config.command} in ${image}`,
595
+ steps: [inDocker(project, image, [before, fill(project.config.command, unixOut)].filter(Boolean).join(" && "))],
596
+ };
597
+ }
598
+ return {
599
+ target, ok: true, route: native ? "native" : "cross", out, how: project.config.command,
600
+ steps: [...pre, { shell: fill(project.config.command, out), env: { CODEROOK_TARGET: target, CODEROOK_OS: os_, CODEROOK_ARCH: arch, CODEROOK_OUT: out } }],
601
+ };
602
+ }
603
+ default:
604
+ return { target, ok: false, why: `no toolchain recognised here; set "preset" in ${CONFIG_FILE}` };
605
+ }
606
+ });
607
+ }
608
+ /* ---------------------------------------------------------------- run */
609
+ /** What a command prints, or null when it does not run. */
610
+ function output(program, args) {
611
+ return new Promise((done) => {
612
+ let text = "";
613
+ const child = (0, node_child_process_1.spawn)(program, args, { windowsHide: true });
614
+ child.stdout.on("data", (chunk) => (text += chunk));
615
+ child.on("error", () => done(null));
616
+ child.on("exit", (code) => done(code === 0 ? text.trim() : null));
617
+ });
618
+ }
619
+ /**
620
+ * Mark the programs in a tar archive executable: Linux ELF binaries and
621
+ * scripts with a `#!` line. Each entry's mode is rewritten in its header and
622
+ * the header checksum recomputed; nothing else in the archive moves.
623
+ */
624
+ function markExecutables(archive) {
625
+ const out = Buffer.from(archive);
626
+ const marked = [];
627
+ let at = 0;
628
+ while (at + 512 <= out.length) {
629
+ const header = out.subarray(at, at + 512);
630
+ if (header.every((byte) => byte === 0))
631
+ break;
632
+ const field = (start, length) => header.subarray(start, start + length).toString("utf8").replace(/\0.*$/s, "");
633
+ const size = parseInt(field(124, 12).trim() || "0", 8);
634
+ const type = String.fromCharCode(header[156] ?? 48);
635
+ const body = out.subarray(at + 512, at + 512 + Math.min(size, 4));
636
+ const program = (body[0] === 0x7f && body.toString("latin1", 1, 4) === "ELF") || body.toString("latin1", 0, 2) === "#!";
637
+ if ((type === "0" || type === "\0") && program) {
638
+ const mode = parseInt(field(100, 8).trim() || "0", 8);
639
+ const wanted = mode | 0o755;
640
+ if (wanted !== mode) {
641
+ header.write(`${wanted.toString(8).padStart(7, "0")}\0`, 100, 8, "latin1");
642
+ header.fill(0x20, 148, 156);
643
+ let sum = 0;
644
+ for (const byte of header)
645
+ sum += byte;
646
+ header.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, 8, "latin1");
647
+ const prefix = field(345, 155);
648
+ marked.push(prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
649
+ }
650
+ }
651
+ at += 512 + Math.ceil(size / 512) * 512;
652
+ }
653
+ return { archive: out, marked };
654
+ }
655
+ async function fixTarModes(folder) {
656
+ for (const name of await (0, promises_1.readdir)(folder).catch(() => [])) {
657
+ if (!name.endsWith(".tar.gz"))
658
+ continue;
659
+ const file = node_path_1.default.join(folder, name);
660
+ const { archive, marked } = markExecutables((0, node_zlib_1.gunzipSync)(await (0, promises_1.readFile)(file)));
661
+ if (!marked.length)
662
+ continue;
663
+ await (0, promises_1.writeFile)(file, (0, node_zlib_1.gzipSync)(archive, { level: 9 }));
664
+ console.log(dim(`Marked ${marked.length} program${marked.length === 1 ? "" : "s"} executable in ${name}.`));
665
+ }
666
+ return 0;
667
+ }
668
+ /** The one file at `name` in a tar archive, read without unpacking the rest. */
669
+ function fileFromTar(archive, name) {
670
+ let at = 0;
671
+ while (at + 512 <= archive.length) {
672
+ const header = archive.subarray(at, at + 512);
673
+ if (header.every((byte) => byte === 0))
674
+ break;
675
+ const field = (start, length) => header.subarray(start, start + length).toString("utf8").replace(/\0.*$/s, "");
676
+ const prefix = field(345, 155);
677
+ const entry = prefix ? `${prefix}/${field(0, 100)}` : field(0, 100);
678
+ const size = parseInt(field(124, 12).trim() || "0", 8);
679
+ if (entry === name)
680
+ return archive.subarray(at + 512, at + 512 + size);
681
+ at += 512 + Math.ceil(size / 512) * 512;
682
+ }
683
+ return null;
684
+ }
685
+ async function fetchBunRuntime(target, to) {
686
+ const version = await output("bun", ["--version"]);
687
+ if (!version) {
688
+ console.error(red("Could not ask bun for its version."));
689
+ return 1;
690
+ }
691
+ const marker = `${to}.version`;
692
+ if ((0, node_fs_1.existsSync)(to) && (await (0, promises_1.readFile)(marker, "utf8").catch(() => "")) === version)
693
+ return 0;
694
+ const pkg = `bun-${osOf(target) === "macos" ? "darwin" : osOf(target)}-${archOf(target) === "arm64" ? "aarch64" : "x64"}`;
695
+ console.log(dim(`Fetching Bun ${version}'s runtime for ${target} (@oven/${pkg})…`));
696
+ try {
697
+ const meta = (await (await fetch(`https://registry.npmjs.org/@oven/${pkg}/${version}`)).json());
698
+ if (!meta.dist?.tarball || !meta.dist.integrity?.startsWith("sha512-"))
699
+ throw new Error("npm did not describe the package");
700
+ const tarball = Buffer.from(await (await fetch(meta.dist.tarball)).arrayBuffer());
701
+ const digest = `sha512-${(0, node_crypto_1.createHash)("sha512").update(tarball).digest("base64")}`;
702
+ if (digest !== meta.dist.integrity)
703
+ throw new Error("the download does not match npm's digest");
704
+ const binary = fileFromTar((0, node_zlib_1.gunzipSync)(tarball), `package/bin/bun${exe(target)}`);
705
+ if (!binary)
706
+ throw new Error("the package holds no runtime");
707
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(to), { recursive: true });
708
+ await (0, promises_1.writeFile)(to, binary, { mode: 0o755 });
709
+ await (0, promises_1.writeFile)(marker, version);
710
+ return 0;
711
+ }
712
+ catch (error) {
713
+ console.error(red(`Could not fetch Bun's runtime for ${target}: ${error instanceof Error ? error.message : String(error)}`));
714
+ return 1;
715
+ }
716
+ }
717
+ /** The project being built, for the steps that need more than a command line. */
718
+ let building = null;
719
+ async function runPortable(step) {
720
+ if (!building?.entry)
721
+ return 1;
722
+ try {
723
+ await (0, portable_python_js_1.buildPortablePython)({
724
+ folder: building.folder,
725
+ target: step.target,
726
+ name: building.name,
727
+ entry: building.entry,
728
+ ...(building.config.python ? { python: building.config.python } : {}),
729
+ ...(building.config.gui ? { gui: true } : {}),
730
+ outFolder: step.out,
731
+ cache: cacheDirectory(),
732
+ say: (text) => console.log(dim(text)),
733
+ });
734
+ return 0;
735
+ }
736
+ catch (error) {
737
+ console.error(red(error instanceof Error ? error.message : String(error)));
738
+ return 1;
739
+ }
740
+ }
741
+ function run(step, cwd) {
742
+ if ("portablePython" in step)
743
+ return runPortable(step.portablePython);
744
+ if ("bunRuntime" in step)
745
+ return fetchBunRuntime(step.bunRuntime.target, step.bunRuntime.to);
746
+ if ("fixTarModes" in step)
747
+ return fixTarModes(step.fixTarModes);
748
+ if ("copy" in step) {
749
+ return (async () => {
750
+ try {
751
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(step.copy.to), { recursive: true });
752
+ await (0, promises_1.copyFile)(step.copy.from, step.copy.to);
753
+ return 0;
754
+ }
755
+ catch (error) {
756
+ console.error(red(`Could not find the built file: ${step.copy.from}`));
757
+ console.error(dim(` ${error instanceof Error ? error.message : String(error)}`));
758
+ return 1;
759
+ }
760
+ })();
761
+ }
762
+ return new Promise((done) => {
763
+ const env = { ...node_process_1.default.env, ...(step.env ?? {}) };
764
+ const child = "shell" in step
765
+ ? (0, node_child_process_1.spawn)(step.shell, { cwd, env, stdio: "inherit", shell: true })
766
+ /*
767
+ Never through a shell: it joins arguments without quoting them, so a
768
+ path with a space in it splits. Electron's builder is started by its
769
+ own script under this Node for the same reason, rather than by npx,
770
+ which on Windows is a .cmd that only a shell can run.
771
+ */
772
+ : (0, node_child_process_1.spawn)(step.program, step.args, { cwd, env, stdio: "inherit" });
773
+ child.on("error", (error) => {
774
+ console.error(red(`${"shell" in step ? step.shell : step.program}: ${error.message}`));
775
+ done(1);
776
+ });
777
+ child.on("exit", (code) => done(code ?? 1));
778
+ });
779
+ }
780
+ /** The files a target produced, at the top of its folder. */
781
+ async function outputsOf(folder) {
782
+ const found = [];
783
+ for (const entry of await (0, promises_1.readdir)(folder, { withFileTypes: true }).catch(() => [])) {
784
+ if (!entry.isFile())
785
+ continue;
786
+ /* electron-builder's own bookkeeping; CodeRook's update feed writes its own. */
787
+ if (/\.(yml|yaml|blockmap|pdb|dbg)$/i.test(entry.name))
788
+ continue;
789
+ const full = node_path_1.default.join(folder, entry.name);
790
+ found.push({ file: full, size: (await (0, promises_1.stat)(full)).size });
791
+ }
792
+ return found;
793
+ }
794
+ const human = (bytes) => bytes > 1024 ** 2 ? `${(bytes / 1024 ** 2).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`;
795
+ /* ------------------------------------------------------------ command */
796
+ async function commandBuild(parsed) {
797
+ const folder = node_path_1.default.resolve(parsed.positional[0] === "init" ? (parsed.positional[1] ?? node_process_1.default.cwd()) : (parsed.positional[0] ?? node_process_1.default.cwd()));
798
+ const flag = (name) => parsed.flags.get(name);
799
+ if (parsed.positional[0] === "init")
800
+ return initConfig(folder);
801
+ let project;
802
+ try {
803
+ project = await describeProject(folder);
804
+ }
805
+ catch (error) {
806
+ console.error(red(error instanceof Error ? error.message : String(error)));
807
+ return 1;
808
+ }
809
+ const presetFlag = flag("preset");
810
+ if (typeof presetFlag === "string")
811
+ project = { ...project, preset: presetFlag };
812
+ if (!project.preset) {
813
+ console.error(red("No toolchain recognised in this folder."));
814
+ console.error(dim(` Looked for go.mod, Cargo.toml, a .csproj, electron-builder, deno.json, a Bun lockfile, a PyInstaller .spec and Python files.`));
815
+ console.error(dim(` Say which with ${accent("--preset <go|bun|deno|rust|dotnet|electron|pyinstaller|python>")}, or describe it in ${CONFIG_FILE} (${accent("cbx build init")}).`));
816
+ return 1;
817
+ }
818
+ let targets;
819
+ try {
820
+ const asked = flag("target") ?? flag("t");
821
+ targets = typeof asked === "string"
822
+ ? parseTargets(asked)
823
+ : project.config.targets?.length
824
+ ? parseTargets(project.config.targets.join(","))
825
+ : exports.DEFAULT_TARGETS;
826
+ }
827
+ catch (error) {
828
+ console.error(red(error instanceof Error ? error.message : String(error)));
829
+ return 1;
830
+ }
831
+ building = project;
832
+ const host = thisHost();
833
+ console.log(dim(`Checking what this ${host.os} ${host.arch} machine can build with…`));
834
+ const tools = await detectTools(host);
835
+ const plan = planBuild(project, host, tools, targets);
836
+ console.log(`${bold(project.name)} ${dim(`(${project.preset})`)}`);
837
+ const width = Math.max(...plan.map((one) => one.target.length));
838
+ for (const one of plan) {
839
+ console.log(one.ok
840
+ ? ` ${accent(one.target.padEnd(width))} ${one.route.padEnd(6)} ${dim(one.how)}`
841
+ : ` ${red(one.target.padEnd(width))} ${"no".padEnd(6)} ${dim(one.why)}`);
842
+ }
843
+ if (parsed.flags.has("plan") || parsed.flags.has("dry-run") || parsed.flags.has("n")) {
844
+ console.log(dim("Nothing was built."));
845
+ return 0;
846
+ }
847
+ /* Kept out of git as the link beside it is, without touching .gitignore. */
848
+ if (!project.config.out) {
849
+ await (0, promises_1.mkdir)(node_path_1.default.join(folder, ".coderook"), { recursive: true });
850
+ if (!(0, node_fs_1.existsSync)(node_path_1.default.join(folder, ".coderook", ".gitignore"))) {
851
+ await (0, promises_1.writeFile)(node_path_1.default.join(folder, ".coderook", ".gitignore"), "*\n");
852
+ }
853
+ }
854
+ const results = [];
855
+ for (const one of plan) {
856
+ if (!one.ok)
857
+ continue;
858
+ console.log(`\n${bold(`Building ${one.target}`)} ${dim(`— ${one.how}`)}`);
859
+ const outAbs = node_path_1.default.join(folder, one.out);
860
+ await (0, promises_1.rm)(outAbs, { recursive: true, force: true });
861
+ await (0, promises_1.mkdir)(outAbs, { recursive: true });
862
+ let code = 0;
863
+ for (const step of one.steps) {
864
+ code = await run(step, folder);
865
+ if (code !== 0)
866
+ break;
867
+ }
868
+ const files = code === 0 ? await outputsOf(outAbs) : [];
869
+ results.push({ target: one.target, ok: code === 0 && files.length > 0, files });
870
+ if (code === 0 && !files.length)
871
+ console.error(red(`${one.target}: the build finished but left nothing in ${one.out}.`));
872
+ }
873
+ console.log(`\n${bold("Built")}`);
874
+ for (const one of plan) {
875
+ const result = results.find((each) => each.target === one.target);
876
+ if (!one.ok) {
877
+ console.log(` ${red("✗")} ${one.target.padEnd(width)} ${dim(`not here: ${one.why}`)}`);
878
+ }
879
+ else if (!result?.ok) {
880
+ console.log(` ${red("✗")} ${one.target.padEnd(width)} ${red("failed")}`);
881
+ }
882
+ else {
883
+ for (const [at, output] of result.files.entries()) {
884
+ console.log(` ${at ? " " : green("✓")} ${(at ? "" : one.target).padEnd(width)} ${node_path_1.default.relative(folder, output.file)} ${dim(human(output.size))}`);
885
+ }
886
+ }
887
+ }
888
+ const failed = results.filter((one) => !one.ok).length;
889
+ if (parsed.flags.has("ship") && results.some((one) => one.ok)) {
890
+ if (failed) {
891
+ console.error(red("\nNothing was attached: a build failed, and a release missing a platform is worse than a late one."));
892
+ return 1;
893
+ }
894
+ const { commandAttach } = await Promise.resolve().then(() => __importStar(require("./attach_command.js")));
895
+ for (const result of results) {
896
+ for (const output of result.files) {
897
+ const base = node_path_1.default.basename(output.file);
898
+ const named = base.includes(result.target) || /win|linux|mac|darwin|osx/i.test(base)
899
+ ? base
900
+ : `${node_path_1.default.parse(base).name}-${result.target}${node_path_1.default.parse(base).ext}`;
901
+ const code = await commandAttach({
902
+ positional: [output.file],
903
+ flags: new Map([["name", named]]),
904
+ });
905
+ if (code !== 0)
906
+ return code;
907
+ }
908
+ }
909
+ }
910
+ if (failed)
911
+ return 1;
912
+ return results.length ? 0 : 1;
913
+ }
914
+ async function initConfig(folder) {
915
+ const file = node_path_1.default.join(folder, CONFIG_FILE);
916
+ if ((0, node_fs_1.existsSync)(file)) {
917
+ console.error(red(`${CONFIG_FILE} already exists here.`));
918
+ return 1;
919
+ }
920
+ const project = await describeProject(folder);
921
+ const config = {
922
+ preset: project.preset ?? "custom",
923
+ name: project.name,
924
+ ...(project.entry ? { entry: project.entry } : {}),
925
+ targets: exports.DEFAULT_TARGETS,
926
+ ...(project.preset ? {} : { command: "echo describe how to build {target} into {out}" }),
927
+ };
928
+ await (0, promises_1.writeFile)(file, `${JSON.stringify(config, null, 2)}\n`);
929
+ console.log(`Wrote ${CONFIG_FILE} ${dim(`(${config.preset}${project.preset ? ", recognised from the files here" : ", fill in the command"})`)}.`);
930
+ console.log(dim(` ${accent("cbx build --plan")} shows what this machine can build from it.`));
931
+ return 0;
932
+ }