@bobfrankston/msger 0.1.353 → 0.1.357

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.
Binary file
@@ -19,7 +19,8 @@ Edit `builder/build-config.json` to control which platforms are built:
19
19
  "windows": true, // Build Windows x64 binary
20
20
  "wsl": false, // Build Linux x64 binary (via WSL)
21
21
  "pi": false, // Build Raspberry Pi ARM64 binary (remote build on Pi)
22
- "arm64": false // Build Linux ARM64 binary (cross-compile via WSL)
22
+ "arm64": false, // Build Linux ARM64 binary (cross-compile via WSL)
23
+ "mac": false // Sync Mac binaries from OneDrive `mac/` staging into bin/ at publish time
23
24
  },
24
25
  "options": {
25
26
  "release": true, // Use release mode (optimized)
@@ -38,6 +39,7 @@ Edit `builder/build-config.json` to control which platforms are built:
38
39
  | `wsl` | `bin/msgernative` | Native build in WSL | WSL with Rust toolchain |
39
40
  | `pi` | `bin/msgernative-linux-aarch64` | **Remote build on Pi** (slower, guaranteed compatible) | SSH access to Raspberry Pi |
40
41
  | `arm64` | `bin/msgernative-linux-aarch64` | **Cross-compile on WSL** (faster, requires setup) | WSL with ARM64 cross-compilation toolchain |
42
+ | `mac` | `bin/msgernative-darwin-{arm64,x64}` | Sync from `mac/` (OneDrive staging) at publish time — Mac builds happen on the Mac via `mac/build-mac.sh` | A Mac (or stale OneDrive copy) |
41
43
 
42
44
  **Pi vs ARM64:**
43
45
  - **`pi`**: Builds on the actual Raspberry Pi via SSH - slower but guaranteed to work
@@ -1,14 +1,19 @@
1
+ // msger-native build configuration (JSONC — comments stripped before parse).
2
+ // Each platform flag toggles whether that target is built/synced; defaults are
3
+ // conservative so a fresh checkout only builds the local Windows binary.
1
4
  {
2
5
  "platforms": {
3
- "windows": true,
4
- "wsl": false,
5
- "pi": false,
6
- "arm64": false
6
+ "windows": true, // Native build on Windows via cargo (msgernative.exe)
7
+ "wsl": false, // Cross-build Linux x64 via WSL (msgernative-linux)
8
+ "pi": false, // Remote SSH build on a Raspberry Pi (msgernative-linux-aarch64)
9
+ "arm64": false, // Cross-compile Linux ARM64 via WSL (msgernative-linux-aarch64)
10
+ "mac": false // Sync mac/msgernative-darwin-{arm64,x64} into bin/ at publish time
11
+ // (Mac binaries are produced on a Mac via mac/build-mac.sh)
7
12
  },
8
13
  "options": {
9
- "release": true,
10
- "verbose": true,
11
- "piHost": "pi4c",
12
- "piProjectPath": "/home/pi/msger/msger-native"
14
+ "release": true, // cargo --release (optimized) vs debug
15
+ "verbose": true, // Stream cargo output instead of capturing
16
+ "piHost": "pi4c", // SSH host alias for the `pi` platform
17
+ "piProjectPath": "/home/pi/msger/msger-native" // Where source is staged on the Pi
13
18
  }
14
19
  }
@@ -3,20 +3,20 @@
3
3
  * msger-native WSL builder — delegates to @bobfrankston/rust-builder
4
4
  * Run this directly inside WSL: node builder/build-under-wsl.ts
5
5
  */
6
- import { loadConfig, build } from "@bobfrankston/rust-builder";
6
+ import { build, type BuildConfig } from "@bobfrankston/rust-builder";
7
7
  import path from "path";
8
+ import fs from "fs";
8
9
  import { fileURLToPath } from "url";
10
+ import JSON5 from "json5";
9
11
 
10
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
13
  const nativeDir = path.join(__dirname, "..");
12
14
  const binDir = path.join(nativeDir, "bin");
13
15
 
14
- const config = loadConfig(
15
- path.join(__dirname, "build-config.json"),
16
- "msgernative",
17
- nativeDir,
18
- binDir
19
- );
16
+ // build-config.json may contain comments — parse with json5 instead of using
17
+ // rust-builder's loadConfig (which is strict JSON-only).
18
+ const json = JSON5.parse(fs.readFileSync(path.join(__dirname, "build-config.json"), "utf-8"));
19
+ const config: BuildConfig = { binaryName: "msgernative", cargoDir: nativeDir, binDir, ...json };
20
20
 
21
21
  // Only build WSL targets when running inside WSL
22
22
  config.platforms.windows = false;
@@ -2,20 +2,20 @@
2
2
  /**
3
3
  * msger-native builder — delegates to @bobfrankston/rust-builder
4
4
  */
5
- import { loadConfig, build } from "@bobfrankston/rust-builder";
5
+ import { build, type BuildConfig } from "@bobfrankston/rust-builder";
6
6
  import path from "path";
7
+ import fs from "fs";
7
8
  import { fileURLToPath } from "url";
9
+ import JSON5 from "json5";
8
10
 
9
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
12
  const nativeDir = path.join(__dirname, "..");
11
13
  const binDir = path.join(nativeDir, "bin");
12
14
 
13
- const config = loadConfig(
14
- path.join(__dirname, "build-config.json"),
15
- "msgernative",
16
- nativeDir,
17
- binDir
18
- );
15
+ // build-config.json may contain comments — parse with json5 instead of using
16
+ // rust-builder's loadConfig (which is strict JSON-only).
17
+ const json = JSON5.parse(fs.readFileSync(path.join(__dirname, "build-config.json"), "utf-8"));
18
+ const config: BuildConfig = { binaryName: "msgernative", cargoDir: nativeDir, binDir, ...json };
19
19
 
20
20
  // msger-specific: copy icons
21
21
  config.options = config.options || {};
@@ -27,7 +27,11 @@ if (isWindows) {
27
27
  baseName = "msgernative";
28
28
  ext = ".exe";
29
29
  } else if (process.platform === "darwin") {
30
- baseName = arch === "arm64" ? "msgernative-arm64" : "msgernative";
30
+ // Mac binaries use explicit darwin names so they don't collide with the
31
+ // Pi (`msgernative-arm64`) or generic Linux ARM (`msgernative-linux-aarch64`)
32
+ // binaries. Built on a Mac via build-mac.sh, synced via OneDrive's `mac/`,
33
+ // copied into bin/ at prepublishOnly time.
34
+ baseName = arch === "arm64" ? "msgernative-darwin-arm64" : "msgernative-darwin-x64";
31
35
  ext = "";
32
36
  } else {
33
37
  baseName = arch === "arm64" ? "msgernative-linux-aarch64" : "msgernative";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/msger",
3
- "version": "0.1.353",
3
+ "version": "0.1.357",
4
4
  "description": "Fast, lightweight, cross-platform message box - Rust-powered alternative to msgview",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -21,7 +21,8 @@
21
21
  "clean": "node msger-native/builder/clean.ts",
22
22
  "test": "node test.js",
23
23
  "postinstall": "node msger-native/builder/postinstall.js",
24
- "prepublishOnly": "npm run build:ts && npm run build:native",
24
+ "prep:mac": "node prep-mac-source.js",
25
+ "prepublishOnly": "node sync-mac-binary.js && npm run build:ts && npm run build:native",
25
26
  "prerelease:local": "git add -A && (git diff-index --quiet HEAD || git commit -m \"Pre-release commit\")",
26
27
  "preversion": "npm run build:ts && npm run build:native && git add -A",
27
28
  "release": "npm run prerelease:local && npm version patch && npm publish --quiet",
@@ -51,8 +52,7 @@
51
52
  "dependencies": {
52
53
  "@bobfrankston/msgcommon": "^0.1.23",
53
54
  "ansi-to-html": "^0.7.2",
54
- "json5": "^2.2.3",
55
- "rcedit": "^4.0.1"
55
+ "json5": "^2.2.3"
56
56
  },
57
57
  "files": [
58
58
  "*.js",
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * prep-mac-source.js — copy `msger-native/` Rust source into `mac/msger-native/`
4
+ * so the Mac (which sees `mac/` via OneDrive) has source to build from.
5
+ *
6
+ * Run from Windows whenever the Rust source changes and you want the Mac to
7
+ * pick it up on its next `./build-mac.sh`. Idempotent — only copies files that
8
+ * are missing or newer.
9
+ *
10
+ * Excludes `target/` (build output, gigabytes), `pibuild/` (Pi-specific),
11
+ * and the existing `bin/` (Linux/Windows binaries — Mac doesn't need them).
12
+ */
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import { fileURLToPath } from "url";
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const srcDir = path.join(__dirname, "msger-native");
19
+ const dstDir = path.join(__dirname, "mac", "msger-native");
20
+
21
+ const SKIP_DIRS = new Set(["target", "pibuild", "bin", "node_modules", ".git"]);
22
+
23
+ if (!fs.existsSync(srcDir)) {
24
+ console.error(`✗ msger-native/ not found at ${srcDir}`);
25
+ process.exit(1);
26
+ }
27
+ if (!fs.existsSync(path.dirname(dstDir))) {
28
+ console.error(`✗ mac/ not found — make sure the OneDrive junction is in place`);
29
+ process.exit(1);
30
+ }
31
+
32
+ let copied = 0;
33
+ let skipped = 0;
34
+
35
+ function copyDir(src, dst) {
36
+ fs.mkdirSync(dst, { recursive: true });
37
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
38
+ const srcPath = path.join(src, entry.name);
39
+ const dstPath = path.join(dst, entry.name);
40
+ if (entry.isDirectory()) {
41
+ if (SKIP_DIRS.has(entry.name)) continue;
42
+ copyDir(srcPath, dstPath);
43
+ } else if (entry.isFile()) {
44
+ const srcMtime = fs.statSync(srcPath).mtimeMs;
45
+ const dstMtime = fs.existsSync(dstPath) ? fs.statSync(dstPath).mtimeMs : 0;
46
+ if (dstMtime >= srcMtime) {
47
+ skipped++;
48
+ continue;
49
+ }
50
+ fs.copyFileSync(srcPath, dstPath);
51
+ copied++;
52
+ }
53
+ }
54
+ }
55
+
56
+ copyDir(srcDir, dstDir);
57
+
58
+ console.log(` msger: prep:mac — ${copied} file(s) copied, ${skipped} unchanged`);
59
+ console.log(` msger: source staged at mac/msger-native/. OneDrive will sync to the Mac;`);
60
+ console.log(` msger: then run \`./build-mac.sh\` from the OneDrive folder on the Mac.`);
package/shower.d.ts CHANGED
@@ -39,6 +39,8 @@ export interface MessageBoxOptions {
39
39
  reset?: boolean; /** Clear localStorage on startup (default: false) */
40
40
  icon?: string; /** Path to window icon. Decoded for the runtime title-bar / taskbar icon. PNG decodes most reliably; the `image` crate's ICO+PNG path is fragile. */
41
41
  relaunchIcon?: string; /** Optional path forwarded to `PKEY_AppUserModel_RelaunchIconResource` so pinned taskbar shortcuts get the app's own icon. Must be a `.ico`. Path is consumed verbatim (no decode). */
42
+ relaunchCommand?: string; /** Command Windows runs when the user clicks the pinned shortcut (`PKEY_AppUserModel_RelaunchCommand`). Critical for msger-hosted apps: without it, the pin captures the bare `mailx.exe` (a webview host that expects JSON stdin) and clicking it does nothing. */
43
+ relaunchDisplayName?: string; /** Display name for the pinned shortcut (`PKEY_AppUserModel_RelaunchDisplayNameResource`). */
42
44
  dev?: boolean; /** Open DevTools automatically on startup (default: false) */
43
45
  debug?: boolean; /** Return debug information (HTML, size) in result (default: false) */
44
46
  showVersion?: boolean; /** Show version in window title (default: false) */
package/shower.js CHANGED
@@ -2,7 +2,6 @@ import { spawn, execFileSync } from 'child_process';
2
2
  import { platform } from 'os';
3
3
  import path from 'path';
4
4
  import { pathToFileURL } from 'url';
5
- import { createRequire } from 'module';
6
5
  import fs from 'fs';
7
6
  import AnsiToHtml from 'ansi-to-html';
8
7
  // Load package.json for version
@@ -90,11 +89,24 @@ function getUserBinDir() { return getUserBinDirFor(_appName); }
90
89
  * bundled binary. Returns null only if msger truly isn't installed. */
91
90
  function findMsgerSource() {
92
91
  const isWindows = platform() === 'win32';
92
+ const isDarwin = platform() === 'darwin';
93
93
  const arch = process.arch;
94
94
  const ext = isWindows ? '.exe' : '';
95
- const baseName = (!isWindows && arch === 'arm64')
96
- ? 'msgernative-linux-aarch64'
97
- : 'msgernative';
95
+ let baseName;
96
+ if (isWindows) {
97
+ baseName = 'msgernative';
98
+ }
99
+ else if (isDarwin) {
100
+ // Mac binaries are built infrequently on a Mac and synced via OneDrive.
101
+ // Names are explicit so they don't collide with the Pi/Linux ARM binaries.
102
+ baseName = arch === 'arm64' ? 'msgernative-darwin-arm64' : 'msgernative-darwin-x64';
103
+ }
104
+ else if (arch === 'arm64') {
105
+ baseName = 'msgernative-linux-aarch64';
106
+ }
107
+ else {
108
+ baseName = 'msgernative';
109
+ }
98
110
  const msgerBin = getUserBinDirFor('msger');
99
111
  const pkgBin = path.join(import.meta.dirname, 'msger-native', 'bin');
100
112
  const stable = path.join(msgerBin, `${baseName}${ext}`);
@@ -134,56 +146,21 @@ function sweepAsideSiblings(perAppExe) {
134
146
  }
135
147
  catch { /* fall through */ }
136
148
  }
137
- /** Locate the rcedit-<arch>.exe shipped with the `rcedit` npm package
138
- * (Electron-team maintained, published unscoped). Returns null on
139
- * non-Windows or if the package isn't installed. We resolve the path
140
- * manually so we can call the binary with `execFileSync` and keep the
141
- * caller synchronous. */
142
- function findRceditBinary() {
143
- if (platform() !== "win32")
144
- return null;
145
- try {
146
- // The package ships rcedit-x64.exe / rcedit-ia32.exe / rcedit-arm64.exe
147
- // in its `bin/` dir. Resolve via require.resolve('rcedit') → walk up
148
- // to the package root, then into bin/.
149
- const pkgEntry = createRequire(import.meta.url).resolve("rcedit");
150
- const pkgRoot = path.dirname(pkgEntry);
151
- const archMap = {
152
- x64: "rcedit-x64.exe",
153
- ia32: "rcedit-ia32.exe",
154
- arm64: "rcedit-arm64.exe",
155
- };
156
- const exeName = archMap[process.arch] || "rcedit-x64.exe";
157
- const candidates = [
158
- path.join(pkgRoot, "bin", exeName),
159
- path.join(pkgRoot, "..", "bin", exeName),
160
- ];
161
- for (const c of candidates) {
162
- if (fs.existsSync(c))
163
- return c;
164
- }
165
- }
166
- catch { /* not installed — caller will fall back to plain copy */ }
167
- return null;
168
- }
169
149
  /** Inject `.ico` into `exePath` as the IDI_ICON1 / RT_GROUP_ICON resource so
170
150
  * the per-app exe shows the app's own icon in Explorer, Task Manager, the
171
151
  * taskbar, and pinned shortcuts — every Windows context that reads the exe
172
- * resource (which `with_window_icon` doesn't reach). Sync — fires the
173
- * bundled rcedit.exe via `execFileSync`. Returns true on success. Failures
174
- * don't break launch the launcher just runs without the embedded icon. */
175
- function injectIcon(exePath, icoPath) {
176
- const rcedit = findRceditBinary();
177
- if (!rcedit) {
178
- console.error(` msger: rcedit binary not found — per-app icon not injected (install \`rcedit\` npm package)`);
179
- return false;
180
- }
152
+ * resource (which `with_window_icon` doesn't reach). Sync — invokes the
153
+ * shipped `msgernative.exe --update-icon` subcommand (no external deps;
154
+ * msger stays self-contained). Returns true on success. Failures don't
155
+ * break launch — the launcher just runs without the embedded icon. */
156
+ function injectIcon(msgerNative, exePath, icoPath) {
181
157
  try {
182
- execFileSync(rcedit, [exePath, "--set-icon", icoPath], { stdio: "pipe" });
158
+ execFileSync(msgerNative, ["--update-icon", exePath, icoPath], { stdio: "pipe" });
183
159
  return true;
184
160
  }
185
161
  catch (e) {
186
- console.error(` msger: rcedit failed for ${exePath}: ${e?.message || e}`);
162
+ const stderr = (e?.stderr ?? "").toString().trim();
163
+ console.error(` msger: --update-icon failed for ${exePath}: ${e?.message || e}${stderr ? ` (${stderr})` : ""}`);
187
164
  return false;
188
165
  }
189
166
  }
@@ -191,7 +168,7 @@ function injectIcon(exePath, icoPath) {
191
168
  * with the per-app icon resource injected. Two modes:
192
169
  *
193
170
  * - `_appIcon` is set (the per-app-identity case): the per-app exe is a
194
- * *real copy*, not a hardlink, because `rcedit` rewrites the resource
171
+ * *real copy*, not a hardlink, because `--update-icon` rewrites the resource
195
172
  * section and we don't want that change to bleed back into the shared
196
173
  * `msgernative.exe` (which a hardlink would). Staleness compared via
197
174
  * mtime of source exe + mtime of source icon. Refresh uses the
@@ -231,7 +208,7 @@ function ensureFresh(perAppExe, sourceExe) {
231
208
  sweepAsideSiblings(perAppExe);
232
209
  return;
233
210
  }
234
- // Copy + rcedit path. Stale when the per-app exe is missing OR the source
211
+ // Copy + --update-icon path. Stale when the per-app exe is missing OR the source
235
212
  // exe is newer (msger updated) OR the source icon is newer (icon changed).
236
213
  const srcMtime = fs.statSync(sourceExe).mtimeMs;
237
214
  const icoMtime = fs.statSync(_appIcon).mtimeMs;
@@ -243,7 +220,7 @@ function ensureFresh(perAppExe, sourceExe) {
243
220
  if (!stale)
244
221
  return;
245
222
  // Build the new exe out-of-band so a running instance never sees a
246
- // half-rcedit'd file. Sequence: copy → rcedit tmp → rename old aside →
223
+ // half---update-icon'd file. Sequence: copy → --update-icon tmp → rename old aside →
247
224
  // rename tmp into place. All renames work even on locked files; only
248
225
  // delete is blocked, and we defer that to the next launch's sweep.
249
226
  const stamp = Date.now();
@@ -251,14 +228,14 @@ function ensureFresh(perAppExe, sourceExe) {
251
228
  const isIco = _appIcon.toLowerCase().endsWith(".ico");
252
229
  try {
253
230
  fs.copyFileSync(sourceExe, tmpExe);
254
- let rceditOk = false;
231
+ let injected = false;
255
232
  if (isIco) {
256
- rceditOk = injectIcon(tmpExe, _appIcon);
233
+ injected = injectIcon(sourceExe, tmpExe, _appIcon);
257
234
  }
258
- if (!isIco || !rceditOk) {
259
- // No .ico available, or rcedit couldn't inject — fall back to a
260
- // plain copy. Window-icon path still gets set via Tao at launch;
261
- // we just don't get the EXE-level resource.
235
+ if (!isIco || !injected) {
236
+ // No .ico available, or the resource update failed — fall back
237
+ // to a plain copy. Window-icon path still gets set via Tao at
238
+ // launch; we just don't get the EXE-level resource.
262
239
  }
263
240
  if (fs.existsSync(perAppExe)) {
264
241
  try {
@@ -312,6 +289,7 @@ function provisionAppIcon(perAppDir) {
312
289
  * own taskbar identity. */
313
290
  function resolveBinaryPath() {
314
291
  const isWindows = platform() === 'win32';
292
+ const isDarwin = platform() === 'darwin';
315
293
  const arch = process.arch;
316
294
  const pkgBinDir = path.join(import.meta.dirname, 'msger-native', 'bin');
317
295
  const userBinDir = getUserBinDir();
@@ -321,6 +299,10 @@ function resolveBinaryPath() {
321
299
  baseName = 'msgernative';
322
300
  ext = '.exe';
323
301
  }
302
+ else if (isDarwin) {
303
+ baseName = arch === 'arm64' ? 'msgernative-darwin-arm64' : 'msgernative-darwin-x64';
304
+ ext = '';
305
+ }
324
306
  else if (arch === 'arm64') {
325
307
  baseName = 'msgernative-linux-aarch64';
326
308
  ext = '';
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sync-mac-binary.js — copy Mac binaries from the OneDrive-synced `mac/`
4
+ * staging area into `msger-native/bin/` so they ship in the next npm publish.
5
+ *
6
+ * Workflow:
7
+ * - On a Mac: run `build-mac.sh`. It compiles, ad-hoc codesigns, and drops
8
+ * the binary into `mac/msgernative-darwin-{arm64,x64}`.
9
+ * - OneDrive syncs that file back to Windows.
10
+ * - This script (run by `prepublishOnly`) copies it into `msger-native/bin/`.
11
+ *
12
+ * Gated by `platforms.mac` in builder/build-config.json (parallel to `pi`).
13
+ * Skipped entirely when false so a publish doesn't refresh Mac binaries — useful
14
+ * when no Mac is available, or to avoid pulling in a stale OneDrive copy.
15
+ *
16
+ * Tolerant of a missing `mac/` or missing binaries — Mac is built rarely, and
17
+ * a publish without a current Mac binary is fine. Mac users on that version
18
+ * will get a clear "Binary not found" error and can flag it for a rebuild.
19
+ */
20
+ import fs from "fs";
21
+ import path from "path";
22
+ import { fileURLToPath } from "url";
23
+ import JSON5 from "json5";
24
+
25
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
+ const macDir = path.join(__dirname, "mac");
27
+ const binDir = path.join(__dirname, "msger-native", "bin");
28
+ const configPath = path.join(__dirname, "msger-native", "builder", "build-config.json");
29
+
30
+ // build-config.json may contain comments — use json5 (already a msger dep).
31
+ const config = JSON5.parse(fs.readFileSync(configPath, "utf-8"));
32
+ if (!config.platforms?.mac) {
33
+ console.log(" msger: platforms.mac=false in build-config.json — skipping Mac binary sync");
34
+ process.exit(0);
35
+ }
36
+
37
+ const targets = ["msgernative-darwin-arm64", "msgernative-darwin-x64"];
38
+
39
+ if (!fs.existsSync(macDir)) {
40
+ console.log(" msger: no mac/ staging dir — skipping Mac binary sync");
41
+ process.exit(0);
42
+ }
43
+
44
+ let copied = 0;
45
+ let skipped = 0;
46
+ for (const name of targets) {
47
+ const src = path.join(macDir, name);
48
+ const dst = path.join(binDir, name);
49
+ if (!fs.existsSync(src)) continue;
50
+
51
+ const srcMtime = fs.statSync(src).mtimeMs;
52
+ const dstMtime = fs.existsSync(dst) ? fs.statSync(dst).mtimeMs : 0;
53
+ if (dstMtime >= srcMtime) {
54
+ console.log(` msger: ${name} already current in bin/`);
55
+ skipped++;
56
+ continue;
57
+ }
58
+
59
+ try {
60
+ fs.copyFileSync(src, dst);
61
+ fs.chmodSync(dst, 0o755);
62
+ const sizeMB = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
63
+ console.log(` msger: synced ${name} (${sizeMB} MB) → bin/`);
64
+ copied++;
65
+ } catch (e) {
66
+ console.error(` msger: failed to sync ${name}: ${e.message}`);
67
+ process.exit(1);
68
+ }
69
+ }
70
+
71
+ if (copied === 0 && skipped === 0) {
72
+ console.log(" msger: no Mac binaries in mac/ — Mac users will get \"Binary not found\" until you build one");
73
+ }