[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
195
electron/autoLaunch.cjs
Normal file
195
electron/autoLaunch.cjs
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Auto Launch - Registers Netcatty to start at system login, hidden to the
|
||||
* tray. Thin wrapper around Electron's app.setLoginItemSettings/
|
||||
* getLoginItemSettings so main.cjs and the settings IPC handlers share one
|
||||
* source of truth.
|
||||
*
|
||||
* Development runs (`electron .`) are unsupported: process.execPath points
|
||||
* at the transient electron.exe, so a login item registered there would
|
||||
* break (or silently do nothing) after the dev process exits.
|
||||
*
|
||||
* Platform support is further limited to macOS and Windows: Electron's
|
||||
* login-item API is a no-op on Linux, which ships as AppImage/deb/rpm/pacman
|
||||
* with no first-party autostart hook, so reporting it as supported there
|
||||
* would show an enabled toggle that does nothing.
|
||||
*/
|
||||
|
||||
const HIDDEN_LAUNCH_ARG = "--hidden";
|
||||
|
||||
function isAutoLaunchSupported({ defaultApp = process.defaultApp, platform = process.platform } = {}) {
|
||||
if (defaultApp) return false;
|
||||
return platform === "darwin" || platform === "win32";
|
||||
}
|
||||
|
||||
function argsEqual(a, b) {
|
||||
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, index) => value === b[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the specific Windows run-key entry our own writes create (path +
|
||||
* --hidden args), not just any entry for this executable.
|
||||
*/
|
||||
function findMatchingLaunchItem(settings, execPath) {
|
||||
if (!Array.isArray(settings?.launchItems)) return null;
|
||||
return settings.launchItems.find(
|
||||
(item) => item?.path === execPath && argsEqual(item?.args, [HIDDEN_LAUNCH_ARG]),
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the OS's effective auto-launch state, not just whether a
|
||||
* registration exists.
|
||||
*
|
||||
* Windows can retain the run-key entry while the user disables it via Task
|
||||
* Manager's Startup Apps UI. Electron's executableWillLaunchAtLogin surfaces
|
||||
* an effective state too, but it deliberately ignores the args option and
|
||||
* reports true if the executable would launch with ANY arguments — so it
|
||||
* can false-positive on an unrelated no-argument entry for the same exe.
|
||||
* launchItems[].enabled is scoped to one specific path+args registration
|
||||
* (the same combination buildLoginItemQueryOptions queries), so look up our
|
||||
* own --hidden entry there instead and use its enabled flag.
|
||||
*/
|
||||
function resolveEffectiveLoginState(settings, platform, execPath) {
|
||||
if (platform === "win32") {
|
||||
const matchingItem = findMatchingLaunchItem(settings, execPath);
|
||||
if (matchingItem) return Boolean(matchingItem.enabled);
|
||||
// No matching --hidden entry: openAtLogin was queried with the same
|
||||
// path+args (see buildLoginItemQueryOptions), so it is still correctly
|
||||
// scoped to "is that exact entry registered" — false if absent.
|
||||
return Boolean(settings?.openAtLogin);
|
||||
}
|
||||
if (platform === "darwin" && settings?.status === "requires-approval") {
|
||||
// macOS 13+ (SMAppService): a freshly registered login item sits in
|
||||
// "requires-approval" until the user approves it in System Settings,
|
||||
// during which openAtLogin reports false even though registration
|
||||
// itself succeeded. Reporting false here would make the renderer's
|
||||
// push effect see a mismatch against what the user just requested and
|
||||
// immediately fire an opposite write, unregistering the pending item
|
||||
// before the user ever gets a chance to approve it. Treat pending
|
||||
// approval as enabled; a later read naturally reflects the real state
|
||||
// once the user approves it (or the OS drops the pending request).
|
||||
return true;
|
||||
}
|
||||
return Boolean(settings?.openAtLogin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Electron's getLoginItemSettings() only reports openAtLogin for the
|
||||
* specific path+args combination you ask about — it does not mean "is
|
||||
* anything registered for this app". Our code only ever registers a login
|
||||
* item with args:[HIDDEN_LAUNCH_ARG], so every read must query that exact
|
||||
* combination (matching what setAutoLaunchEnabled writes) or Windows
|
||||
* reports a false negative.
|
||||
*/
|
||||
function buildLoginItemQueryOptions(execPath) {
|
||||
return { path: execPath, args: [HIDDEN_LAUNCH_ARG] };
|
||||
}
|
||||
|
||||
function getAutoLaunchEnabled({
|
||||
app,
|
||||
execPath = process.execPath,
|
||||
defaultApp = process.defaultApp,
|
||||
platform = process.platform,
|
||||
} = {}) {
|
||||
if (!isAutoLaunchSupported({ defaultApp, platform })) {
|
||||
return { success: true, enabled: false, supported: false };
|
||||
}
|
||||
try {
|
||||
const settings = app.getLoginItemSettings(buildLoginItemQueryOptions(execPath));
|
||||
return { success: true, enabled: resolveEffectiveLoginState(settings, platform, execPath), supported: true };
|
||||
} catch (err) {
|
||||
console.warn("[AutoLaunch] Failed to read login item settings:", err?.message || err);
|
||||
// success:false signals "OS state unknown" (as opposed to a confirmed
|
||||
// disabled state) so callers — notably the renderer's mount-time
|
||||
// hydration — know not to trust `enabled` here. Blindly applying
|
||||
// enabled:false on a transient read failure would overwrite the
|
||||
// renderer's cached value and cascade into an unwanted disable write.
|
||||
return { success: false, enabled: false, supported: true };
|
||||
}
|
||||
}
|
||||
|
||||
function setAutoLaunchEnabled(enabled, {
|
||||
app,
|
||||
execPath = process.execPath,
|
||||
defaultApp = process.defaultApp,
|
||||
platform = process.platform,
|
||||
} = {}) {
|
||||
if (!isAutoLaunchSupported({ defaultApp, platform })) {
|
||||
// enabled:false is a confirmed fact here (the feature genuinely isn't
|
||||
// available), not an unknown state — matches getAutoLaunchEnabled's
|
||||
// equivalent branch so both functions agree on what `success` means.
|
||||
return { success: true, enabled: false, supported: false };
|
||||
}
|
||||
const wantEnabled = Boolean(enabled);
|
||||
try {
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: wantEnabled,
|
||||
// openAsHidden only applies on macOS App Store builds; Windows relies
|
||||
// on the --hidden arg below, which main.cjs checks on cold start.
|
||||
openAsHidden: wantEnabled,
|
||||
path: execPath,
|
||||
args: wantEnabled ? [HIDDEN_LAUNCH_ARG] : [],
|
||||
});
|
||||
const settings = app.getLoginItemSettings(buildLoginItemQueryOptions(execPath));
|
||||
return { success: true, enabled: resolveEffectiveLoginState(settings, platform, execPath), supported: true };
|
||||
} catch (err) {
|
||||
console.warn("[AutoLaunch] Failed to update login item settings:", err?.message || err);
|
||||
// `success` means "is `enabled` trustworthy", not "did the write
|
||||
// succeed" — the write itself failing does not make a subsequent,
|
||||
// independent read failing too. Surface whatever the fallback read
|
||||
// determines: if it succeeds, `enabled` reflects the real (unwritten)
|
||||
// state and the renderer's push effect correctly rolls the optimistic
|
||||
// toggle back instead of leaving it stuck on a change that never
|
||||
// actually happened. Only a genuine double failure (write AND fallback
|
||||
// read both throw) reports success:false, so the renderer preserves its
|
||||
// last-known state instead of guessing.
|
||||
const fallback = getAutoLaunchEnabled({ app, execPath, defaultApp, platform });
|
||||
return { success: fallback.success, enabled: fallback.enabled, supported: true };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when this process was launched by the OS login item (cold start
|
||||
* only). Windows relies on the --hidden arg (macOS never puts args from
|
||||
* setLoginItemSettings() into argv for a login launch, so it needs a
|
||||
* different signal).
|
||||
*
|
||||
* macOS's own hidden-launch flags (openAsHidden/wasOpenedAsHidden) are
|
||||
* deprecated and stop working on macOS 13+ per Electron's docs, so they
|
||||
* cannot be trusted to detect an actual hidden launch there. wasOpenedAtLogin
|
||||
* still works on 13+ and is not deprecated; since Netcatty only ever
|
||||
* registers a macOS login item to satisfy this "launch hidden" feature (there
|
||||
* is no scenario where it registers one for a normal, visible startup), any
|
||||
* automatic login launch should apply our own hidden-window behavior.
|
||||
*/
|
||||
function wasLaunchedHidden({ argv = process.argv, app, platform = process.platform } = {}) {
|
||||
if (Array.isArray(argv) && argv.includes(HIDDEN_LAUNCH_ARG)) return true;
|
||||
if (platform !== "darwin" || typeof app?.getLoginItemSettings !== "function") return false;
|
||||
try {
|
||||
return Boolean(app.getLoginItemSettings().wasOpenedAtLogin);
|
||||
} catch (err) {
|
||||
console.warn("[AutoLaunch] Failed to read macOS login-item launch state:", err?.message || err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function registerHandlers(ipcMain, { app, platform = process.platform }) {
|
||||
ipcMain.handle("netcatty:autoLaunch:get", async () => {
|
||||
return getAutoLaunchEnabled({ app, platform });
|
||||
});
|
||||
|
||||
ipcMain.handle("netcatty:autoLaunch:set", async (_event, { enabled }) => {
|
||||
return setAutoLaunchEnabled(enabled, { app, platform });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HIDDEN_LAUNCH_ARG,
|
||||
isAutoLaunchSupported,
|
||||
resolveEffectiveLoginState,
|
||||
buildLoginItemQueryOptions,
|
||||
getAutoLaunchEnabled,
|
||||
setAutoLaunchEnabled,
|
||||
wasLaunchedHidden,
|
||||
registerHandlers,
|
||||
};
|
||||
Reference in New Issue
Block a user