diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 6949a15fcff1..9862080841ef 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -129,6 +129,7 @@ import { hiddenWindowsChildOptions } from './windows-child-options' import { readWindowsUserEnvVar } from './windows-user-env' import { isPackagedInstallPath as isPackagedInstallPathUnderRoots } from './workspace-cwd' import { readWslWindowsClipboardImage } from './wsl-clipboard-image' +import { buildPathExtCandidates, chooseUpdaterArgs, resolveVenvHermesCommand } from './windows-hermes-path' const USER_DATA_OVERRIDE = process.env.HERMES_DESKTOP_USER_DATA_DIR @@ -1415,9 +1416,7 @@ function findOnPath(command) { // shell-script shim named `hermes` — must not shadow `hermes.cmd`/`hermes.exe`. // The empty entry is kept LAST so callers that already include the extension // (py.exe, pwsh.exe, powershell.exe) still resolve. - const extensions = IS_WINDOWS - ? [...(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean), ''] - : [''] + const extensions = buildPathExtCandidates(process.env.PATHEXT, IS_WINDOWS) for (const entry of pathEntries) { for (const extension of extensions) { @@ -1437,71 +1436,21 @@ function isCommandScript(command) { } function unwrapWindowsVenvHermesCommand(command, backendArgs) { - if (!IS_WINDOWS || !command || isCommandScript(command)) { - return null - } - - const resolved = path.resolve(String(command)) - - if (!/^hermes(?:\.exe)?$/i.test(path.basename(resolved))) { - return null - } - - const scriptsDir = path.dirname(resolved) - - if (path.basename(scriptsDir).toLowerCase() !== 'scripts') { - return null - } - - const venvRoot = path.dirname(scriptsDir) - const python = getVenvPython(venvRoot) - - if (!fileExists(python)) { - return null - } - - const root = path.dirname(venvRoot) - - // Smoke-test the venv interpreter before trusting it. A venv whose update - // died mid-`pip install` still has python.exe + hermes.exe on disk, but the - // backend dies on its first import (e.g. ModuleNotFoundError: dotenv) before - // the gateway ever binds. Returning it here also BYPASSED the caller's - // `--version` probe, so Retry/"Repair install" re-resolved the same broken - // venv forever instead of falling through to the bootstrap installer. - // Mirror isActiveRuntimeUsable(): probe with the checkout on PYTHONPATH so a - // healthy source-tree venv passes. - if ( - !canImportHermesCli(python, { - env: { - PYTHONPATH: [...(directoryExists(root) ? [root] : []), process.env.PYTHONPATH] - .filter(Boolean) - .join(path.delimiter) - } - }) - ) { - rememberLog( - `Ignoring venv Hermes at ${python}: runtime import probe failed (broken/partial venv); falling through to bootstrap.` - ) - - return null - } - - return { - label: `existing Hermes Python at ${python}`, - command: python, - args: ['-m', 'hermes_cli.main', ...backendArgs], - bootstrap: false, - env: buildDesktopBackendEnv({ - hermesHome: HERMES_HOME, - pythonPathEntries: [...(directoryExists(root) ? [root] : []), ...getVenvSitePackagesEntries(venvRoot)], - venvRoot - }), - kind: 'python', - // Surfaced so backendSupportsServe() can read this runtime's source for the - // `serve` capability check instead of falling back to a heavyweight probe. - root, - shell: false - } + return resolveVenvHermesCommand(command, backendArgs, { + isWindows: IS_WINDOWS, + isCommandScript, + fileExists, + directoryExists, + canImportHermesCli, + getVenvPython, + getVenvSitePackagesEntries, + buildDesktopBackendEnv, + hermesHome: HERMES_HOME, + resolvePath: (...segments) => path.resolve(...segments), + dirname: p => path.dirname(p), + basename: p => path.basename(p), + rememberLog + }) } // Does the resolved runtime understand the `serve` subcommand? The desktop @@ -2656,7 +2605,7 @@ async function handOffWindowsBootstrapRecovery(reason) { const haveRealInstall = fileExists(venvPython) || fileExists(venvHermes) || fileExists(path.join(updateRoot, '.hermes-bootstrap-complete')) - const updaterArgs = haveRealInstall ? ['--update', '--branch', branch] : ['--repair', '--branch', branch] + const updaterArgs = chooseUpdaterArgs(haveRealInstall, branch) await releaseBackendLockForUpdate(updateRoot) diff --git a/apps/desktop/electron/windows-hermes-path.test.ts b/apps/desktop/electron/windows-hermes-path.test.ts new file mode 100644 index 000000000000..a83af63115fb --- /dev/null +++ b/apps/desktop/electron/windows-hermes-path.test.ts @@ -0,0 +1,139 @@ +// Unit tests for the pure Windows `hermes` resolution helpers extracted from +// main.ts's findOnPath(), handOffWindowsBootstrapRecovery(), and +// unwrapWindowsVenvHermesCommand(). These pin the two Windows resolution bugs +// that caused desktop reinstall loops: +// 1. buildPathExtCandidates() — PATHEXT extensions must be tried BEFORE the +// empty extension, or an extensionless Git-Bash `hermes` shim shadows +// the real hermes.cmd/hermes.exe. +// 2. chooseUpdaterArgs() — must gate on haveRealInstall (any real-install +// signal), not just the hermes.exe console-script shim, or healthy +// installs get forced into a destructive --repair. +// 3. resolveVenvHermesCommand() — must probe the venv python via +// canImportHermesCli() before trusting it, or a broken venv gets +// re-selected forever instead of falling through to bootstrap. + +import assert from 'node:assert/strict' +import test from 'node:test' + +import { buildPathExtCandidates, chooseUpdaterArgs, resolveVenvHermesCommand } from './windows-hermes-path' + +test('buildPathExtCandidates: Windows tries PATHEXT extensions before the empty extension', () => { + const extensions = buildPathExtCandidates('.COM;.EXE;.BAT;.CMD', true) + + assert.deepEqual(extensions, ['.COM', '.EXE', '.BAT', '.CMD', '']) + assert.equal(extensions[extensions.length - 1], '', 'empty extension must be last, not first') + assert.notEqual(extensions[0], '', 'the buggy empty-extension-first order must not return') +}) + +test('buildPathExtCandidates: defaults to .COM;.EXE;.BAT;.CMD when PATHEXT is unset on Windows', () => { + assert.deepEqual(buildPathExtCandidates(undefined, true), ['.COM', '.EXE', '.BAT', '.CMD', '']) +}) + +test('buildPathExtCandidates: respects a custom PATHEXT, still empty-last', () => { + assert.deepEqual(buildPathExtCandidates('.EXE;.PS1', true), ['.EXE', '.PS1', '']) +}) + +test('buildPathExtCandidates: non-Windows only tries the bare name', () => { + assert.deepEqual(buildPathExtCandidates('.COM;.EXE;.BAT;.CMD', false), ['']) + assert.deepEqual(buildPathExtCandidates(undefined, false), ['']) +}) + +test('chooseUpdaterArgs: gentle --update when a real-install signal is present', () => { + assert.deepEqual(chooseUpdaterArgs(true, 'main'), ['--update', '--branch', 'main']) +}) + +test('chooseUpdaterArgs: destructive --repair only when NO real-install signal is present', () => { + assert.deepEqual(chooseUpdaterArgs(false, 'main'), ['--repair', '--branch', 'main']) +}) + +test('chooseUpdaterArgs: passes the branch through unchanged in both cases', () => { + assert.deepEqual(chooseUpdaterArgs(true, 'release/1.2'), ['--update', '--branch', 'release/1.2']) + assert.deepEqual(chooseUpdaterArgs(false, 'release/1.2'), ['--repair', '--branch', 'release/1.2']) +}) + +function makeDeps(overrides: Partial[2]> = {}) { + return { + isWindows: true, + isCommandScript: () => false, + fileExists: () => true, + directoryExists: () => false, + canImportHermesCli: () => true, + getVenvPython: (venvRoot: string) => `${venvRoot}/Scripts/python.exe`, + getVenvSitePackagesEntries: () => [], + buildDesktopBackendEnv: () => ({ FAKE_ENV: '1' }), + hermesHome: '/fake/hermes-home', + resolvePath: (...segments: string[]) => segments.join('/').replace(/\/+/g, '/'), + dirname: (p: string) => p.slice(0, p.lastIndexOf('/')) || '/', + basename: (p: string) => p.slice(p.lastIndexOf('/') + 1), + rememberLog: () => {}, + ...overrides + } +} + +test('resolveVenvHermesCommand: returns null off Windows', () => { + const deps = makeDeps({ isWindows: false }) + + assert.equal(resolveVenvHermesCommand('/root/venv/Scripts/hermes.exe', [], deps), null) +}) + +test('resolveVenvHermesCommand: returns null for a .cmd/.bat script command', () => { + const deps = makeDeps({ isCommandScript: () => true }) + + assert.equal(resolveVenvHermesCommand('/root/venv/Scripts/hermes.cmd', [], deps), null) +}) + +test('resolveVenvHermesCommand: returns null when the basename is not hermes/hermes.exe', () => { + const deps = makeDeps() + + assert.equal(resolveVenvHermesCommand('/root/venv/Scripts/python.exe', [], deps), null) +}) + +test('resolveVenvHermesCommand: returns null when the parent dir is not Scripts', () => { + const deps = makeDeps() + + assert.equal(resolveVenvHermesCommand('/root/venv/bin/hermes.exe', [], deps), null) +}) + +test('resolveVenvHermesCommand: returns null when the venv python does not exist on disk', () => { + const deps = makeDeps({ fileExists: () => false }) + + assert.equal(resolveVenvHermesCommand('/root/venv/Scripts/hermes.exe', [], deps), null) +}) + +test('resolveVenvHermesCommand: probes the venv python before trusting it (returns null on failed probe)', () => { + let probed = false + + const deps = makeDeps({ + canImportHermesCli: (python: string) => { + probed = true + assert.equal(python, '/root/venv/Scripts/python.exe') + + return false + } + }) + + const result = resolveVenvHermesCommand('/root/venv/Scripts/hermes.exe', ['serve'], deps) + + assert.equal(probed, true, 'must probe the venv interpreter; a broken venv must not be re-selected forever') + assert.equal(result, null, 'a failed probe must fall through (return null) so the resolver reaches bootstrap') +}) + +test('resolveVenvHermesCommand: returns the resolved python backend descriptor when the probe passes', () => { + const deps = makeDeps() + const result = resolveVenvHermesCommand('/root/venv/Scripts/hermes.exe', ['serve', '--port', '0'], deps) + + assert.ok(result, 'a passing probe must return a backend descriptor, not null') + assert.equal(result.command, '/root/venv/Scripts/python.exe') + assert.deepEqual(result.args, ['-m', 'hermes_cli.main', 'serve', '--port', '0']) + assert.equal(result.bootstrap, false) + assert.equal(result.kind, 'python') + assert.equal(result.shell, false) + assert.deepEqual(result.env, { FAKE_ENV: '1' }) +}) + +test('resolveVenvHermesCommand: is case-insensitive on hermes.exe and the Scripts dir name', () => { + const deps = makeDeps() + + assert.ok(resolveVenvHermesCommand('/root/venv/Scripts/HERMES.EXE', [], deps)) + assert.ok(resolveVenvHermesCommand('/root/venv/SCRIPTS/hermes.exe', [], deps)) +}) diff --git a/apps/desktop/electron/windows-hermes-path.ts b/apps/desktop/electron/windows-hermes-path.ts new file mode 100644 index 000000000000..c78ec614e876 --- /dev/null +++ b/apps/desktop/electron/windows-hermes-path.ts @@ -0,0 +1,206 @@ +/** + * windows-hermes-path.ts + * + * Pure, dependency-injected pieces of Windows `hermes` resolution pulled out + * of main.ts's findOnPath(), handOffWindowsBootstrapRecovery(), and + * unwrapWindowsVenvHermesCommand(). Each of the three functions here pins one + * of the Windows resolution bugs that caused desktop reinstall loops: + * + * 1. buildPathExtCandidates() — findOnPath() tried the empty extension + * FIRST, so an extensionless Git-Bash `hermes` shim shadowed the real + * hermes.cmd/hermes.exe; the shim then failed the --version probe and + * the desktop fell through to a spurious bootstrap/repair. The fix: + * PATHEXT extensions first, empty extension LAST. + * 2. chooseUpdaterArgs() — handOffWindowsBootstrapRecovery() chose + * --update vs the destructive --repair by checking ONLY + * venv\Scripts\hermes.exe (the console-script shim, written at the END + * of venv setup and absent in interrupted states), so it escalated to a + * full venv recreate even on healthy installs. The fix: gate on ANY + * real-install signal, not just the shim. + * 3. resolveVenvHermesCommand() — unwrapWindowsVenvHermesCommand() returned + * the venv python with NO runtime probe (bypassing the caller's + * --version check too), so a venv broken mid-update (e.g. missing + * python-dotenv) was re-selected forever: Retry / "Repair install" + * resolved the same dead interpreter instead of falling through to the + * bootstrap installer. The fix: probe-before-trust. + * + * Kept in a standalone ts module (no Electron imports, dependencies passed + * as parameters) so it can be unit-tested with `node --test` without + * mocking Electron or the filesystem, same pattern as backend-probes.ts and + * backend-command.ts. + */ + +import path from 'node:path' + +/** + * Build the ordered list of extensions findOnPath() should try when + * resolving a bare command name off PATH. + * + * On Windows this MUST try PATHEXT extensions (.COM;.EXE;.BAT;.CMD by + * default) BEFORE the bare/empty-extension name: a real command resolves via + * its .exe/.cmd per Windows command-resolution semantics, and an + * extensionless file (e.g. a Git-Bash shell-script shim named `hermes`) must + * not shadow `hermes.cmd`/`hermes.exe`. The empty entry is kept LAST so + * callers that already include the extension (py.exe, pwsh.exe, + * powershell.exe) still resolve. + * + * On non-Windows platforms there is no PATHEXT concept: only the bare name + * is tried. + * + * @param {string | undefined} pathext - process.env.PATHEXT (or undefined). + * @param {boolean} isWindows + * @returns {string[]} extensions to try, in order, always ending in ''. + */ +export function buildPathExtCandidates(pathext: string | undefined, isWindows: boolean): string[] { + if (!isWindows) { + return [''] + } + + return [...(pathext || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean), ''] +} + +/** + * Choose the Windows bootstrap-recovery updater invocation: the gentle + * in-place --update when ANY real-install signal is present, the + * destructive --repair (full venv recreate) otherwise. + * + * haveRealInstall must be computed by the caller from ALL real-install + * signals (venv python interpreter, venv hermes shim, bootstrap-complete + * marker) — gating on just the hermes.exe console-script shim alone is the + * regression this function's callers must avoid: that shim is written at + * the END of venv setup and is absent in exactly the interrupted/quarantined + * states this recovery exists to heal. + * + * @param {boolean} haveRealInstall + * @param {string} branch + * @returns {string[]} updater argv, e.g. ['--update', '--branch', 'main']. + */ +export function chooseUpdaterArgs(haveRealInstall: boolean, branch: string): string[] { + return haveRealInstall ? ['--update', '--branch', branch] : ['--repair', '--branch', branch] +} + +export interface ResolveVenvHermesCommandDeps { + isWindows: boolean + isCommandScript: (command: string) => boolean + fileExists: (filePath: string) => boolean + directoryExists: (filePath: string) => boolean + canImportHermesCli: (python: string, opts?: { env?: Record }) => boolean + getVenvPython: (venvRoot: string) => string + getVenvSitePackagesEntries: (venvRoot: string) => string[] + buildDesktopBackendEnv: (opts: { + hermesHome: string + pythonPathEntries: string[] + venvRoot: string + }) => Record + hermesHome: string + resolvePath: (...segments: string[]) => string + dirname: (p: string) => string + basename: (p: string) => string + rememberLog?: (message: string) => void +} + +/** + * If `command` is a Windows venv `hermes`/`hermes.exe` console-script shim + * (i.e. `/Scripts/hermes(.exe)`), resolve it to the underlying + * venv python invoked as `python -m hermes_cli.main ` — but + * ONLY after smoke-testing that interpreter with canImportHermesCli(). A + * venv whose update died mid-`pip install` still has python.exe + hermes.exe + * on disk, but the backend dies on its first import (e.g. + * ModuleNotFoundError: dotenv) before the gateway ever binds. Returning it + * unprobed also bypasses the caller's `--version` probe, so Retry/"Repair + * install" re-resolves the same broken venv forever instead of falling + * through to the bootstrap installer. + * + * Mirrors isActiveRuntimeUsable(): probes with the checkout on PYTHONPATH so + * a healthy source-tree venv passes. + * + * Returns null when `command` is not a venv hermes shim, the underlying + * python doesn't exist, or the import probe fails. Otherwise returns the + * resolved backend descriptor. + */ +export function resolveVenvHermesCommand( + command: string, + backendArgs: string[], + deps: ResolveVenvHermesCommandDeps +): { + label: string + command: string + args: string[] + bootstrap: false + env: Record + kind: 'python' + root: string + shell: false +} | null { + const { + isWindows, + isCommandScript, + fileExists, + directoryExists, + canImportHermesCli, + getVenvPython, + getVenvSitePackagesEntries, + buildDesktopBackendEnv, + hermesHome, + resolvePath, + dirname, + basename, + rememberLog + } = deps + + if (!isWindows || !command || isCommandScript(command)) { + return null + } + + const resolved = resolvePath(String(command)) + + if (!/^hermes(?:\.exe)?$/i.test(basename(resolved))) { + return null + } + + const scriptsDir = dirname(resolved) + + if (basename(scriptsDir).toLowerCase() !== 'scripts') { + return null + } + + const venvRoot = dirname(scriptsDir) + const python = getVenvPython(venvRoot) + + if (!fileExists(python)) { + return null + } + + const root = dirname(venvRoot) + + if ( + !canImportHermesCli(python, { + env: { + PYTHONPATH: [...(directoryExists(root) ? [root] : []), process.env.PYTHONPATH] + .filter((entry): entry is string => Boolean(entry)) + .join(path.delimiter) + } + }) + ) { + rememberLog?.( + `Ignoring venv Hermes at ${python}: runtime import probe failed (broken/partial venv); falling through to bootstrap.` + ) + + return null + } + + return { + label: `existing Hermes Python at ${python}`, + command: python, + args: ['-m', 'hermes_cli.main', ...backendArgs], + bootstrap: false, + env: buildDesktopBackendEnv({ + hermesHome, + pythonPathEntries: [...(directoryExists(root) ? [root] : []), ...getVenvSitePackagesEntries(venvRoot)], + venvRoot + }), + kind: 'python', + root, + shell: false + } +} diff --git a/apps/desktop/electron/windows-hermes-resolution.test.ts b/apps/desktop/electron/windows-hermes-resolution.test.ts deleted file mode 100644 index 1b91a8acb7f4..000000000000 --- a/apps/desktop/electron/windows-hermes-resolution.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Regression guards for Windows `hermes` resolution in main.ts. -// -// main.ts has no module.exports, so these follow the repo's source-assertion -// test pattern (see windows-child-process.test.ts). They pin the two Windows -// resolution bugs that caused desktop reinstall loops: -// 1. findOnPath() tried the empty extension FIRST, so an extensionless -// Git-Bash `hermes` shim shadowed the real hermes.cmd/hermes.exe; the -// shim then failed the --version probe and the desktop fell through to a -// spurious bootstrap/repair. -// 2. handOffWindowsBootstrapRecovery() chose --update vs the destructive -// --repair by checking ONLY venv\Scripts\hermes.exe (the console-script -// shim, written at the END of venv setup and absent in interrupted -// states), so it escalated to a full venv recreate even on healthy -// installs. -// 3. unwrapWindowsVenvHermesCommand() returned the venv python with NO -// runtime probe (bypassing the caller's --version check too), so a venv -// broken mid-update (e.g. missing python-dotenv) was re-selected forever: -// Retry / "Repair install" resolved the same dead interpreter instead of -// falling through to the bootstrap installer. - -import assert from 'node:assert/strict' -import fs from 'node:fs' -import path from 'node:path' -import test from 'node:test' -import { fileURLToPath } from 'node:url' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) - -function readMain() { - return fs.readFileSync(path.join(__dirname, 'main.ts'), 'utf8').replace(/\r\n/g, '\n') -} - -test('findOnPath tries PATHEXT extensions before the bare (empty) name on Windows', () => { - const source = readMain() - // Fixed order: PATHEXT first, empty string LAST. - assert.match( - source, - /\(process\.env\.PATHEXT \|\| '\.COM;\.EXE;\.BAT;\.CMD'\)\.split\(';'\)\.filter\(Boolean\), ''\]/, - 'extensions array must end with the empty string, not start with it' - ) - // The buggy empty-first order must not return. - assert.doesNotMatch( - source, - /\['', \.\.\.\(process\.env\.PATHEXT/, - 'empty-extension-first order regressed: an extensionless shim can shadow hermes.cmd/.exe' - ) -}) - -test('Windows bootstrap recovery chooses --update when any real-install signal is present', () => { - const source = readMain() - assert.match(source, /const haveRealInstall =/, 'recovery must compute haveRealInstall') - assert.match(source, /fileExists\(venvPython\)/, 'recovery must accept the venv interpreter as a real-install signal') - assert.match( - source, - /\.hermes-bootstrap-complete/, - 'recovery must accept the bootstrap-complete marker as a real-install signal' - ) - assert.match(source, /updaterArgs = haveRealInstall \? \['--update'/, 'updaterArgs must gate on haveRealInstall') - // The old too-narrow check (only venv\Scripts\hermes.exe) must not return. - assert.doesNotMatch( - source, - /updaterArgs = fileExists\(venvHermes\) \?/, - 'recovery regressed to gating only on the hermes.exe shim, which forces destructive --repair' - ) -}) - -test('unwrapWindowsVenvHermesCommand smoke-tests the venv python before trusting it', () => { - const source = readMain() - const fnStart = source.indexOf('function unwrapWindowsVenvHermesCommand(') - assert.notEqual(fnStart, -1, 'unwrapWindowsVenvHermesCommand must exist in main.ts') - // Slice out just the function body (up to the next top-level function decl) - const fnEnd = source.indexOf('\nfunction ', fnStart + 1) - const body = source.slice(fnStart, fnEnd === -1 ? undefined : fnEnd) - assert.match( - body, - /canImportHermesCli\(python/, - 'unwrap must probe the venv interpreter; returning it unprobed re-selects a broken venv ' + - 'forever (Retry/Repair loop on a mid-update venv missing e.g. python-dotenv)' - ) - assert.match( - body, - /return null\s*\n\s*\}\s*\n\s*return \{/, - 'a failed probe must fall through (return null) so the resolver reaches the bootstrap rung' - ) -}) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 317174330698..9269dce251e0 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -38,7 +38,7 @@ "test:desktop:nsis": "node scripts/test-desktop.mjs nsis", "test:desktop:existing": "node scripts/test-desktop.mjs existing", "test:desktop:fresh": "node scripts/test-desktop.mjs fresh", - "test:desktop:platforms": "node --test electron/bootstrap-platform.test.ts electron/hardening.test.ts electron/backend-env.test.ts electron/backend-probes.test.ts electron/backend-ready.test.ts electron/bootstrap-runner.test.ts electron/connection-config.test.ts electron/dashboard-token.test.ts electron/gateway-ws-probe.test.ts electron/oauth-net-request.test.ts electron/desktop-uninstall.test.ts electron/session-windows.test.ts electron/link-title-window.test.ts electron/workspace-cwd.test.ts electron/fs-read-dir.test.ts electron/git-root.test.ts electron/git-worktree-ops.test.ts electron/windows-child-options.test.ts electron/update-remote.test.ts electron/update-count.test.ts electron/update-rebuild.test.ts electron/update-marker.test.ts electron/update-relaunch.test.ts electron/windows-user-env.test.ts electron/wsl-clipboard-image.test.ts electron/titlebar-overlay-width.test.ts electron/window-state.test.ts electron/zoom.test.ts electron/windows-hermes-resolution.test.ts electron/oauth-session-request.test.ts", + "test:desktop:platforms": "node --test electron/bootstrap-platform.test.ts electron/hardening.test.ts electron/backend-env.test.ts electron/backend-probes.test.ts electron/backend-ready.test.ts electron/bootstrap-runner.test.ts electron/connection-config.test.ts electron/dashboard-token.test.ts electron/gateway-ws-probe.test.ts electron/oauth-net-request.test.ts electron/desktop-uninstall.test.ts electron/session-windows.test.ts electron/link-title-window.test.ts electron/workspace-cwd.test.ts electron/fs-read-dir.test.ts electron/git-root.test.ts electron/git-worktree-ops.test.ts electron/windows-child-options.test.ts electron/update-remote.test.ts electron/update-count.test.ts electron/update-rebuild.test.ts electron/update-marker.test.ts electron/update-relaunch.test.ts electron/windows-user-env.test.ts electron/wsl-clipboard-image.test.ts electron/titlebar-overlay-width.test.ts electron/window-state.test.ts electron/zoom.test.ts electron/windows-hermes-path.test.ts electron/oauth-session-request.test.ts", "typecheck": "tsc -p . --noEmit && tsc -p tsconfig.electron.json --noEmit", "lint": "eslint src/ electron/", "lint:fix": "eslint src/ electron/ --fix",