cleanup(desktop): npm run fix for fmtting

we should run this as part of merges at some point :)
This commit is contained in:
ethernet 2026-07-08 18:54:56 -04:00
parent 7a65530fa5
commit 56a8e81d33
54 changed files with 1163 additions and 484 deletions

View file

@ -32,7 +32,9 @@ export function serveBackendArgs(profile?: string) {
export function dashboardFallbackArgs(args) {
const i = args.indexOf('serve')
if (i === -1) {return args.slice()}
if (i === -1) {
return args.slice()
}
return [...args.slice(0, i), 'dashboard', '--no-open', ...args.slice(i + 1)]
}

View file

@ -2,12 +2,14 @@ import assert from 'node:assert/strict'
import path from 'node:path'
import test from 'node:test'
import { appendUniquePathEntries,
import {
appendUniquePathEntries,
buildDesktopBackendEnv,
buildDesktopBackendPath,
normalizeHermesHomeRoot,
pathEnvKey,
POSIX_SANE_PATH_ENTRIES } from './backend-env'
POSIX_SANE_PATH_ENTRIES
} from './backend-env'
test('desktop backend PATH adds Hermes-managed bins and missing POSIX sane entries', () => {
const result = buildDesktopBackendPath({

View file

@ -23,7 +23,9 @@ function pathModuleForPlatform(platform = process.platform) {
}
function pathEnvKey(env = process.env, platform = process.platform) {
if (platform !== 'win32') {return 'PATH'}
if (platform !== 'win32') {
return 'PATH'
}
return Object.keys(env || {}).find(key => key.toUpperCase() === 'PATH') || 'PATH'
}
@ -39,11 +41,15 @@ function appendUniquePathEntries(entries, { delimiter = path.delimiter } = {}) {
const ordered = []
for (const entry of entries) {
if (!entry) {continue}
if (!entry) {
continue
}
const parts = Array.isArray(entry) ? entry : String(entry).split(delimiter)
for (const part of parts) {
if (!part || seen.has(part)) {continue}
if (!part || seen.has(part)) {
continue
}
seen.add(part)
ordered.push(part)
}
@ -68,7 +74,9 @@ function buildDesktopBackendPath({
}
function normalizeHermesHomeRoot(hermesHome, { pathModule = pathModuleForPlatform(process.platform) }: any = {}) {
if (!hermesHome) {return hermesHome}
if (!hermesHome) {
return hermesHome
}
const resolved = pathModule.resolve(String(hermesHome))
const parent = pathModule.dirname(resolved)
@ -103,10 +111,12 @@ function buildDesktopBackendEnv({
}
}
export { appendUniquePathEntries,
export {
appendUniquePathEntries,
buildDesktopBackendEnv,
buildDesktopBackendPath,
delimiterForPlatform,
normalizeHermesHomeRoot,
pathEnvKey,
POSIX_SANE_PATH_ENTRIES }
POSIX_SANE_PATH_ENTRIES
}

View file

@ -65,8 +65,10 @@ function hermesRuntimeImportProbe() {
* @param {object} [opts.env] - Additional environment for the probe.
* @returns {boolean}
*/
function canImportHermesCli(pythonPath: string, opts:{env?: Record<string, string>} = {}) {
if (!pythonPath) {return false}
function canImportHermesCli(pythonPath: string, opts: { env?: Record<string, string> } = {}) {
if (!pythonPath) {
return false
}
try {
execFileSync(pythonPath, ['-c', hermesRuntimeImportProbe()], {
@ -102,8 +104,10 @@ function canImportHermesCli(pythonPath: string, opts:{env?: Record<string, strin
* in resolveHermesBackend.
* @returns {boolean}
*/
function verifyHermesCli(hermesCommand: string, opts?: {shell?: boolean}) {
if (!hermesCommand) {return false}
function verifyHermesCli(hermesCommand: string, opts?: { shell?: boolean }) {
if (!hermesCommand) {
return false
}
try {
execFileSync(hermesCommand, ['--version'], {
@ -119,7 +123,4 @@ function verifyHermesCli(hermesCommand: string, opts?: {shell?: boolean}) {
}
}
export { canImportHermesCli,
hermesRuntimeImportProbe,
PROBE_TIMEOUT_MS,
verifyHermesCli }
export { canImportHermesCli, hermesRuntimeImportProbe, PROBE_TIMEOUT_MS, verifyHermesCli }

View file

@ -18,13 +18,15 @@ import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { DEFAULT_PORT_ANNOUNCE_TIMEOUT_MS,
import {
DEFAULT_PORT_ANNOUNCE_TIMEOUT_MS,
MIN_PORT_ANNOUNCE_TIMEOUT_MS,
readDashboardReadyFile,
resolvePortAnnounceTimeoutMs,
waitForDashboardPort,
waitForDashboardPortAnnouncement,
waitForDashboardReadyFile } from './backend-ready'
waitForDashboardReadyFile
} from './backend-ready'
type FakeChildProcess = EventEmitter & {
stdout: EventEmitter

View file

@ -57,7 +57,9 @@ function waitForDashboardPort(child, timeoutMs = resolvePortAnnounceTimeoutMs())
let done = false
function cleanup() {
if (done) {return}
if (done) {
return
}
done = true
clearTimeout(timer)
child.stdout.off('data', onData)
@ -105,7 +107,9 @@ function waitForDashboardPort(child, timeoutMs = resolvePortAnnounceTimeoutMs())
}
function readDashboardReadyFile(readyFile: fs.PathOrFileDescriptor) {
if (!readyFile) {return null}
if (!readyFile) {
return null
}
try {
const parsed = JSON.parse(fs.readFileSync(readyFile, 'utf8'))
@ -123,11 +127,15 @@ function waitForDashboardReadyFile(readyFile, child, timeoutMs = resolvePortAnno
let interval = null
function cleanup() {
if (done) {return}
if (done) {
return
}
done = true
clearTimeout(timer)
if (interval) {clearInterval(interval)}
if (interval) {
clearInterval(interval)
}
child.off('exit', onExit)
child.off('error', onError)
}
@ -160,15 +168,20 @@ function waitForDashboardReadyFile(readyFile, child, timeoutMs = resolvePortAnno
child.on('error', onError)
interval = setInterval(check, 50)
if (typeof interval.unref === 'function') {interval.unref()}
if (typeof interval.unref === 'function') {
interval.unref()
}
check()
})
}
function waitForDashboardPortAnnouncement(child, options: {
readyFile?: fs.PathOrFileDescriptor,
timeoutMs?: number
} = {}) {
function waitForDashboardPortAnnouncement(
child,
options: {
readyFile?: fs.PathOrFileDescriptor
timeoutMs?: number
} = {}
) {
const timeoutMs = options.timeoutMs ?? resolvePortAnnounceTimeoutMs()
if (options.readyFile) {
@ -178,10 +191,12 @@ function waitForDashboardPortAnnouncement(child, options: {
return waitForDashboardPort(child, timeoutMs)
}
export { DEFAULT_PORT_ANNOUNCE_TIMEOUT_MS,
export {
DEFAULT_PORT_ANNOUNCE_TIMEOUT_MS,
MIN_PORT_ANNOUNCE_TIMEOUT_MS,
readDashboardReadyFile,
resolvePortAnnounceTimeoutMs,
waitForDashboardPort,
waitForDashboardPortAnnouncement,
waitForDashboardReadyFile }
waitForDashboardReadyFile
}

View file

@ -1,10 +1,12 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { bundledRuntimeImportCheck,
import {
bundledRuntimeImportCheck,
detectRemoteDisplay,
isWindowsBinaryPathInWsl,
isWslEnvironment } from './bootstrap-platform'
isWslEnvironment
} from './bootstrap-platform'
test('isWslEnvironment detects WSL2 env vars on linux', () => {
assert.equal(isWslEnvironment({ WSL_DISTRO_NAME: 'Ubuntu' }, 'linux'), true)

View file

@ -1,9 +1,13 @@
import fs from 'node:fs'
function isWslEnvironment(env = process.env, platform = process.platform, kernelRelease = null) {
if (platform !== 'linux') {return false}
if (platform !== 'linux') {
return false
}
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {return true}
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
return true
}
try {
const release = kernelRelease ?? fs.readFileSync('/proc/sys/kernel/osrelease', 'utf8')
@ -14,10 +18,15 @@ function isWslEnvironment(env = process.env, platform = process.platform, kernel
}
}
function isWindowsBinaryPathInWsl(filePath, options: {isWsl?: boolean, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform} = {}) {
function isWindowsBinaryPathInWsl(
filePath,
options: { isWsl?: boolean; env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform } = {}
) {
const isWsl = options.isWsl ?? isWslEnvironment(options.env, options.platform)
if (!isWsl) {return false}
if (!isWsl) {
return false
}
const normalized = String(filePath || '')
.replace(/\\/g, '/')
@ -51,7 +60,7 @@ const GPU_OVERRIDE_OFF = new Set(['0', 'false', 'no', 'off'])
*
* Pure + dependency-free so it can be unit-tested and called before app ready.
*/
function detectRemoteDisplay(options: {env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform} = {}) {
function detectRemoteDisplay(options: { env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform } = {}) {
const env = options.env ?? process.env
const platform = options.platform ?? process.platform
@ -59,13 +68,19 @@ function detectRemoteDisplay(options: {env?: NodeJS.ProcessEnv, platform?: NodeJ
.trim()
.toLowerCase()
if (GPU_OVERRIDE_ON.has(override)) {return 'override (HERMES_DESKTOP_DISABLE_GPU)'}
if (GPU_OVERRIDE_ON.has(override)) {
return 'override (HERMES_DESKTOP_DISABLE_GPU)'
}
if (GPU_OVERRIDE_OFF.has(override)) {return null}
if (GPU_OVERRIDE_OFF.has(override)) {
return null
}
// Launched from an SSH session → the display is X11-forwarded or otherwise
// remote. Covers the common `ssh user@box` + GUI-forwarding case.
if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY) {return 'ssh-session'}
if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY) {
return 'ssh-session'
}
if (platform === 'linux') {
// X11 forwarding sets DISPLAY to "<host>:N" (e.g. "localhost:10.0"); a
@ -84,13 +99,12 @@ function detectRemoteDisplay(options: {env?: NodeJS.ProcessEnv, platform?: NodeJ
// "Console".
const sessionName = String(env.SESSIONNAME || '')
if (/^rdp-/i.test(sessionName)) {return `rdp (SESSIONNAME=${sessionName})`}
if (/^rdp-/i.test(sessionName)) {
return `rdp (SESSIONNAME=${sessionName})`
}
}
return null
}
export { bundledRuntimeImportCheck,
detectRemoteDisplay,
isWindowsBinaryPathInWsl,
isWslEnvironment }
export { bundledRuntimeImportCheck, detectRemoteDisplay, isWindowsBinaryPathInWsl, isWslEnvironment }

View file

@ -4,10 +4,7 @@ import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { cachedScriptPath,
installedAgentInstallScript,
resolveInstallScript,
runBootstrap } from './bootstrap-runner'
import { cachedScriptPath, installedAgentInstallScript, resolveInstallScript, runBootstrap } from './bootstrap-runner'
const SCRIPT_NAME = process.platform === 'win32' ? 'install.ps1' : 'install.sh'

View file

@ -70,7 +70,9 @@ function installScriptKind() {
}
function resolveLocalInstallScript(sourceRepoRoot) {
if (!sourceRepoRoot) {return null}
if (!sourceRepoRoot) {
return null
}
const candidate = path.join(sourceRepoRoot, 'scripts', installScriptName())
try {
@ -91,7 +93,9 @@ function bootstrapCacheDir(hermesHome) {
// the pinned commit can't be fetched from GitHub (e.g. a locally-built desktop
// app stamped to an unpushed HEAD).
function installedAgentInstallScript(hermesHome) {
if (!hermesHome) {return null}
if (!hermesHome) {
return null
}
const candidate = path.join(hermesHome, 'hermes-agent', 'scripts', installScriptName())
try {
@ -300,7 +304,9 @@ function resolveWindowsPowerShell() {
const candidate = powershellUnderRoot(root)
try {
if (fs.statSync(candidate).isFile()) {return candidate}
if (fs.statSync(candidate).isFile()) {
return candidate
}
} catch {
void 0
}
@ -314,7 +320,9 @@ function resolveWindowsPowerShell() {
const candidate = path.join(dir, exe)
try {
if (fs.statSync(candidate).isFile()) {return candidate}
if (fs.statSync(candidate).isFile()) {
return candidate
}
} catch {
void 0
}
@ -379,7 +387,9 @@ function spawnPowerShell(scriptPath, args, { emit, stageName, abortSignal, herme
const line = stdoutBuf.slice(0, nl).replace(/\r$/, '')
stdoutBuf = stdoutBuf.slice(nl + 1)
if (line) {emit && emit({ type: 'log', stage: stageName, line, stream: 'stdout' })}
if (line) {
emit && emit({ type: 'log', stage: stageName, line, stream: 'stdout' })
}
}
})
@ -393,22 +403,32 @@ function spawnPowerShell(scriptPath, args, { emit, stageName, abortSignal, herme
const line = stderrBuf.slice(0, nl).replace(/\r$/, '')
stderrBuf = stderrBuf.slice(nl + 1)
if (line) {emit && emit({ type: 'log', stage: stageName, line, stream: 'stderr' })}
if (line) {
emit && emit({ type: 'log', stage: stageName, line, stream: 'stderr' })
}
}
})
child.on('error', err => {
if (abortSignal) {abortSignal.removeEventListener('abort', onAbort)}
if (abortSignal) {
abortSignal.removeEventListener('abort', onAbort)
}
reject(err)
})
child.on('close', (code, signal) => {
if (abortSignal) {abortSignal.removeEventListener('abort', onAbort)}
if (abortSignal) {
abortSignal.removeEventListener('abort', onAbort)
}
// Flush any trailing bytes
if (stdoutBuf) {emit && emit({ type: 'log', stage: stageName, line: stdoutBuf, stream: 'stdout' } as any)}
if (stdoutBuf) {
emit && emit({ type: 'log', stage: stageName, line: stdoutBuf, stream: 'stdout' } as any)
}
if (stderrBuf) {emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' } as any)}
if (stderrBuf) {
emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' } as any)
}
resolve({ stdout, stderr, code, signal, killed } as any)
})
})
@ -459,7 +479,9 @@ function spawnBash(scriptPath, args, { emit, stageName, abortSignal, hermesHome
const line = stdoutBuf.slice(0, nl).replace(/\r$/, '')
stdoutBuf = stdoutBuf.slice(nl + 1)
if (line) {emit && emit({ type: 'log', stage: stageName, line, stream: 'stdout' })}
if (line) {
emit && emit({ type: 'log', stage: stageName, line, stream: 'stdout' })
}
}
})
@ -473,21 +495,31 @@ function spawnBash(scriptPath, args, { emit, stageName, abortSignal, hermesHome
const line = stderrBuf.slice(0, nl).replace(/\r$/, '')
stderrBuf = stderrBuf.slice(nl + 1)
if (line) {emit && emit({ type: 'log', stage: stageName, line, stream: 'stderr' })}
if (line) {
emit && emit({ type: 'log', stage: stageName, line, stream: 'stderr' })
}
}
})
child.on('error', err => {
if (abortSignal) {abortSignal.removeEventListener('abort', onAbort)}
if (abortSignal) {
abortSignal.removeEventListener('abort', onAbort)
}
reject(err)
})
child.on('close', (code, signal) => {
if (abortSignal) {abortSignal.removeEventListener('abort', onAbort)}
if (abortSignal) {
abortSignal.removeEventListener('abort', onAbort)
}
if (stdoutBuf) {emit && emit({ type: 'log', stage: stageName, line: stdoutBuf, stream: 'stdout' })}
if (stdoutBuf) {
emit && emit({ type: 'log', stage: stageName, line: stdoutBuf, stream: 'stdout' })
}
if (stderrBuf) {emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' })}
if (stderrBuf) {
emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' })
}
resolve({ stdout, stderr, code, signal, killed })
})
})
@ -723,7 +755,9 @@ async function runBootstrap(opts) {
}
try {
if (typeof onEvent === 'function') {onEvent(ev)}
if (typeof onEvent === 'function') {
onEvent(ev)
}
} catch (err) {
// Don't let a subscriber bug crash the bootstrap
runLog.stream.write(`emit error: ${err && err.message}\n`)
@ -812,10 +846,12 @@ async function runBootstrap(opts) {
}
}
export { cachedScriptPath,
export {
cachedScriptPath,
installedAgentInstallScript,
// Exposed for testability
parseStageResult,
resolveInstallScript,
resolveLocalInstallScript,
runBootstrap }
runBootstrap
}

View file

@ -13,7 +13,8 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { AT_COOKIE_VARIANTS,
import {
AT_COOKIE_VARIANTS,
authModeFromStatus,
buildGatewayWsUrl,
buildGatewayWsUrlWithTicket,
@ -27,7 +28,8 @@ import { AT_COOKIE_VARIANTS,
resolveAuthMode,
resolveTestWsUrl,
RT_COOKIE_VARIANTS,
tokenPreview } from './connection-config'
tokenPreview
} from './connection-config'
// --- connectionScopeKey / normAuthMode ---

View file

@ -237,11 +237,17 @@ function authModeFromStatus(statusBody) {
* Returns 'oauth' | 'token'.
*/
function resolveAuthMode(inputAuthMode, existingAuthMode) {
if (inputAuthMode === 'oauth') {return 'oauth'}
if (inputAuthMode === 'oauth') {
return 'oauth'
}
if (inputAuthMode === 'token') {return 'token'}
if (inputAuthMode === 'token') {
return 'token'
}
if (existingAuthMode === 'oauth') {return 'oauth'}
if (existingAuthMode === 'oauth') {
return 'oauth'
}
return 'token'
}
@ -258,7 +264,9 @@ function resolveAuthMode(inputAuthMode, existingAuthMode) {
* need to know whether an unexpired access token is present right now.
*/
function cookiesHaveSession(cookies) {
if (!Array.isArray(cookies)) {return false}
if (!Array.isArray(cookies)) {
return false
}
return cookies.some(c => c && AT_COOKIE_VARIANTS.includes(c.name) && c.value)
}
@ -277,12 +285,15 @@ function cookiesHaveSession(cookies) {
* the RT is also dead/revoked).
*/
function cookiesHaveLiveSession(cookies) {
if (!Array.isArray(cookies)) {return false}
if (!Array.isArray(cookies)) {
return false
}
return cookies.some(c => c && c.value && (AT_COOKIE_VARIANTS.includes(c.name) || RT_COOKIE_VARIANTS.includes(c.name)))
}
export { AT_COOKIE_VARIANTS,
export {
AT_COOKIE_VARIANTS,
authModeFromStatus,
buildGatewayWsUrl,
buildGatewayWsUrlWithTicket,
@ -296,4 +307,5 @@ export { AT_COOKIE_VARIANTS,
resolveAuthMode,
resolveTestWsUrl,
RT_COOKIE_VARIANTS,
tokenPreview }
tokenPreview
}

View file

@ -8,12 +8,14 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { adoptServedDashboardToken,
import {
adoptServedDashboardToken,
dashboardIndexUrl,
extractInjectedDashboardToken,
fetchPublicText,
isForeignBackendToken,
resolveServedDashboardToken } from './dashboard-token'
resolveServedDashboardToken
} from './dashboard-token'
test('extractInjectedDashboardToken reads the JSON-encoded dashboard token', () => {
const html = '<script>window.__HERMES_SESSION_TOKEN__="served-token";window.__HERMES_BASE_PATH__=""</script>'

View file

@ -28,7 +28,9 @@ async function fetchPublicText(url, options: any = {}) {
const text = await res.text()
if (!res.ok) {throw new Error(`${res.status}: ${text || res.statusText}`)}
if (!res.ok) {
throw new Error(`${res.status}: ${text || res.statusText}`)
}
return text
}
@ -36,7 +38,9 @@ async function fetchPublicText(url, options: any = {}) {
function extractInjectedDashboardToken(html) {
const match = /window\.__HERMES_SESSION_TOKEN__\s*=\s*("(?:\\.|[^"\\])*")/.exec(String(html || ''))
if (!match) {return null}
if (!match) {
return null
}
try {
return JSON.parse(match[1])
@ -97,10 +101,12 @@ async function adoptServedDashboardToken(baseUrl, spawnToken, { childAlive, labe
return servedToken
}
export { adoptServedDashboardToken,
export {
adoptServedDashboardToken,
dashboardIndexUrl,
DEFAULT_TOKEN_FETCH_TIMEOUT_MS,
extractInjectedDashboardToken,
fetchPublicText,
isForeignBackendToken,
resolveServedDashboardToken }
resolveServedDashboardToken
}

View file

@ -12,14 +12,16 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { buildPosixCleanupScript,
import {
buildPosixCleanupScript,
buildWindowsCleanupScript,
modeRemovesAgent,
modeRemovesUserData,
resolveRemovableAppPath,
shouldRemoveAppBundle,
UNINSTALL_MODES,
uninstallArgsForMode } from './desktop-uninstall'
uninstallArgsForMode
} from './desktop-uninstall'
// --- uninstallArgsForMode ---

View file

@ -69,7 +69,9 @@ function modeRemovesUserData(mode) {
function resolveRemovableAppPath(execPath, platform, env: any = {}) {
const exe = String(execPath || '')
if (!exe) {return null}
if (!exe) {
return null
}
// Use the path flavor that matches the TARGET platform, not the host running
// this code — so the Windows branch parses backslash paths correctly even
@ -82,7 +84,9 @@ function resolveRemovableAppPath(execPath, platform, env: any = {}) {
const contents = p.dirname(macOsDir) // …/Contents
const appBundle = p.dirname(contents) // …/Hermes.app
if (appBundle.endsWith('.app')) {return appBundle}
if (appBundle.endsWith('.app')) {
return appBundle
}
return null
}
@ -91,17 +95,23 @@ function resolveRemovableAppPath(execPath, platform, env: any = {}) {
// NSIS per-user installs Hermes.exe directly in the install dir.
const dir = p.dirname(exe)
if (/[\\/]Hermes$/i.test(dir) || /[\\/]hermes-desktop$/i.test(dir)) {return dir}
if (/[\\/]Hermes$/i.test(dir) || /[\\/]hermes-desktop$/i.test(dir)) {
return dir
}
return null
}
// Linux: an AppImage exposes its own path via the APPIMAGE env var.
if (env.APPIMAGE) {return env.APPIMAGE}
if (env.APPIMAGE) {
return env.APPIMAGE
}
// Unpacked electron-builder tree: …/linux-unpacked/hermes
const dir = p.dirname(exe)
if (/-unpacked$/.test(dir)) {return dir}
if (/-unpacked$/.test(dir)) {
return dir
}
return null
}
@ -245,11 +255,13 @@ function buildWindowsCleanupScript({
return lines.join('\r\n')
}
export { buildPosixCleanupScript,
export {
buildPosixCleanupScript,
buildWindowsCleanupScript,
modeRemovesAgent,
modeRemovesUserData,
resolveRemovableAppPath,
shouldRemoveAppBundle,
UNINSTALL_MODES,
uninstallArgsForMode }
uninstallArgsForMode
}

View file

@ -36,7 +36,9 @@ function direntIsSymbolicLink(dirent) {
}
function shouldStatDirent(dirent) {
if (direntIsDirectory(dirent)) {return false}
if (direntIsDirectory(dirent)) {
return false
}
return direntIsSymbolicLink(dirent) || !direntIsFile(dirent)
}

View file

@ -38,11 +38,14 @@ const DEFAULT_READY_GRACE_MS = 750
* @param {string} wsUrl - Fully-formed ws(s):// URL including the credential.
* @returns {Promise<{ ok: boolean, reason?: string }>}
*/
function probeGatewayWebSocket<T>(wsUrl: string, options:{
WebSocketImpl?: any,
connectTimeoutMs?: number
readyGraceMs?: number
} = {}) {
function probeGatewayWebSocket<T>(
wsUrl: string,
options: {
WebSocketImpl?: any
connectTimeoutMs?: number
readyGraceMs?: number
} = {}
) {
const WebSocketImpl = options.WebSocketImpl
const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS
const readyGraceMs = options.readyGraceMs ?? DEFAULT_READY_GRACE_MS
@ -74,7 +77,9 @@ function probeGatewayWebSocket<T>(wsUrl: string, options:{
}
const finish = result => {
if (settled) {return}
if (settled) {
return
}
settled = true
clearTimers()
@ -99,7 +104,9 @@ function probeGatewayWebSocket<T>(wsUrl: string, options:{
}
const onOpen = () => {
if (settled) {return}
if (settled) {
return
}
opened = true
// Upgrade accepted. Give the server a brief window to reject the
// credential post-handshake (early close) before declaring success.
@ -122,7 +129,9 @@ function probeGatewayWebSocket<T>(wsUrl: string, options:{
}
const onClose = event => {
if (settled) {return}
if (settled) {
return
}
if (opened) {
// Opened, then closed inside the grace window: the upgrade was accepted
@ -173,14 +182,22 @@ function addListener(socket, type, handler) {
}
function extractErrorReason(event) {
if (!event) {return ''}
if (!event) {
return ''
}
if (event instanceof Error) {return event.message}
if (event instanceof Error) {
return event.message
}
const err = event.error || event.message
if (err instanceof Error) {return err.message}
if (err instanceof Error) {
return err.message
}
if (typeof err === 'string') {return err}
if (typeof err === 'string') {
return err
}
return ''
}
@ -189,15 +206,19 @@ function closeReason(event, fallback) {
const code = event && typeof event.code === 'number' ? event.code : null
const reason = event && typeof event.reason === 'string' ? event.reason.trim() : ''
if (code && reason) {return `${fallback} (code ${code}: ${reason})`}
if (code && reason) {
return `${fallback} (code ${code}: ${reason})`
}
if (code) {return `${fallback} (code ${code})`}
if (code) {
return `${fallback} (code ${code})`
}
if (reason) {return `${fallback} (${reason})`}
if (reason) {
return `${fallback} (${reason})`
}
return fallback
}
export { DEFAULT_CONNECT_TIMEOUT_MS,
DEFAULT_READY_GRACE_MS,
probeGatewayWebSocket }
export { DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READY_GRACE_MS, probeGatewayWebSocket }

View file

@ -8,7 +8,7 @@ import { execFile } from 'node:child_process'
import fs from 'node:fs/promises'
import path from 'node:path'
import simpleGit from "simple-git";
import simpleGit from 'simple-git'
import { resolveRequestedPathForIpc } from './hardening'
@ -31,7 +31,7 @@ function ghEnv(ghBin) {
// Run the `gh` CLI in a repo. Resolves { ok, stdout } so callers branch on
// availability/auth without a throw. gh missing/unauthed → ok:false.
function runGh(args, cwd, ghBin): Promise<{ok: boolean, stdout: string}> {
function runGh(args, cwd, ghBin): Promise<{ ok: boolean; stdout: string }> {
return new Promise(resolve => {
execFile(
ghBin || 'gh',
@ -242,8 +242,8 @@ async function reviewList(repoPath, scope, baseRef, gitBin) {
const files = summary.files.map(file => ({
path: resolveRenamePath(file.file),
added: 'insertions' in file ? file.insertions : 0 ,
removed: 'deletions' in file ? file.deletions : 0 ,
added: 'insertions' in file ? file.insertions : 0,
removed: 'deletions' in file ? file.deletions : 0,
status: 'M',
staged: false
}))
@ -674,7 +674,8 @@ async function repoStatus(repoPath, gitBin) {
return result
}
export { branchBase,
export {
branchBase,
fileDiffVsHead,
repoStatus,
resolveRenamePath,
@ -688,4 +689,5 @@ export { branchBase,
reviewRevParse,
reviewShipInfo,
reviewStage,
reviewUnstage }
reviewUnstage
}

View file

@ -27,7 +27,7 @@ function findGitRoot(start, fsImpl = fs) {
return null
}
async function gitRootForIpc(startPath, options: {fs?: typeof fs} = {}) {
async function gitRootForIpc(startPath, options: { fs?: typeof fs } = {}) {
const fsImpl = options.fs || fs
let resolved
@ -47,5 +47,4 @@ async function gitRootForIpc(startPath, options: {fs?: typeof fs} = {}) {
}
}
export { findGitRoot,
gitRootForIpc }
export { findGitRoot, gitRootForIpc }

View file

@ -5,12 +5,14 @@ import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { addWorktree,
import {
addWorktree,
ensureGitRepo,
listBranches,
parseWorktrees,
sanitizeBranch,
switchBranch } from './git-worktree-ops'
switchBranch
} from './git-worktree-ops'
test('sanitizeBranch: spaces → hyphens, forbidden chars dropped, edges trimmed', () => {
assert.equal(sanitizeBranch('beach vibes'), 'beach-vibes')

View file

@ -337,11 +337,13 @@ async function switchBranch(repoPath, branch, gitBin) {
return { branch: target }
}
export { addWorktree,
export {
addWorktree,
ensureGitRepo,
listBranches,
listWorktrees,
parseWorktrees,
removeWorktree,
sanitizeBranch,
switchBranch }
switchBranch
}

View file

@ -5,13 +5,15 @@ import path from 'node:path'
import test from 'node:test'
import { pathToFileURL } from 'node:url'
import { DEFAULT_FETCH_TIMEOUT_MS,
import {
DEFAULT_FETCH_TIMEOUT_MS,
encryptDesktopSecret,
resolveDirectoryForIpc,
resolveReadableFileForIpc,
resolveRequestedPathForIpc,
resolveTimeoutMs,
sensitiveFileBlockReason } from './hardening'
sensitiveFileBlockReason
} from './hardening'
async function rejectsWithCode(promise, code: string) {
await assert.rejects(promise, (error: any) => {

View file

@ -110,9 +110,9 @@ function sensitiveFileBlockReason(filePath) {
return null
}
function ipcPathError(code: any, message: string): Error & {code: any} {
const error = new Error(message) as Error & {code: any}
(error as any).code = code
function ipcPathError(code: any, message: string): Error & { code: any } {
const error = new Error(message) as Error & { code: any }
;(error as any).code = code
return error
}
@ -146,7 +146,7 @@ function rejectUnsafePathSyntax(filePath, purpose = 'File read') {
return raw
}
function resolveRequestedPathForIpc(filePath, options: {purpose?: string, baseDir?: fs.PathOrFileDescriptor} = {}) {
function resolveRequestedPathForIpc(filePath, options: { purpose?: string; baseDir?: fs.PathOrFileDescriptor } = {}) {
const purpose = String(options.purpose || 'File read')
let raw = rejectUnsafePathSyntax(filePath, purpose)
@ -187,7 +187,7 @@ function resolveRequestedPathForIpc(filePath, options: {purpose?: string, baseDi
return resolvedPath
}
async function statForIpc(fsImpl: {promises: {stat: typeof fs.promises.stat}}, resolvedPath, purpose, typeLabel) {
async function statForIpc(fsImpl: { promises: { stat: typeof fs.promises.stat } }, resolvedPath, purpose, typeLabel) {
try {
return await fsImpl.promises.stat(resolvedPath)
} catch (error) {
@ -231,7 +231,14 @@ function rejectSensitiveFilePath(filePath, purpose) {
}
}
async function resolveDirectoryForIpc(dirPath, options: {purpose?: string , baseDir?: fs.PathOrFileDescriptor, fs?: {promises:{stat: typeof fs.promises.stat}}} = {}) {
async function resolveDirectoryForIpc(
dirPath,
options: {
purpose?: string
baseDir?: fs.PathOrFileDescriptor
fs?: { promises: { stat: typeof fs.promises.stat } }
} = {}
) {
const purpose = String(options.purpose || 'Directory read')
const fsImpl = options.fs || fs
const resolvedPath = resolveRequestedPathForIpc(dirPath, { baseDir: options.baseDir, purpose })
@ -246,7 +253,16 @@ async function resolveDirectoryForIpc(dirPath, options: {purpose?: string , base
return { realPath, resolvedPath, stat }
}
async function resolveReadableFileForIpc(filePath, options: {purpose?: string , baseDir?: fs.PathOrFileDescriptor, fs?: typeof fs, blockSensitive?: boolean, maxBytes?: number} = {}) {
async function resolveReadableFileForIpc(
filePath,
options: {
purpose?: string
baseDir?: fs.PathOrFileDescriptor
fs?: typeof fs
blockSensitive?: boolean
maxBytes?: number
} = {}
) {
const purpose = String(options.purpose || 'File read')
const fsImpl = options.fs || fs
const resolvedPath = resolveRequestedPathForIpc(filePath, { baseDir: options.baseDir, purpose })
@ -286,7 +302,8 @@ async function resolveReadableFileForIpc(filePath, options: {purpose?: string ,
return { realPath, resolvedPath, stat }
}
export { DATA_URL_READ_MAX_BYTES,
export {
DATA_URL_READ_MAX_BYTES,
DEFAULT_FETCH_TIMEOUT_MS,
encryptDesktopSecret,
rejectUnsafePathSyntax,
@ -295,4 +312,5 @@ export { DATA_URL_READ_MAX_BYTES,
resolveRequestedPathForIpc,
resolveTimeoutMs,
sensitiveFileBlockReason,
TEXT_PREVIEW_SOURCE_MAX_BYTES }
TEXT_PREVIEW_SOURCE_MAX_BYTES
}

View file

@ -1,10 +1,12 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { createLinkTitleWindow,
import {
createLinkTitleWindow,
guardLinkTitleSession,
linkTitleWindowOptions,
readLinkTitleWindowTitle } from './link-title-window'
readLinkTitleWindowTitle
} from './link-title-window'
function makeFakeBrowserWindow() {
const calls = { audioMuted: [] }

View file

@ -52,10 +52,14 @@ export function guardLinkTitleSession(partitionSession) {
// guard isDestroyed and swallow Electron's "Object has been destroyed" throws.
export function readLinkTitleWindowTitle(window) {
try {
if (!window || window.isDestroyed()) {return ''}
if (!window || window.isDestroyed()) {
return ''
}
const contents = window.webContents
if (!contents || contents.isDestroyed()) {return ''}
if (!contents || contents.isDestroyed()) {
return ''
}
return contents.getTitle() || ''
} catch {

File diff suppressed because it is too large Load diff

View file

@ -14,5 +14,4 @@ function setJsonRequestHeaders(request) {
request.setHeader('Content-Type', 'application/json')
}
export { serializeJsonBody,
setJsonRequestHeaders }
export { serializeJsonBody, setJsonRequestHeaders }

View file

@ -1,9 +1,7 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { buildSessionWindowUrl,
chatWindowWebPreferences,
createSessionWindowRegistry } from './session-windows'
import { buildSessionWindowUrl, chatWindowWebPreferences, createSessionWindowRegistry } from './session-windows'
// A minimal fake BrowserWindow: tracks listeners + destroyed state and lets a
// test fire the 'closed' event, mirroring the slice of the Electron API the

View file

@ -115,8 +115,10 @@ function createSessionWindowRegistry() {
}
}
export { buildSessionWindowUrl,
export {
buildSessionWindowUrl,
chatWindowWebPreferences,
createSessionWindowRegistry,
SESSION_WINDOW_MIN_HEIGHT,
SESSION_WINDOW_MIN_WIDTH }
SESSION_WINDOW_MIN_WIDTH
}

View file

@ -1,7 +1,12 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { MACOS_TAHOE_DARWIN_MAJOR, macTitleBarOverlayHeight, nativeOverlayWidth, OVERLAY_FALLBACK_WIDTH } from './titlebar-overlay-width'
import {
MACOS_TAHOE_DARWIN_MAJOR,
macTitleBarOverlayHeight,
nativeOverlayWidth,
OVERLAY_FALLBACK_WIDTH
} from './titlebar-overlay-width'
// This static reservation is only the pre-layout FALLBACK. Once laid out the
// renderer reads the exact width from navigator.windowControlsOverlay

View file

@ -15,7 +15,9 @@ export const OVERLAY_FALLBACK_WIDTH = 144
* @param {{ isMac?: boolean }} opts
*/
export function nativeOverlayWidth({ isWindows = false, isWsl = false, isMac = false } = {}) {
if (isMac) {return 0}
if (isMac) {
return 0
}
return OVERLAY_FALLBACK_WIDTH
}
@ -38,4 +40,3 @@ export const MACOS_TAHOE_DARWIN_MAJOR = 25
export function macTitleBarOverlayHeight({ darwinMajor = 0, titlebarHeight = 0 } = {}) {
return darwinMajor >= MACOS_TAHOE_DARWIN_MAJOR ? 0 : titlebarHeight
}

View file

@ -17,7 +17,9 @@ function shouldCountCommits({ isShallow, hasMergeBase }) {
// (developers / Docker dev images) keep the exact count path unchanged.
function resolveBehindCount({ countStr, currentSha, targetSha, isShallow, hasMergeBase }) {
if (!shouldCountCommits({ isShallow, hasMergeBase })) {
if (currentSha && targetSha && currentSha === targetSha) {return 0}
if (currentSha && targetSha && currentSha === targetSha) {
return 0
}
return 1 // behind by an unknown amount — show a generic "update available"
}

View file

@ -18,7 +18,13 @@ import test from 'node:test'
import os from 'os'
import path from 'path'
import { isPidAlive, markerPath, readLiveUpdateMarker, UPDATE_MARKER_MAX_AGE_MS, writeUpdateMarker } from './update-marker'
import {
isPidAlive,
markerPath,
readLiveUpdateMarker,
UPDATE_MARKER_MAX_AGE_MS,
writeUpdateMarker
} from './update-marker'
function tmpHome(tag) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `hermes-marker-${tag}-`))
@ -32,9 +38,9 @@ function writeMarker(home, pid, startedAtSec) {
const ALIVE: typeof process.kill = () => true // injected kill that "succeeds" => pid alive
const DEAD : typeof process.kill= () => {
const err = new Error('no such process');
(err as any).code = 'ESRCH'
const DEAD: typeof process.kill = () => {
const err = new Error('no such process')
;(err as any).code = 'ESRCH'
throw err
}
@ -86,8 +92,8 @@ test('isPidAlive: own pid is alive, impossible pid is dead', () => {
test('isPidAlive: EPERM counts as alive (process owned by another user)', () => {
const eperm = () => {
const err = new Error('operation not permitted');
(err as any).code = 'EPERM'
const err = new Error('operation not permitted')
;(err as any).code = 'EPERM'
throw err
}

View file

@ -38,7 +38,9 @@ export function markerPath(hermesHome) {
// EPERM => alive but owned by another user (still "alive" for our purposes).
// Injectable `kill` keeps it unit-testable.
export function isPidAlive(pid, kill: typeof process.kill = process.kill.bind(process)) {
if (!Number.isInteger(pid) || pid <= 0) {return false}
if (!Number.isInteger(pid) || pid <= 0) {
return false
}
try {
kill(pid, 0)
@ -61,9 +63,18 @@ export function isPidAlive(pid, kill: typeof process.kill = process.kill.bind(pr
* Pure-ish: file I/O against the given path, plus an injectable pid probe and
* clock for tests.
*/
export function readLiveUpdateMarker(hermesHome, { kill, now = Date.now, maxAgeMs = UPDATE_MARKER_MAX_AGE_MS }: {
now?: () => number, maxAgeMs?: number, kill?: typeof process.kill
} = {}) {
export function readLiveUpdateMarker(
hermesHome,
{
kill,
now = Date.now,
maxAgeMs = UPDATE_MARKER_MAX_AGE_MS
}: {
now?: () => number
maxAgeMs?: number
kill?: typeof process.kill
} = {}
) {
const file = markerPath(hermesHome)
let raw

View file

@ -24,7 +24,8 @@ import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { buildRelaunchScript,
import {
buildRelaunchScript,
collectRelaunchArgs,
collectRelaunchEnv,
decideRelaunchOutcome,
@ -32,7 +33,8 @@ import { buildRelaunchScript,
sandboxFallbackFromEnv,
sandboxPreflight,
shellQuote,
unpackedDirName } from './update-relaunch'
unpackedDirName
} from './update-relaunch'
const ROOT = '/home/u/.hermes/hermes-agent'
const UNPACKED = path.join(ROOT, 'apps', 'desktop', 'release', 'linux-unpacked')

View file

@ -39,9 +39,13 @@ import path from 'node:path'
// Map process.platform → electron-builder's `release/<dir>-unpacked` name.
function unpackedDirName(platform) {
if (platform === 'darwin') {return 'mac-unpacked'} // not used (mac swaps bundles)
if (platform === 'darwin') {
return 'mac-unpacked'
} // not used (mac swaps bundles)
if (platform === 'win32') {return 'win-unpacked'}
if (platform === 'win32') {
return 'win-unpacked'
}
return 'linux-unpacked'
}
@ -56,7 +60,9 @@ function unpackedDirName(platform) {
* `.../release/linux-unpacked-evil` can't masquerade as `.../release/linux-unpacked`.
*/
function resolveUnpackedRelease(execPath, updateRoot, platform) {
if (!execPath || !updateRoot) {return null}
if (!execPath || !updateRoot) {
return null
}
const releaseDir = path.join(updateRoot, 'apps', 'desktop', 'release')
const unpacked = path.join(releaseDir, unpackedDirName(platform))
const normalizedExec = path.resolve(String(execPath))
@ -83,9 +89,13 @@ function resolveUnpackedRelease(execPath, updateRoot, platform) {
* app. Closeable manual-restart terminal state.
*/
function decideRelaunchOutcome({ underUnpacked, sandboxOk }) {
if (!underUnpacked) {return 'guiSkew'}
if (!underUnpacked) {
return 'guiSkew'
}
if (!sandboxOk) {return 'manual'}
if (!sandboxOk) {
return 'manual'
}
return 'relaunch'
}
@ -103,7 +113,9 @@ function decideRelaunchOutcome({ underUnpacked, sandboxOk }) {
* `statSync` is injectable so this is testable without a real setuid file.
*/
function sandboxPreflight(unpackedDir, statSync) {
if (!unpackedDir) {return { ok: false, reason: 'no-unpacked-dir', path: null }}
if (!unpackedDir) {
return { ok: false, reason: 'no-unpacked-dir', path: null }
}
const sandboxPath = path.join(unpackedDir, 'chrome-sandbox')
let st
@ -126,7 +138,9 @@ function sandboxPreflight(unpackedDir, statSync) {
return { ok: false, reason: 'not-root-not-setuid', path: sandboxPath }
}
if (!ownedByRoot) {return { ok: false, reason: 'not-root', path: sandboxPath }}
if (!ownedByRoot) {
return { ok: false, reason: 'not-root', path: sandboxPath }
}
return { ok: false, reason: 'not-setuid', path: sandboxPath }
}
@ -148,9 +162,13 @@ function sandboxPreflight(unpackedDir, statSync) {
function sandboxFallbackFromEnv(env, launchArgs) {
const disable = String((env && env.ELECTRON_DISABLE_SANDBOX) || '').trim()
if (disable === '1' || disable.toLowerCase() === 'true') {return true}
if (disable === '1' || disable.toLowerCase() === 'true') {
return true
}
if (Array.isArray(launchArgs) && launchArgs.some(a => a === '--no-sandbox')) {return true}
if (Array.isArray(launchArgs) && launchArgs.some(a => a === '--no-sandbox')) {
return true
}
return false
}
@ -189,10 +207,14 @@ const INTERNAL_ARG_PREFIXES = [
* the exec path itself; there is no entry-script arg as in a dev run).
*/
function collectRelaunchArgs(argv) {
if (!Array.isArray(argv)) {return []}
if (!Array.isArray(argv)) {
return []
}
return argv.filter(arg => {
if (typeof arg !== 'string' || arg.length === 0) {return false}
if (typeof arg !== 'string' || arg.length === 0) {
return false
}
return !INTERNAL_ARG_PREFIXES.some(prefix =>
prefix.endsWith('=') ? arg.startsWith(prefix) : arg === prefix || arg.startsWith(prefix + '=')
@ -213,10 +235,14 @@ const PRESERVED_ENV_PREFIXES = ['HERMES_DESKTOP_']
function collectRelaunchEnv(env) {
const out = {}
if (!env || typeof env !== 'object') {return out}
if (!env || typeof env !== 'object') {
return out
}
for (const [key, value] of Object.entries(env)) {
if (value == null) {continue}
if (value == null) {
continue
}
if (PRESERVED_ENV_KEYS.includes(key) || PRESERVED_ENV_PREFIXES.some(p => key.startsWith(p))) {
out[key] = String(value)
@ -270,7 +296,8 @@ exec ${shellQuote(execPath)}${quotedArgs ? ' ' + quotedArgs : ''}
`
}
export { buildRelaunchScript,
export {
buildRelaunchScript,
collectRelaunchArgs,
collectRelaunchEnv,
decideRelaunchOutcome,
@ -281,4 +308,5 @@ export { buildRelaunchScript,
sandboxFallbackFromEnv,
sandboxPreflight,
shellQuote,
unpackedDirName }
unpackedDirName
}

View file

@ -18,11 +18,13 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { canonicalGitHubRemote,
import {
canonicalGitHubRemote,
isOfficialSshRemote,
isSshRemote,
OFFICIAL_REPO_CANONICAL,
OFFICIAL_REPO_HTTPS_URL } from './update-remote'
OFFICIAL_REPO_HTTPS_URL
} from './update-remote'
test('canonicalGitHubRemote normalizes SSH and HTTPS forms to the same value', () => {
assert.equal(canonicalGitHubRemote('git@github.com:NousResearch/hermes-agent.git'), OFFICIAL_REPO_CANONICAL)

View file

@ -19,7 +19,9 @@ const OFFICIAL_REPO_CANONICAL = 'github.com/nousresearch/hermes-agent'
// no trailing slash, no .git suffix) so SSH and HTTPS forms of the same repo
// compare equal.
function canonicalGitHubRemote(url) {
if (!url) {return ''}
if (!url) {
return ''
}
let value = String(url).trim()
if (value.startsWith('git@github.com:')) {
@ -30,7 +32,9 @@ function canonicalGitHubRemote(url) {
try {
const parsed = new URL(value)
if (parsed.hostname && parsed.pathname) {value = `${parsed.hostname}${parsed.pathname}`}
if (parsed.hostname && parsed.pathname) {
value = `${parsed.hostname}${parsed.pathname}`
}
} catch {
// Leave non-URL forms unchanged.
}
@ -38,7 +42,9 @@ function canonicalGitHubRemote(url) {
value = value.trim().replace(/\/+$/, '')
if (value.endsWith('.git')) {value = value.slice(0, -4)}
if (value.endsWith('.git')) {
value = value.slice(0, -4)
}
return value.toLowerCase()
}
@ -55,8 +61,4 @@ function isOfficialSshRemote(url) {
return isSshRemote(url) && canonicalGitHubRemote(url) === OFFICIAL_REPO_CANONICAL
}
export { canonicalGitHubRemote,
isOfficialSshRemote,
isSshRemote,
OFFICIAL_REPO_CANONICAL,
OFFICIAL_REPO_HTTPS_URL }
export { canonicalGitHubRemote, isOfficialSshRemote, isSshRemote, OFFICIAL_REPO_CANONICAL, OFFICIAL_REPO_HTTPS_URL }

View file

@ -334,10 +334,4 @@ async function fetchMarketplaceThemes(id) {
const __testing = { themeEntryName, looksLikeIconTheme }
export {
__testing,
extractThemes,
fetchMarketplaceThemes,
readCentralDirectory,
searchMarketplaceThemes
}
export { __testing, extractThemes, fetchMarketplaceThemes, readCentralDirectory, searchMarketplaceThemes }

View file

@ -7,14 +7,16 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { computeWindowOptions,
import {
computeWindowOptions,
debounce,
DEFAULT_HEIGHT,
DEFAULT_WIDTH,
MIN_HEIGHT,
MIN_WIDTH,
onScreen,
sanitizeWindowState } from './window-state'
sanitizeWindowState
} from './window-state'
// A single 1920×1080 monitor (work area trimmed for the taskbar).
const PRIMARY = [{ workArea: { x: 0, y: 0, width: 1920, height: 1040 } }]

View file

@ -21,26 +21,29 @@ const MIN_VISIBLE = 48
const finite = v => typeof v === 'number' && Number.isFinite(v)
const clamp = (v, lo, hi) => Math.max(lo, Math.min(v, hi))
interface SanitizedWindowState{
width: number, height: number, isMaximized: boolean, x?: number,y?: number
interface SanitizedWindowState {
width: number
height: number
isMaximized: boolean
x?: number
y?: number
}
// Parse raw JSON → clean state, or null if garbage. width/height are required
// and floored; x/y survive only as a finite pair; isMaximized is strict.
function sanitizeWindowState(raw?: any): SanitizedWindowState | null
{
if (!raw || typeof raw !== 'object' || !finite(raw.width) || !finite(raw.height)) {return null}
function sanitizeWindowState(raw?: any): SanitizedWindowState | null {
if (!raw || typeof raw !== 'object' || !finite(raw.width) || !finite(raw.height)) {
return null
}
const state: SanitizedWindowState = {
width: Math.max(MIN_WIDTH, Math.round(raw.width)),
height: Math.max(MIN_HEIGHT, Math.round(raw.height)),
isMaximized: raw.isMaximized === true,
isMaximized: raw.isMaximized === true
}
if (finite(raw.x) && finite(raw.y)) {
state.x = Math.round(raw.x);
state.x = Math.round(raw.x)
state.y = Math.round(raw.y)
}
@ -50,10 +53,14 @@ function sanitizeWindowState(raw?: any): SanitizedWindowState | null
// True when `bounds` overlaps some display's work area by ≥ MIN_VISIBLE on both
// axes. `displays` is Electron's screen.getAllDisplays() shape.
function onScreen(bounds, displays) {
if (!Array.isArray(displays)) {return false}
if (!Array.isArray(displays)) {
return false
}
return displays.some(({ workArea: a } = {}) => {
if (!a) {return false}
if (!a) {
return false
}
const x = Math.min(bounds.x + bounds.width, a.x + a.width) - Math.max(bounds.x, a.x)
const y = Math.min(bounds.y + bounds.height, a.y + a.height) - Math.max(bounds.y, a.y)
@ -97,7 +104,7 @@ function computeWindowOptions(state, displays): WindowOptions {
finite(state.y) &&
onScreen({ x: state.x, y: state.y, width: opts.width, height: opts.height }, displays)
) {
opts.x = state.x;
opts.x = state.x
opts.y = state.y
}
@ -127,7 +134,8 @@ function debounce(fn, delayMs) {
return debounced
}
export { computeWindowOptions,
export {
computeWindowOptions,
debounce,
DEFAULT_HEIGHT,
DEFAULT_WIDTH,
@ -135,4 +143,5 @@ export { computeWindowOptions,
MIN_VISIBLE,
MIN_WIDTH,
onScreen,
sanitizeWindowState }
sanitizeWindowState
}

View file

@ -9,7 +9,6 @@ const ELECTRON_DIR = path.dirname(fileURLToPath(import.meta.url))
// TODO FIXME these tests all grep source code for specific things. This is an antipattern.
// Tests should NEVER read src, only assert behavior.
function readElectronFile(name) {
return fs.readFileSync(path.join(ELECTRON_DIR, name), 'utf8').replace(/\r\n/g, '\n')
}

View file

@ -20,7 +20,9 @@ import { execFileSync } from 'node:child_process'
// Returns the raw value string (spaces inside the value preserved), or null when
// the requested value line isn't present.
function parseRegQueryValue(stdout, name) {
if (!stdout || !name) {return null}
if (!stdout || !name) {
return null
}
const typePattern = /^(\S+)\s+(?:REG_SZ|REG_EXPAND_SZ|REG_MULTI_SZ|REG_DWORD|REG_QWORD|REG_BINARY|REG_NONE)\s+(.*)$/
for (const rawLine of String(stdout).split(/\r?\n/)) {
@ -39,7 +41,9 @@ function parseRegQueryValue(stdout, name) {
// unexpanded references; plain REG_SZ paths have none, so this is a no-op for
// the common F:\... case. Unknown references are left verbatim.
function expandWindowsEnvRefs(value, env = process.env) {
if (!value) {return value}
if (!value) {
return value
}
return value.replace(/%([^%]+)%/g, (whole, name) => {
const key = Object.keys(env).find(k => k.toUpperCase() === String(name).toUpperCase())
@ -51,10 +55,21 @@ function expandWindowsEnvRefs(value, env = process.env) {
// Read a User-scoped env var from HKCU\Environment. Windows-only: returns null
// off-Windows (without spawning), on any spawn error, when `reg` exits non-zero
// (the value doesn't exist), or when the value is empty.
function readWindowsUserEnvVar(name, { platform = process.platform, env = process.env, exec = execFileSync }: {
platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, exec?: typeof execFileSync | ((file?: string, args?: any) => string)
} = {}) {
if (platform !== 'win32' || !name) {return null}
function readWindowsUserEnvVar(
name,
{
platform = process.platform,
env = process.env,
exec = execFileSync
}: {
platform?: NodeJS.Platform
env?: NodeJS.ProcessEnv
exec?: typeof execFileSync | ((file?: string, args?: any) => string)
} = {}
) {
if (platform !== 'win32' || !name) {
return null
}
let stdout
try {
@ -70,12 +85,12 @@ function readWindowsUserEnvVar(name, { platform = process.platform, env = proces
const raw = parseRegQueryValue(stdout, name)
if (raw == null) {return null}
if (raw == null) {
return null
}
const expanded = expandWindowsEnvRefs(raw, env).trim()
return expanded || null
}
export { expandWindowsEnvRefs,
parseRegQueryValue,
readWindowsUserEnvVar }
export { expandWindowsEnvRefs, parseRegQueryValue, readWindowsUserEnvVar }

View file

@ -1,7 +1,7 @@
import path from 'node:path'
/** True when `dir` lives inside a packaged app bundle / install tree. */
function isPackagedInstallPath(dir, { installRoots, isPackaged }: { installRoots: string[], isPackaged:boolean }) {
function isPackagedInstallPath(dir, { installRoots, isPackaged }: { installRoots: string[]; isPackaged: boolean }) {
if (!isPackaged || !dir) {
return false
}

View file

@ -1,10 +1,12 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { decodeClipboardImageBase64,
import {
decodeClipboardImageBase64,
encodePowerShellCommand,
powershellCandidates,
readWslWindowsClipboardImage } from './wsl-clipboard-image'
readWslWindowsClipboardImage
} from './wsl-clipboard-image'
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
@ -66,11 +68,11 @@ test('readWslWindowsClipboardImage decodes the first candidate that returns a PN
test('readWslWindowsClipboardImage returns null and stops when stdout is empty (no image)', () => {
let count = 0
const exec =(() => {
const exec = (() => {
count += 1
return ''
} )as any
return ''
}) as any
const result = readWslWindowsClipboardImage({
exec,

View file

@ -34,7 +34,9 @@ function powershellCandidates() {
function decodeClipboardImageBase64(stdout) {
const b64 = String(stdout || '').trim()
if (!b64) {return null}
if (!b64) {
return null
}
let buffer
@ -57,7 +59,10 @@ function decodeClipboardImageBase64(stdout) {
// Read the Windows clipboard image from inside WSL. Returns a PNG Buffer, or
// null when there's no image, PowerShell is unreachable, or output is invalid.
// Linux-only by contract (caller gates on IS_WSL); never throws.
function readWslWindowsClipboardImage({ exec = execFileSync, candidates = powershellCandidates() }: {exec?: typeof execFileSync, candidates?: string[]} = {}) {
function readWslWindowsClipboardImage({
exec = execFileSync,
candidates = powershellCandidates()
}: { exec?: typeof execFileSync; candidates?: string[] } = {}) {
const encoded = encodePowerShellCommand(PS_SCRIPT)
for (const ps of candidates) {
@ -78,10 +83,14 @@ function readWslWindowsClipboardImage({ exec = execFileSync, candidates = powers
const decoded = decodeClipboardImageBase64(stdout)
if (decoded) {return decoded}
if (decoded) {
return decoded
}
// Empty stdout = no image on the clipboard; stop, don't try fallbacks.
if (String(stdout || '').trim() === '') {return null}
if (String(stdout || '').trim() === '') {
return null
}
} catch {
// This powershell.exe candidate is missing/failed — try the next one.
}
@ -90,7 +99,4 @@ function readWslWindowsClipboardImage({ exec = execFileSync, candidates = powers
return null
}
export { decodeClipboardImageBase64,
encodePowerShellCommand,
powershellCandidates,
readWslWindowsClipboardImage }
export { decodeClipboardImageBase64, encodePowerShellCommand, powershellCandidates, readWslWindowsClipboardImage }

View file

@ -13,7 +13,9 @@ const MIN_ZOOM_LEVEL = -9
const MAX_ZOOM_LEVEL = 9
export function clampZoomLevel(value) {
if (!Number.isFinite(value)) {return 0}
if (!Number.isFinite(value)) {
return 0
}
return Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
}
@ -23,7 +25,9 @@ export function zoomLevelToPercent(level) {
}
export function percentToZoomLevel(percent) {
if (!Number.isFinite(percent) || percent <= 0) {return 0}
if (!Number.isFinite(percent) || percent <= 0) {
return 0
}
return clampZoomLevel(Math.log(percent / 100) / Math.log(ZOOM_FACTOR_BASE))
}
}

View file

@ -216,7 +216,10 @@ export function ArtifactsView({ setStatusbarItemGroup: _setStatusbarItemGroup, .
const titles = [...new Set(artifacts.map(artifact => artifact.sessionTitle).filter(Boolean))].slice(0, 2)
const hints = [...extensions.map(ext => t.common.tryHint(`.${ext}`)), ...titles.map(title => t.common.tryHint(title))]
const hints = [
...extensions.map(ext => t.common.tryHint(`.${ext}`)),
...titles.map(title => t.common.tryHint(title))
]
return hints.length > 0 ? hints : undefined
}, [artifacts, t])

View file

@ -323,7 +323,9 @@ export function SkillsHub({ query }: SkillsHubProps) {
<div className="flex shrink-0 items-center justify-between gap-3 px-4 pb-1.5 text-[0.68rem] text-(--ui-text-tertiary)">
<span className="min-w-0 truncate">
{term.length > 0 ? h.resultCount(results.length, null) : h.featured}
{anyFetching && results.length > 0 && <span className="ml-2 text-(--ui-text-quaternary)">{h.searching}</span>}
{anyFetching && results.length > 0 && (
<span className="ml-2 text-(--ui-text-quaternary)">{h.searching}</span>
)}
</span>
{hasInstalled && (

View file

@ -492,7 +492,11 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
try {
await editLearningNode(skillEditor.name, skillDraft)
notify({ kind: 'success', title: t.skills.skillUpdated, message: t.skills.appliesToNewSessions(skillEditor.name) })
notify({
kind: 'success',
title: t.skills.skillUpdated,
message: t.skills.appliesToNewSessions(skillEditor.name)
})
setSkillEditor(null)
void refreshCapabilities()
} catch (err) {
@ -577,7 +581,9 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
left={sortButton(skillsSortDesc, () => $skillsSortDesc.set(!$skillsSortDesc.get()))}
right={
<ListStripMenu
items={[{ disabled: bulkBusy, label: t.skills.disableUnused, onSelect: () => void disableUnused() }]}
items={[
{ disabled: bulkBusy, label: t.skills.disableUnused, onSelect: () => void disableUnused() }
]}
label={t.skills.tabSkills}
toggle={bulkSwitch(allSkillsEnabled)}
/>

View file

@ -233,5 +233,11 @@ export function CopyButton({
)
// Only icon-only buttons need a tooltip; the text variant already shows its label.
return appearance === 'icon' ? <Tip label={feedbackLabel} side={side ?? 'bottom'}>{button}</Tip> : button
return appearance === 'icon' ? (
<Tip label={feedbackLabel} side={side ?? 'bottom'}>
{button}
</Tip>
) : (
button
)
}

View file

@ -281,7 +281,8 @@ export const zhHant = defineLocale({
toolViewTitle: '工具呼叫顯示',
toolViewDesc: '產品模式會隱藏原始工具 payload技術模式會顯示完整輸入/輸出。',
uiScaleTitle: '介面縮放',
uiScaleDesc: (percent: number) => `縮放整個應用程式的文字與介面。也可使用 Cmd/Ctrl 加 +、- 或 0 調整。目前:${percent}%`,
uiScaleDesc: (percent: number) =>
`縮放整個應用程式的文字與介面。也可使用 Cmd/Ctrl 加 +、- 或 0 調整。目前:${percent}%`,
translucencyTitle: '視窗透明',
translucencyDesc: '讓整個視窗透出桌面。僅支援 macOS 與 Windows。',
embedsTitle: '內嵌預覽',

View file

@ -372,7 +372,8 @@ export const zh: Translations = {
toolViewTitle: '工具调用显示',
toolViewDesc: '产品模式隐藏原始工具数据;技术模式显示完整输入/输出。',
uiScaleTitle: '界面缩放',
uiScaleDesc: (percent: number) => `缩放整个应用的文字和界面。也可使用 Cmd/Ctrl 加 +、- 或 0 调整。当前:${percent}%`,
uiScaleDesc: (percent: number) =>
`缩放整个应用的文字和界面。也可使用 Cmd/Ctrl 加 +、- 或 0 调整。当前:${percent}%`,
translucencyTitle: '窗口透明',
translucencyDesc: '让整个窗口透出桌面。仅支持 macOS 和 Windows。',
embedsTitle: '内嵌预览',