@mirinjs/cli 0.1.0-alpha.2 → 0.1.0-alpha.22

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.
Files changed (39) hide show
  1. package/package.json +8 -5
  2. package/src/artifacts.ts +165 -26
  3. package/src/build.ts +236 -43
  4. package/src/bundle/linux/app.ts +275 -0
  5. package/src/bundle/linux/index.ts +1 -0
  6. package/src/{bundle.ts → bundle/macos/app.ts} +65 -23
  7. package/src/bundle/macos/index.ts +1 -0
  8. package/src/bundle/shared/cef-locales.ts +91 -0
  9. package/src/{bundle-win.ts → bundle/windows/app.ts} +10 -6
  10. package/src/bundle/windows/index.ts +1 -0
  11. package/src/check.ts +271 -0
  12. package/src/dev.ts +214 -35
  13. package/src/dmg.ts +22 -10
  14. package/src/doctor.ts +227 -0
  15. package/src/extras.ts +52 -13
  16. package/src/icons/macos/appearance.ts +96 -0
  17. package/src/icons/macos/index.ts +1 -0
  18. package/src/icons/sources.ts +25 -0
  19. package/src/icons/windows/icon.ts +255 -0
  20. package/src/icons/windows/index.ts +1 -0
  21. package/src/index.ts +109 -7
  22. package/src/installer-inno.ts +141 -0
  23. package/src/installer-win.ts +17 -11
  24. package/src/package/linux/index.ts +2 -0
  25. package/src/package/linux/lib/defaults.ts +53 -0
  26. package/src/package/linux/lib/tools.ts +45 -0
  27. package/src/package/linux/lib/validation.ts +71 -0
  28. package/src/package/linux/package.ts +293 -0
  29. package/src/package/linux/types.ts +30 -0
  30. package/src/release/installers.ts +148 -0
  31. package/src/release/previous.ts +105 -0
  32. package/src/release.ts +166 -116
  33. package/src/shared/driver.ts +240 -0
  34. package/src/shared/inspector.ts +64 -0
  35. package/src/shared/report.ts +69 -0
  36. package/src/shared/scenario.ts +58 -0
  37. package/src/shared/session.ts +153 -0
  38. package/src/skill.ts +51 -0
  39. package/src/icon-win.ts +0 -81
@@ -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
+ }
@@ -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 { join, isAbsolute } from "node:path";
16
+ import { isAbsolute, join } from "node:path";
17
+ import { $ } from "bun";
18
18
  import type { NsisConfig } from "mirinjs";
19
- import { makeWindowsIcon } from "./icon-win.ts";
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 } = input;
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 ?? (perMachine ? `$PROGRAMFILES64\\${appName}` : `$LOCALAPPDATA\\Programs\\${appName}`);
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(resolve(projectDir, options.installerIcon), join(outDir, "_installer.ico"));
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(`!define MUI_ABORTWARNING`);
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) => L.push(` WriteRegStr ${root} "${key}" "${name}" "${val}"`);
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", '$INSTDIR\\Uninstall.exe');
137
- reg("QuietUninstallString", '$INSTDIR\\Uninstall.exe /S');
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("Section \"Uninstall\"");
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,2 @@
1
+ export * from "./package.ts";
2
+ export * from "./types.ts";
@@ -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
+ }