@mirinjs/cli 0.1.0-alpha.2 → 0.1.0-alpha.20
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/package.json +8 -5
- package/src/artifacts.ts +165 -26
- package/src/build.ts +234 -43
- package/src/bundle/linux/app.ts +275 -0
- package/src/bundle/linux/index.ts +1 -0
- package/src/{bundle.ts → bundle/macos/app.ts} +65 -23
- package/src/bundle/macos/index.ts +1 -0
- package/src/bundle/shared/cef-locales.ts +91 -0
- package/src/{bundle-win.ts → bundle/windows/app.ts} +10 -6
- package/src/bundle/windows/index.ts +1 -0
- package/src/check.ts +271 -0
- package/src/dev.ts +213 -35
- package/src/dmg.ts +22 -10
- package/src/doctor.ts +227 -0
- package/src/extras.ts +52 -13
- package/src/icons/macos/appearance.ts +96 -0
- package/src/icons/macos/index.ts +1 -0
- package/src/icons/windows/icon.ts +255 -0
- package/src/icons/windows/index.ts +1 -0
- package/src/index.ts +109 -7
- package/src/installer-inno.ts +141 -0
- package/src/installer-win.ts +17 -11
- package/src/package/linux/index.ts +2 -0
- package/src/package/linux/lib/defaults.ts +53 -0
- package/src/package/linux/lib/tools.ts +45 -0
- package/src/package/linux/lib/validation.ts +71 -0
- package/src/package/linux/package.ts +293 -0
- package/src/package/linux/types.ts +30 -0
- package/src/release/installers.ts +148 -0
- package/src/release/previous.ts +105 -0
- package/src/release.ts +166 -116
- package/src/shared/driver.ts +240 -0
- package/src/shared/inspector.ts +64 -0
- package/src/shared/report.ts +69 -0
- package/src/shared/scenario.ts +58 -0
- package/src/shared/session.ts +153 -0
- package/src/skill.ts +51 -0
- package/src/icon-win.ts +0 -81
package/src/index.ts
CHANGED
|
@@ -1,32 +1,133 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* mirin CLI — `dev`, `build`, `init`.
|
|
3
|
+
* mirin CLI — `dev`, `check`, `doctor`, `build`, `release`, `init`, `skill`.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import { scaffold } from "create-mirinjs";
|
|
8
8
|
import { build } from "./build.ts";
|
|
9
|
+
import { check } from "./check.ts";
|
|
10
|
+
import { dev } from "./dev.ts";
|
|
11
|
+
import { doctor } from "./doctor.ts";
|
|
12
|
+
import { parseLinuxFormats } from "./package/linux/index.ts";
|
|
9
13
|
import { release } from "./release.ts";
|
|
10
|
-
import {
|
|
14
|
+
import { skill } from "./skill.ts";
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
/** Minimal flag parser: `--k=v`, `--k v`, and boolean `--k`; the rest are positionals. */
|
|
17
|
+
function parseArgs(argv: string[]): {
|
|
18
|
+
positionals: string[];
|
|
19
|
+
opts: Record<string, string | boolean>;
|
|
20
|
+
} {
|
|
21
|
+
const opts: Record<string, string | boolean> = {};
|
|
22
|
+
const positionals: string[] = [];
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
if (a === undefined) continue;
|
|
26
|
+
if (a.startsWith("--")) {
|
|
27
|
+
const eq = a.indexOf("=");
|
|
28
|
+
if (eq >= 0) {
|
|
29
|
+
opts[a.slice(2, eq)] = a.slice(eq + 1);
|
|
30
|
+
} else {
|
|
31
|
+
const key = a.slice(2);
|
|
32
|
+
const next = argv[i + 1];
|
|
33
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
34
|
+
opts[key] = next;
|
|
35
|
+
i++;
|
|
36
|
+
} else {
|
|
37
|
+
opts[key] = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
positionals.push(a);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { positionals, opts };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [command, ...rest] = Bun.argv.slice(2);
|
|
48
|
+
const { positionals, opts } = parseArgs(rest);
|
|
49
|
+
const arg = positionals[0];
|
|
13
50
|
|
|
14
51
|
const USAGE = `mirin — build desktop apps with Bun + Chromium
|
|
15
52
|
|
|
16
53
|
Usage:
|
|
17
54
|
mirin dev run the app against the Vite dev server (HMR + typed RPC)
|
|
18
|
-
mirin
|
|
55
|
+
mirin check boot once, capture a screenshot + UI snapshot, report, exit
|
|
56
|
+
mirin doctor check the project and environment without building
|
|
57
|
+
mirin build package a standalone app (output: ./build)
|
|
19
58
|
mirin release build + emit update artifacts (output: ./build/release)
|
|
20
59
|
mirin init [dir] scaffold a new app
|
|
60
|
+
mirin skill install the agent skill into this project (.claude/skills/mirin)
|
|
61
|
+
|
|
62
|
+
Options (build):
|
|
63
|
+
--version <v> override the app version (else package.json)
|
|
64
|
+
--linux also emit Linux packages (AppImage + .deb + .rpm)
|
|
65
|
+
--linux-target <a,b,…> restrict Linux packaging to these formats
|
|
66
|
+
(appimage,deb,rpm) — implies --linux
|
|
67
|
+
|
|
68
|
+
Options (check):
|
|
69
|
+
--timeout <ms> how long to wait for a window (default 45000)
|
|
70
|
+
--settle <ms> quiet time before capturing (default 1000)
|
|
71
|
+
--scenario <file> drive the app with a scenario before capturing;
|
|
72
|
+
the file default-exports defineCheck(async app => …)
|
|
73
|
+
|
|
74
|
+
Options (check, doctor, dev, build):
|
|
75
|
+
--json machine-readable output on stdout
|
|
21
76
|
`;
|
|
22
77
|
|
|
78
|
+
/** `--flag <n>` as a positive integer, or undefined when absent/unusable. */
|
|
79
|
+
function intOpt(key: string): number | undefined {
|
|
80
|
+
const raw = opts[key];
|
|
81
|
+
if (typeof raw !== "string") return undefined;
|
|
82
|
+
const value = Number(raw);
|
|
83
|
+
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const asJson = opts.json === true;
|
|
87
|
+
|
|
23
88
|
switch (command) {
|
|
24
89
|
case "dev": {
|
|
25
|
-
process.exit(await dev());
|
|
90
|
+
process.exit(await dev(process.cwd(), asJson ? { json: true } : undefined));
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "check": {
|
|
94
|
+
process.exit(
|
|
95
|
+
await check(process.cwd(), {
|
|
96
|
+
json: asJson,
|
|
97
|
+
...(intOpt("timeout") !== undefined ? { timeoutMs: intOpt("timeout") } : {}),
|
|
98
|
+
...(intOpt("settle") !== undefined ? { settleMs: intOpt("settle") } : {}),
|
|
99
|
+
...(typeof opts.scenario === "string" ? { scenario: opts.scenario } : {}),
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case "doctor": {
|
|
105
|
+
process.exit(await doctor(process.cwd(), { json: asJson }));
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "skill": {
|
|
109
|
+
process.exit(await skill(process.cwd(), { json: asJson }));
|
|
26
110
|
break;
|
|
27
111
|
}
|
|
28
112
|
case "build": {
|
|
29
|
-
|
|
113
|
+
const version = typeof opts.version === "string" ? opts.version : undefined;
|
|
114
|
+
const target = typeof opts["linux-target"] === "string" ? opts["linux-target"] : undefined;
|
|
115
|
+
const linuxFormats = target ? parseLinuxFormats(target) : undefined;
|
|
116
|
+
const packageLinux = opts.linux === true || linuxFormats != null;
|
|
117
|
+
const result = await build(process.cwd(), { version, packageLinux, linuxFormats });
|
|
118
|
+
if (asJson) {
|
|
119
|
+
// Only what a caller needs; the rest of BuildResult is plumbing for `mirin release`.
|
|
120
|
+
console.log(
|
|
121
|
+
JSON.stringify({
|
|
122
|
+
phase: "result",
|
|
123
|
+
app: result.app,
|
|
124
|
+
appName: result.appName,
|
|
125
|
+
bundleId: result.bundleId,
|
|
126
|
+
version: result.version,
|
|
127
|
+
channel: result.channel,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
30
131
|
process.exit(0);
|
|
31
132
|
break;
|
|
32
133
|
}
|
|
@@ -39,6 +140,7 @@ switch (command) {
|
|
|
39
140
|
try {
|
|
40
141
|
const name = scaffold(targetDir);
|
|
41
142
|
console.log(`\n✓ Created ${name} in ${arg ?? "my-mirin-app"}`);
|
|
143
|
+
console.log(" .claude/skills/mirin/ — agent skill for verifying changes");
|
|
42
144
|
console.log(" bun install && bun run dev");
|
|
43
145
|
process.exit(0);
|
|
44
146
|
} catch (err) {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows installer via **Inno Setup** — a modern, clean wizard (the installer VS
|
|
3
|
+
* Code ships). Generates an `.iss` script from the app folder + config and compiles
|
|
4
|
+
* it with `iscc`. Like the NSIS path it installs per-user (`%LOCALAPPDATA%\Programs`,
|
|
5
|
+
* no elevation) or per-machine (Program Files), creates Start Menu + Desktop
|
|
6
|
+
* shortcuts, and registers an uninstaller / Add-Remove-Programs entry — but with
|
|
7
|
+
* Inno's `WizardStyle=modern` flat UI instead of NSIS's dated one.
|
|
8
|
+
*
|
|
9
|
+
* Requires `iscc` (Inno Setup 6+) on PATH; the caller checks `hasInno()` and falls
|
|
10
|
+
* back to NSIS / the portable `.zip` when it's absent.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { isAbsolute, join } from "node:path";
|
|
15
|
+
import { $ } from "bun";
|
|
16
|
+
import type { InnoConfig } from "mirinjs";
|
|
17
|
+
import { makeWindowsIcon } from "./icons/windows/index.ts";
|
|
18
|
+
|
|
19
|
+
export interface BuildInnoInput {
|
|
20
|
+
appDir: string; // the assembled app folder (build/<App>)
|
|
21
|
+
appName: string;
|
|
22
|
+
exeName: string; // <App>.exe
|
|
23
|
+
version: string;
|
|
24
|
+
bundleId: string; // reverse-DNS id → AppId / uninstall key
|
|
25
|
+
outDir: string; // build/release
|
|
26
|
+
fileName: string; // e.g. stable-win32-x64-Anko-setup.exe
|
|
27
|
+
options: InnoConfig;
|
|
28
|
+
projectDir: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Whether the Inno Setup compiler `iscc` is on PATH. */
|
|
32
|
+
export function hasInno(): boolean {
|
|
33
|
+
return Bun.which("iscc") != null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const resolve = (projectDir: string, p: string) => (isAbsolute(p) ? p : join(projectDir, p));
|
|
37
|
+
/** Escape an Inno directive value: `{` is the constant sigil. */
|
|
38
|
+
const v = (s: string) => s.replace(/\{/g, "{{");
|
|
39
|
+
|
|
40
|
+
/** Build the installer with `iscc`, returning the setup.exe path. */
|
|
41
|
+
export async function buildInnoInstaller(input: BuildInnoInput): Promise<string> {
|
|
42
|
+
const { appDir, appName, exeName, version, bundleId, outDir, fileName, options, projectDir } =
|
|
43
|
+
input;
|
|
44
|
+
const out = join(outDir, fileName);
|
|
45
|
+
rmSync(out, { force: true });
|
|
46
|
+
if (!fileName.endsWith(".exe")) throw new Error("inno: fileName must end with .exe");
|
|
47
|
+
const baseName = fileName.slice(0, -".exe".length);
|
|
48
|
+
|
|
49
|
+
const perMachine = options.perMachine === true;
|
|
50
|
+
const minimal = options.oneClick === true;
|
|
51
|
+
const desktop = options.desktopShortcut !== false;
|
|
52
|
+
const startMenu = options.startMenuShortcut !== false;
|
|
53
|
+
const runAfter = options.runAfterFinish !== false;
|
|
54
|
+
const changeDir = options.allowChangeInstallDir !== false && !minimal;
|
|
55
|
+
const publisher = options.publisher ?? appName;
|
|
56
|
+
const installDir =
|
|
57
|
+
options.installDir ??
|
|
58
|
+
(perMachine ? "{autopf}\\" + appName : "{localappdata}\\Programs\\" + appName);
|
|
59
|
+
|
|
60
|
+
// Setup icon: an explicit .ico, else the app's bundled icon.ico.
|
|
61
|
+
let icon: string | undefined;
|
|
62
|
+
if (options.installerIcon) {
|
|
63
|
+
icon = makeWindowsIcon(resolve(projectDir, options.installerIcon), join(outDir, "_inno.ico"));
|
|
64
|
+
} else if (existsSync(join(appDir, "icon.ico"))) {
|
|
65
|
+
icon = join(appDir, "icon.ico");
|
|
66
|
+
}
|
|
67
|
+
const license = options.license ? resolve(projectDir, options.license) : undefined;
|
|
68
|
+
if (license && !existsSync(license)) throw new Error(`inno: license not found: ${license}`);
|
|
69
|
+
|
|
70
|
+
const L: string[] = [];
|
|
71
|
+
L.push("[Setup]");
|
|
72
|
+
L.push(`AppId={{${bundleId}}`);
|
|
73
|
+
L.push(`AppName=${v(appName)}`);
|
|
74
|
+
L.push(`AppVersion=${v(version)}`);
|
|
75
|
+
// Without this, Inno's display name defaults to "<name> version <x>" (shown in
|
|
76
|
+
// the wizard title + Add/Remove Programs); pin it to the app name.
|
|
77
|
+
L.push(`AppVerName=${v(appName)}`);
|
|
78
|
+
L.push(`AppPublisher=${v(publisher)}`);
|
|
79
|
+
L.push("WizardStyle=modern");
|
|
80
|
+
L.push(`DefaultDirName=${installDir}`);
|
|
81
|
+
L.push(`DefaultGroupName=${v(appName)}`);
|
|
82
|
+
L.push(`PrivilegesRequired=${perMachine ? "admin" : "lowest"}`);
|
|
83
|
+
L.push(`OutputDir=${outDir}`);
|
|
84
|
+
L.push(`OutputBaseFilename=${baseName}`);
|
|
85
|
+
// CEF is the bulk of the installer. Normal compression plus two independent
|
|
86
|
+
// LZMA2 blocks is substantially faster on CI with a modest size tradeoff.
|
|
87
|
+
L.push("Compression=lzma2/normal");
|
|
88
|
+
L.push("LZMANumBlockThreads=2");
|
|
89
|
+
L.push("SolidCompression=yes");
|
|
90
|
+
L.push("ArchitecturesAllowed=x64compatible");
|
|
91
|
+
L.push("ArchitecturesInstallIn64BitMode=x64compatible");
|
|
92
|
+
L.push(`UninstallDisplayIcon={app}\\${exeName}`);
|
|
93
|
+
if (icon) L.push(`SetupIconFile=${icon}`);
|
|
94
|
+
if (license) L.push(`LicenseFile=${license}`);
|
|
95
|
+
if (minimal) {
|
|
96
|
+
L.push("DisableWelcomePage=yes");
|
|
97
|
+
L.push("DisableReadyPage=yes");
|
|
98
|
+
}
|
|
99
|
+
if (!changeDir) L.push("DisableDirPage=yes");
|
|
100
|
+
if (!startMenu) L.push("DisableProgramGroupPage=yes");
|
|
101
|
+
if (options.include) L.push("", "; --- user include ---", options.include, "");
|
|
102
|
+
|
|
103
|
+
L.push("", "[Files]");
|
|
104
|
+
L.push(
|
|
105
|
+
`Source: "${appDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs`,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (desktop) {
|
|
109
|
+
L.push("", "[Tasks]");
|
|
110
|
+
L.push(
|
|
111
|
+
`Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional:"`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
L.push("", "[Icons]");
|
|
116
|
+
if (startMenu) L.push(`Name: "{group}\\${v(appName)}"; Filename: "{app}\\${exeName}"`);
|
|
117
|
+
if (desktop) {
|
|
118
|
+
L.push(
|
|
119
|
+
`Name: "{autodesktop}\\${v(appName)}"; Filename: "{app}\\${exeName}"; Tasks: desktopicon`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (runAfter) {
|
|
124
|
+
L.push("", "[Run]");
|
|
125
|
+
L.push(
|
|
126
|
+
`Filename: "{app}\\${exeName}"; Description: "Launch ${v(appName)}"; ` +
|
|
127
|
+
"Flags: nowait postinstall skipifsilent",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const script = join(outDir, "_installer.iss");
|
|
132
|
+
writeFileSync(script, L.join("\n") + "\n");
|
|
133
|
+
console.log(`[mirin release] compiling Inno Setup installer → ${fileName}`);
|
|
134
|
+
const res = await $`iscc /Q ${script}`.nothrow();
|
|
135
|
+
rmSync(script, { force: true });
|
|
136
|
+
rmSync(join(outDir, "_inno.ico"), { force: true });
|
|
137
|
+
if (res.exitCode !== 0 || !existsSync(out)) {
|
|
138
|
+
throw new Error(`iscc failed (exit ${res.exitCode})`);
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
package/src/installer-win.ts
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* falls back to the portable `.zip` when it's absent.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { $ } from "bun";
|
|
16
15
|
import { existsSync, rmSync, writeFileSync } from "node:fs";
|
|
17
|
-
import {
|
|
16
|
+
import { isAbsolute, join } from "node:path";
|
|
17
|
+
import { $ } from "bun";
|
|
18
18
|
import type { NsisConfig } from "mirinjs";
|
|
19
|
-
import { makeWindowsIcon } from "./
|
|
19
|
+
import { makeWindowsIcon } from "./icons/windows/index.ts";
|
|
20
20
|
|
|
21
21
|
export interface BuildNsisInput {
|
|
22
22
|
/** The assembled app folder (build/<App>). */
|
|
@@ -50,7 +50,8 @@ const resolve = (projectDir: string, p: string) => (isAbsolute(p) ? p : join(pro
|
|
|
50
50
|
|
|
51
51
|
/** Build the installer with `makensis`, returning the setup.exe path. */
|
|
52
52
|
export async function buildNsisInstaller(input: BuildNsisInput): Promise<string> {
|
|
53
|
-
const { appDir, appName, exeName, version, bundleId, outDir, fileName, options, projectDir } =
|
|
53
|
+
const { appDir, appName, exeName, version, bundleId, outDir, fileName, options, projectDir } =
|
|
54
|
+
input;
|
|
54
55
|
const out = join(outDir, fileName);
|
|
55
56
|
rmSync(out, { force: true });
|
|
56
57
|
|
|
@@ -63,12 +64,16 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
|
|
|
63
64
|
const publisher = options.publisher ?? appName;
|
|
64
65
|
const root = perMachine ? "HKLM" : "HKCU";
|
|
65
66
|
const installDir =
|
|
66
|
-
options.installDir ??
|
|
67
|
+
options.installDir ??
|
|
68
|
+
(perMachine ? `$PROGRAMFILES64\\${appName}` : `$LOCALAPPDATA\\Programs\\${appName}`);
|
|
67
69
|
|
|
68
70
|
// Installer/uninstaller icon: an explicit .ico, else the app's bundled icon.ico.
|
|
69
71
|
let icon: string | undefined;
|
|
70
72
|
if (options.installerIcon) {
|
|
71
|
-
icon = makeWindowsIcon(
|
|
73
|
+
icon = makeWindowsIcon(
|
|
74
|
+
resolve(projectDir, options.installerIcon),
|
|
75
|
+
join(outDir, "_installer.ico"),
|
|
76
|
+
);
|
|
72
77
|
} else if (existsSync(join(appDir, "icon.ico"))) {
|
|
73
78
|
icon = join(appDir, "icon.ico");
|
|
74
79
|
}
|
|
@@ -85,7 +90,7 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
|
|
|
85
90
|
L.push(`OutFile "${nsis(out)}"`);
|
|
86
91
|
L.push(`InstallDir "${installDir}"`);
|
|
87
92
|
L.push(`RequestExecutionLevel ${perMachine ? "admin" : "user"}`);
|
|
88
|
-
L.push(
|
|
93
|
+
L.push("!define MUI_ABORTWARNING");
|
|
89
94
|
if (icon) {
|
|
90
95
|
L.push(`!define MUI_ICON "${nsis(icon)}"`);
|
|
91
96
|
L.push(`!define MUI_UNICON "${nsis(icon)}"`);
|
|
@@ -127,14 +132,15 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
|
|
|
127
132
|
// Estimated size for Add/Remove Programs.
|
|
128
133
|
L.push(` \${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2`);
|
|
129
134
|
L.push(` IntFmt $0 "0x%08X" $0`);
|
|
130
|
-
const reg = (name: string, val: string) =>
|
|
135
|
+
const reg = (name: string, val: string) =>
|
|
136
|
+
L.push(` WriteRegStr ${root} "${key}" "${name}" "${val}"`);
|
|
131
137
|
reg("DisplayName", nsis(appName));
|
|
132
138
|
reg("DisplayVersion", nsis(version));
|
|
133
139
|
reg("Publisher", nsis(publisher));
|
|
134
140
|
reg("DisplayIcon", `$INSTDIR\\${nsis(exeName)}`);
|
|
135
141
|
reg("InstallLocation", "$INSTDIR");
|
|
136
|
-
reg("UninstallString",
|
|
137
|
-
reg("QuietUninstallString",
|
|
142
|
+
reg("UninstallString", "$INSTDIR\\Uninstall.exe");
|
|
143
|
+
reg("QuietUninstallString", "$INSTDIR\\Uninstall.exe /S");
|
|
138
144
|
L.push(` WriteRegDWORD ${root} "${key}" "NoModify" 1`);
|
|
139
145
|
L.push(` WriteRegDWORD ${root} "${key}" "NoRepair" 1`);
|
|
140
146
|
L.push(` WriteRegDWORD ${root} "${key}" "EstimatedSize" $0`);
|
|
@@ -142,7 +148,7 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
|
|
|
142
148
|
L.push("SectionEnd");
|
|
143
149
|
|
|
144
150
|
// Uninstall section.
|
|
145
|
-
L.push(
|
|
151
|
+
L.push('Section "Uninstall"');
|
|
146
152
|
L.push(` SetShellVarContext ${ctx}`);
|
|
147
153
|
if (desktop) L.push(` Delete "$DESKTOP\\${nsis(appName)}.lnk"`);
|
|
148
154
|
if (startMenu) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { LinuxPackageFormat } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
/** CEF/GTK runtime deps on Debian/Ubuntu package naming (best-effort; overridable). */
|
|
4
|
+
export const DEFAULT_DEB_DEPENDS = [
|
|
5
|
+
"libgtk-3-0",
|
|
6
|
+
"libnss3",
|
|
7
|
+
"libnspr4",
|
|
8
|
+
"libasound2",
|
|
9
|
+
"libx11-6",
|
|
10
|
+
"libxcomposite1",
|
|
11
|
+
"libxdamage1",
|
|
12
|
+
"libxext6",
|
|
13
|
+
"libxfixes3",
|
|
14
|
+
"libxrandr2",
|
|
15
|
+
"libgbm1",
|
|
16
|
+
"libxkbcommon0",
|
|
17
|
+
"libpango-1.0-0",
|
|
18
|
+
"libcairo2",
|
|
19
|
+
"libatk1.0-0",
|
|
20
|
+
"libatk-bridge2.0-0",
|
|
21
|
+
"libcups2",
|
|
22
|
+
"libdrm2",
|
|
23
|
+
"libdbus-1-3",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** CEF/GTK runtime deps on RPM (Fedora/RHEL/openSUSE) package naming. */
|
|
27
|
+
export const DEFAULT_RPM_DEPENDS = [
|
|
28
|
+
"gtk3",
|
|
29
|
+
"nss",
|
|
30
|
+
"nspr",
|
|
31
|
+
"alsa-lib",
|
|
32
|
+
"libX11",
|
|
33
|
+
"libXcomposite",
|
|
34
|
+
"libXdamage",
|
|
35
|
+
"libXext",
|
|
36
|
+
"libXfixes",
|
|
37
|
+
"libXrandr",
|
|
38
|
+
"mesa-libgbm",
|
|
39
|
+
"libxkbcommon",
|
|
40
|
+
"pango",
|
|
41
|
+
"cairo",
|
|
42
|
+
"atk",
|
|
43
|
+
"at-spi2-atk",
|
|
44
|
+
"cups-libs",
|
|
45
|
+
"libdrm",
|
|
46
|
+
"dbus-libs",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const LINUX_FORMATS = [
|
|
50
|
+
"appimage",
|
|
51
|
+
"deb",
|
|
52
|
+
"rpm",
|
|
53
|
+
] as const satisfies readonly LinuxPackageFormat[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve an external tool path: an explicit `$ENV` override (if it exists), then
|
|
7
|
+
* PATH, then the first existing `fallback`. Throws a clear, actionable error when
|
|
8
|
+
* none resolve (no user-specific absolute path is baked into committed code).
|
|
9
|
+
*/
|
|
10
|
+
function resolveTool(bin: string, envVar: string, fallbacks: string[]): string {
|
|
11
|
+
const fromEnv = process.env[envVar];
|
|
12
|
+
if (fromEnv && existsSync(fromEnv)) return fromEnv;
|
|
13
|
+
const onPath = Bun.which(bin);
|
|
14
|
+
if (onPath) return onPath;
|
|
15
|
+
for (const f of fallbacks) if (existsSync(f)) return f;
|
|
16
|
+
throw new Error(
|
|
17
|
+
`[mirin] "${bin}" not found — install it and put it on PATH, or set $${envVar} ` +
|
|
18
|
+
"to its absolute path.",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `~/.local/share/gem/ruby/<ver>/bin` (RubyGems user install dir), if resolvable. */
|
|
23
|
+
function gemUserBin(): string | undefined {
|
|
24
|
+
const ruby = Bun.which("ruby");
|
|
25
|
+
if (!ruby) return undefined;
|
|
26
|
+
try {
|
|
27
|
+
const dir = Bun.spawnSync([ruby, "-e", "print Gem.user_dir"]).stdout.toString().trim();
|
|
28
|
+
return dir ? join(dir, "bin") : undefined;
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveFpm(): string {
|
|
35
|
+
const fallbacks: string[] = [];
|
|
36
|
+
const gemBin = gemUserBin();
|
|
37
|
+
if (gemBin) fallbacks.push(join(gemBin, "fpm"));
|
|
38
|
+
return resolveTool("fpm", "MIRIN_FPM", fallbacks);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveAppimagetool(): string {
|
|
42
|
+
return resolveTool("appimagetool", "MIRIN_APPIMAGETOOL", [
|
|
43
|
+
join(homedir(), ".local", "bin", "appimagetool"),
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { LinuxPackageFormat, LinuxPackageInput } from "../types.ts";
|
|
2
|
+
import { LINUX_FORMATS } from "./defaults.ts";
|
|
3
|
+
|
|
4
|
+
/** A Debian/RPM-friendly package name: lowercase, alnum + `.-+`, no leading sigil. */
|
|
5
|
+
function shortName(input: string): string {
|
|
6
|
+
return (
|
|
7
|
+
input
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9.\-+]/g, "-")
|
|
10
|
+
.replace(/^[.\-+]+/, "") || "app"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function safeLinuxPackageInput(input: LinuxPackageInput): LinuxPackageInput {
|
|
15
|
+
return {
|
|
16
|
+
...input,
|
|
17
|
+
appName: safeAppBinaryName(input.appName),
|
|
18
|
+
bundleId: safeLinuxIdentifier(input.bundleId),
|
|
19
|
+
version: safePackageVersion(input.version),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function safeLinuxIdentifier(value: string): string {
|
|
24
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) || value === "." || value === "..") {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`[mirin] invalid Linux app id "${value}" — use a reverse-DNS style id with letters, ` +
|
|
27
|
+
'digits, ".", "_" or "-".',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function safeAppBinaryName(value: string): string {
|
|
34
|
+
if (value.length === 0 || value === "." || value === ".." || /[\0\r\n/"\\`$]/.test(value)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`[mirin] invalid Linux app name "${value}" — it becomes a binary path segment, ` +
|
|
37
|
+
"so it cannot contain path separators, quotes, shell expansion characters, or newlines.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function safePackageVersion(value: string): string {
|
|
44
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9.+:~_-]*$/.test(value)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`[mirin] invalid Linux package version "${value}" — use only letters, digits, ".", "+", ":", "~", "_" or "-".`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function safePackageName(value: string): string {
|
|
53
|
+
return shortName(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function validateLinuxFormats(
|
|
57
|
+
formats: readonly string[],
|
|
58
|
+
source: string,
|
|
59
|
+
): LinuxPackageFormat[] {
|
|
60
|
+
const allowed = new Set<string>(LINUX_FORMATS);
|
|
61
|
+
const out: LinuxPackageFormat[] = [];
|
|
62
|
+
for (const format of formats) {
|
|
63
|
+
if (!allowed.has(format)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`[mirin] invalid Linux package format "${format}" in ${source}; expected appimage, deb, or rpm.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!out.includes(format as LinuxPackageFormat)) out.push(format as LinuxPackageFormat);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|