open-design/tools/pack/src/index.ts
PerishFire bfcafc81fd
feat(pack): add Windows portable zip target alongside NSIS installer (#2937)
Adds a new `--to zip` (and `--to all`) tools-pack Windows build target that
produces a portable `.zip` from the cached `win-unpacked` tree using the
bundled 7z. The zip lays files at the archive root so users can extract it
anywhere and launch `Open Design.exe` without going through the NSIS
installer, addressing the no-install download request.

Release plumbing is updated to publish the portable zip and its sha256
beside the existing installer on R2 for beta, preview, and stable channels
(default on, gated by `WINDOWS_INCLUDE_ZIP`/`WIN_INCLUDE_ZIP`). The
electron-updater `latest.yml` feed continues to point only at the
installer; the zip is a manual-download convenience and is intentionally
excluded from the in-app updater.

Closes #1121

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

Co-authored-by: libertecode <libertecode@proton.me>
2026-05-26 06:14:44 +00:00

237 lines
8.2 KiB
TypeScript

import { cac } from "cac";
import type { CAC } from "cac";
import { resolveToolPackConfig, type ToolPackCliOptions, type ToolPackPlatform } from "./config.js";
import {
cleanupPackedMacNamespace,
installPackedMacDmg,
inspectPackedMacApp,
packMac,
readPackedMacLogs,
startPackedMacApp,
stopPackedMacApp,
uninstallPackedMacApp,
} from "./mac/index.js";
import {
cleanupPackedWinNamespace,
installPackedWinApp,
inspectPackedWinApp,
listPackedWinNamespaces,
packWin,
readPackedWinLogs,
resetPackedWinNamespaces,
startPackedWinApp,
stopPackedWinApp,
uninstallPackedWinApp,
} from "./win/index.js";
import {
cleanupPackedLinuxNamespace,
installPackedLinuxApp,
installPackedLinuxHeadless,
inspectPackedLinuxApp,
packLinux,
readPackedLinuxLogs,
resolveLinuxLifecycleMode,
startPackedLinuxApp,
startPackedLinuxHeadless,
stopPackedLinuxApp,
stopPackedLinuxHeadless,
uninstallPackedLinuxApp,
uninstallPackedLinuxHeadless,
} from "./linux.js";
type CliOptions = ToolPackCliOptions;
function printJson(payload: unknown): void {
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
}
function printLogs(result: { logs: Record<string, { lines: string[]; logPath: string }>; namespace: string }, options: CliOptions): void {
if (options.json === true) {
printJson(result);
return;
}
for (const [app, entry] of Object.entries(result.logs)) {
process.stdout.write(`[${app}] ${entry.logPath}\n`);
process.stdout.write(entry.lines.length > 0 ? `${entry.lines.join("\n")}\n` : "(no log lines)\n");
}
}
type CacCommand = ReturnType<CAC["command"]>;
function addSharedOptions(command: CacCommand) {
return command
.option("--cache-dir <path>", "tools-pack cache directory")
.option("--dir <path>", "tools-pack root directory")
.option("--json", "print JSON")
.option("--namespace <name>", "runtime namespace")
.option("--expr <expression>", "desktop inspect eval expression")
.option("--path <path>", "desktop inspect screenshot path")
.option("--update-action <action>", "desktop update action: status|check|download|install");
}
// Per-platform `--to` help text mirroring resolveToolPackBuildOutput in
// config.ts. Keep these in sync: the resolver throws on any value not listed
// here for the given platform.
const TO_HELP_BY_PLATFORM: Record<ToolPackPlatform, string> = {
linux: "build target: all|appimage|dir (default: all)",
mac: "build target: all|app|dmg|zip (default: all)",
win: "build target: all|dir|nsis|zip (default: nsis). `zip` produces a portable zip from the unpacked build; `all` produces dir+nsis+zip.",
};
function addBuildOptions(command: CacCommand, platform: ToolPackPlatform) {
return command
.option("--app-version <version>", "override packaged app version for release artifacts")
.option("--portable", "do not bake local tools-pack runtime roots into the packaged config")
.option("--signed", "build a signed/notarized mac artifact")
.option("--to <target>", TO_HELP_BY_PLATFORM[platform]);
}
function addMacBuildOptions(command: CacCommand) {
return addBuildOptions(command, "mac")
.option("--mac-compression <mode>", "mac artifact compression: normal|maximum|store (default: normal)");
}
function addWinLifecycleOptions(command: CacCommand) {
return command
.option("--remove-data", "remove packaged data during uninstall/reset/cleanup")
.option("--remove-logs", "remove packaged logs during uninstall/reset/cleanup")
.option("--remove-product-user-data", "remove the public Electron app userData root during Windows uninstall/reset/cleanup")
.option("--remove-sidecars", "remove packaged sidecar runtime during uninstall/reset/cleanup")
.option("--silent", "run installer/uninstaller silently", { default: true });
}
const cli = cac("tools-pack");
addMacBuildOptions(addSharedOptions(cli.command("mac <action>", "Mac packaging commands: build|install|start|stop|logs|uninstall|cleanup|inspect"))).action(
async (action: string, options: CliOptions) => {
const config = resolveToolPackConfig("mac", options);
switch (action) {
case "build":
printJson(await packMac(config));
return;
case "install":
printJson(await installPackedMacDmg(config));
return;
case "start":
printJson(await startPackedMacApp(config));
return;
case "stop":
printJson(await stopPackedMacApp(config));
return;
case "logs":
printLogs(await readPackedMacLogs(config), options);
return;
case "inspect":
printJson(await inspectPackedMacApp(config, options));
return;
case "uninstall":
printJson(await uninstallPackedMacApp(config));
return;
case "cleanup":
printJson(await cleanupPackedMacNamespace(config));
return;
default:
throw new Error(`unsupported mac action: ${action}`);
}
},
);
addWinLifecycleOptions(
addBuildOptions(
addSharedOptions(
cli.command(
"win <action>",
"Windows packaging commands: build|install|start|stop|logs|uninstall|cleanup|list|reset|inspect",
),
),
"win",
),
).action(async (action: string, options: CliOptions) => {
const config = resolveToolPackConfig("win", options);
switch (action) {
case "build":
printJson(await packWin(config));
return;
case "install":
printJson(await installPackedWinApp(config));
return;
case "start":
printJson(await startPackedWinApp(config));
return;
case "stop":
printJson(await stopPackedWinApp(config));
return;
case "logs":
printLogs(await readPackedWinLogs(config), options);
return;
case "uninstall":
printJson(await uninstallPackedWinApp(config));
return;
case "cleanup":
printJson(await cleanupPackedWinNamespace(config));
return;
case "list":
printJson(await listPackedWinNamespaces(config));
return;
case "reset":
printJson(await resetPackedWinNamespaces(config));
return;
case "inspect":
printJson(await inspectPackedWinApp(config, options));
return;
default:
throw new Error(`unsupported win action: ${action}`);
}
});
addBuildOptions(addSharedOptions(cli.command("linux <action>", "Linux packaging commands: build|install|start|stop|logs|uninstall|cleanup|inspect")), "linux")
.option("--containerized", "build inside electronuserland/builder Docker for wider glibc compatibility")
.option("--headless", "install/start/stop/uninstall/cleanup the headless entry; inspect returns status only")
.action(async (action: string, options: CliOptions) => {
const config = resolveToolPackConfig("linux", options);
switch (action) {
case "build":
printJson(await packLinux(config));
return;
case "install": {
const mode = resolveLinuxLifecycleMode(options, "install");
printJson(await (mode === "headless" ? installPackedLinuxHeadless(config) : installPackedLinuxApp(config)));
return;
}
case "start": {
const mode = resolveLinuxLifecycleMode(options, "start");
printJson(await (mode === "headless" ? startPackedLinuxHeadless(config) : startPackedLinuxApp(config)));
return;
}
case "stop": {
const mode = resolveLinuxLifecycleMode(options, "stop");
printJson(await (mode === "headless" ? stopPackedLinuxHeadless(config) : stopPackedLinuxApp(config)));
return;
}
case "logs":
printLogs(await readPackedLinuxLogs(config), options);
return;
case "inspect":
printJson(await inspectPackedLinuxApp(config, {
expr: options.expr,
headless: options.headless === true,
path: options.path,
}));
return;
case "uninstall": {
const mode = resolveLinuxLifecycleMode(options, "uninstall");
printJson(await (mode === "headless" ? uninstallPackedLinuxHeadless(config) : uninstallPackedLinuxApp(config)));
return;
}
case "cleanup":
printJson(await cleanupPackedLinuxNamespace(config, options));
return;
default:
throw new Error(`unsupported linux action: ${action}`);
}
});
cli.help();
cli.parse();