mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
cleanup(desktop): npm run fix for fmtting
we should run this as part of merges at some point :)
This commit is contained in:
parent
7a65530fa5
commit
56a8e81d33
54 changed files with 1163 additions and 484 deletions
|
|
@ -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)]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: [] }
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -14,5 +14,4 @@ function setJsonRequestHeaders(request) {
|
|||
request.setHeader('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
export { serializeJsonBody,
|
||||
setJsonRequestHeaders }
|
||||
export { serializeJsonBody, setJsonRequestHeaders }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -115,8 +115,10 @@ function createSessionWindowRegistry() {
|
|||
}
|
||||
}
|
||||
|
||||
export { buildSessionWindowUrl,
|
||||
export {
|
||||
buildSessionWindowUrl,
|
||||
chatWindowWebPreferences,
|
||||
createSessionWindowRegistry,
|
||||
SESSION_WINDOW_MIN_HEIGHT,
|
||||
SESSION_WINDOW_MIN_WIDTH }
|
||||
SESSION_WINDOW_MIN_WIDTH
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -334,10 +334,4 @@ async function fetchMarketplaceThemes(id) {
|
|||
|
||||
const __testing = { themeEntryName, looksLikeIconTheme }
|
||||
|
||||
export {
|
||||
__testing,
|
||||
extractThemes,
|
||||
fetchMarketplaceThemes,
|
||||
readCentralDirectory,
|
||||
searchMarketplaceThemes
|
||||
}
|
||||
export { __testing, extractThemes, fetchMarketplaceThemes, readCentralDirectory, searchMarketplaceThemes }
|
||||
|
|
|
|||
|
|
@ -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 } }]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: '內嵌預覽',
|
||||
|
|
|
|||
|
|
@ -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: '内嵌预览',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue