Migrate to Electrobun 2.0.2 (Hutch)

Electrobun 2.x replaces the vendored CLI with Hutch: the npm package is now a
bootstrap whose exports are a throwing stub, and the SDK is projected into
.hutch/devkit. The app stays on the Bun main process — `mainProcess: "bun"` is
load-bearing, since the 2.x default (Cottontail) ignores build.bun and with it
the ssh2 externals — and the bundle now ships the pinned Bun 1.4.0.

- electrobun 2.0.2 as an exact devDependency. `electrobun prepare` takes the
  old CLI re-sign step's slot in postinstall and every dev/build/start/release
  script: tsc (root tsconfig extends the devkit's) and the Angular build
  (electrobun/view mapped into the devkit) resolve the SDK there.
- Imports move to electrobun/main. The CLI re-sign script and the v1 "three"
  type shim are gone (Hutch's binaries are validly signed).
- UI: Electroview's bunSocket is hostSocket; noImplicitReturns is off because
  the devkit's raw-TS browser SDK compiles inside the UI program.
- Updater.applyUpdate() now consults before-quit, so the transfers/unsaved
  guard's "Quit Anyway" re-runs the apply instead of quitting without it.
- 2.x applies trafficLightOffset from macOS's default button position (1.18.1
  clamped it inside the titlebar); {12, 4} centres the lights in the header.
- Releases: mac-x64 dropped (2.x publishes no macOS x64 core, so no Rosetta
  path). artifacts/ is emptied before each build and published verbatim.
  Installers lost the stable- prefix (the update feed kept it), so release.ts,
  deploy.sh and the site links follow. Linux ships Electrobun's Setup
  installer instead of the portable tarball and its miniSCP.sh wrapper.
- Docs cover all of the above, plus re-checks against 2.0.2: still no native
  modal windows; window close is now cancellable through will-close.

Verified: tsc, build:ui, a stable mac-arm64 release (dmg + the v1-compatible
stable-macos-arm64 feed) and dev launches (RPC, Settings window, traffic
lights). Windows/Linux builds and the v1 -> 2.x Windows update bridge still
need their first run on native machines.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-10-05 11:08:03 +02:00
co-authored by Claude
parent 9bd237d89c
commit 0eb5608873
30 changed files with 431 additions and 447 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { Utils, type BrowserWindow } from "electrobun/bun";
import { Utils, type BrowserWindow } from "electrobun/main";
import { BRANDING } from "../shared/branding";
import type { EditorOpenContext } from "../shared/rpc";
import type { AppSettings } from "../shared/types";
+19 -2
View File
@@ -1,4 +1,4 @@
import Electrobun, { ApplicationMenu, BrowserWindow, Updater, Utils } from "electrobun/bun";
import Electrobun, { ApplicationMenu, BrowserWindow, Updater, Utils } from "electrobun/main";
import { BRANDING } from "../shared/branding";
import type { EditorOpenContext } from "../shared/rpc";
import { effectiveConcurrency, type AppSettings } from "../shared/types";
@@ -55,6 +55,7 @@ const events = {
openConnectionManager: () => {},
openSettings: () => {},
showAbout: () => {},
applyUpdate: async () => {},
};
const rpc = createAppRPC({ registry, engine, store, settings, edits, watcher, events });
// late-bound like events: a watched local dir changed -> tell the main webview
@@ -211,11 +212,24 @@ engine.onUpdate = (snapshot) => {
// don't quit out from under running transfers or unsaved editors
let quitConfirmed = false;
// Updater.applyUpdate() asks this guard first (2.x), so remember the quit is
// update-initiated: "Quit Anyway" must re-run the apply, or a plain quit would
// drop the downloaded update.
let applyingUpdate = false;
events.applyUpdate = async () => {
applyingUpdate = true;
try {
await Updater.applyUpdate();
} finally {
applyingUpdate = false;
}
};
Electrobun.events.on("before-quit", (event) => {
const transfers = engine.activeJobCount();
const dirtyEditors = editors.dirtyCount();
if (quitConfirmed || (transfers === 0 && dirtyEditors === 0)) return;
(event as { response: { allow: boolean } }).response = { allow: false };
const updating = applyingUpdate; // read now: it clears once the vetoed apply returns
void (async () => {
const parts = [
transfers > 0 && `${transfers} transfer${transfers === 1 ? " is" : "s are"} still running`,
@@ -233,7 +247,10 @@ Electrobun.events.on("before-quit", (event) => {
});
if (response === 0) {
quitConfirmed = true;
Utils.quit();
// the trailing quit is a no-op once the apply is quitting, and still
// quits if it bails (no prepared download)
if (updating) void Updater.applyUpdate().finally(() => Utils.quit());
else Utils.quit();
}
})();
});
+1 -1
View File
@@ -1,4 +1,4 @@
import type { BrowserWindow } from "electrobun/bun";
import type { BrowserWindow } from "electrobun/main";
import type { AppSettings } from "../shared/types";
import { createManagerRPC } from "./rpc-handlers";
import type { ConnectionStore } from "./store/connections";
+1 -1
View File
@@ -1,4 +1,4 @@
import { ApplicationMenu, type ApplicationMenuItemConfig } from "electrobun/bun";
import { ApplicationMenu, type ApplicationMenuItemConfig } from "electrobun/main";
import { BRANDING } from "../shared/branding";
import type { SavedConnection } from "../shared/types";
import { t } from "./i18n";
+4 -2
View File
@@ -1,4 +1,4 @@
import { BrowserView, Updater, Utils } from "electrobun/bun";
import { BrowserView, Updater, Utils } from "electrobun/main";
import type { AppRPC, EditorOpenContext } from "../shared/rpc";
import type { AppSettings, Result } from "../shared/types";
import { readFileText, writeFileText, type EditManager } from "./edits";
@@ -33,6 +33,8 @@ export interface AppDeps {
openConnectionManager: () => void;
openSettings: () => void;
showAbout: () => void;
/** "Restart to apply" — lives next to the before-quit guard it must get past. */
applyUpdate: () => Promise<void>;
};
}
@@ -352,7 +354,7 @@ export function createAppRPC({
applyUpdate: () =>
wrap(async () => {
await Updater.applyUpdate(); // relaunches + quits; may never resolve
await events.applyUpdate(); // relaunches + quits; may never resolve
return {};
}),
},
+1 -1
View File
@@ -1,4 +1,4 @@
import type { BrowserWindow } from "electrobun/bun";
import type { BrowserWindow } from "electrobun/main";
import type { AppSettings } from "../shared/types";
import { t } from "./i18n";
import { createSettingsRPC } from "./rpc-handlers";
+1 -1
View File
@@ -1,4 +1,4 @@
import { Tray, type BrowserWindow } from "electrobun/bun";
import { Tray, type BrowserWindow } from "electrobun/main";
/**
* Menu-bar / notification-area icon. Clicking it brings the app window to the
+7 -2
View File
@@ -1,4 +1,4 @@
import { BrowserWindow } from "electrobun/bun";
import { BrowserWindow } from "electrobun/main";
import type { AppSettings } from "../shared/types";
/**
@@ -44,10 +44,15 @@ export function buildPreload(
* lights overlay it, which is what IS_MAC_CHROME reserves pl-24 for); other
* platforms keep the native titlebar. Constructor-only: there is no API to
* re-decorate a live window.
*
* electrobun 2.x adds trafficLightOffset to macOS's default button position
* (9/9 in the 32pt macOS 26 titlebar) and grows the titlebar to fit — 1.18.1
* clamped the shift inside the stock titlebar instead. {12, 4} puts the 14pt
* buttons at left 21 / top 13: vertically centred in the 40px h-10 header.
*/
export function chromeOptions() {
return process.platform === "darwin"
? { titleBarStyle: "hiddenInset" as const, trafficLightOffset: { x: 12, y: 13 } }
? { titleBarStyle: "hiddenInset" as const, trafficLightOffset: { x: 12, y: 4 } }
: {};
}
-3
View File
@@ -1,3 +0,0 @@
// electrobun ships raw .ts sources whose WGPU adapter imports "three" without
// bundled types; we never use that path, so give tsc an any-typed stand-in.
declare module "three";