mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 00:02:56 +00:00
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day minimum-release-age supply-chain guard; nothing here needs it). The bump is a no-op on the existing config. Enable 3 error rules (backlog autofixed to zero in this commit) and 4 warn rules (surface signal without gating CI): error (autofixed, behavior-preserving): - unicorn/prefer-node-protocol (~1531 sites: bare builtin -> node:) - typescript/no-import-type-side-effects (~36: all-inline-type -> import type) - unicorn/no-array-reverse (19: copy-then-reverse -> toReversed) warn (real signal, current fires are test-only/correct): - unicorn/no-array-fill-with-reference-type (aliasing footgun guard) - typescript/no-unsafe-function-type (bans bare Function type) - unicorn/prefer-array-flat-map (map().flat() -> flatMap()) - unicorn/prefer-regexp-test (.match() in bool ctx -> .test()) mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix ran from root and covered mobile/ too. Verification (all green): oxlint 0 errors (root+mobile+aux configs), oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed, builds (electron-vite + web + cli) succeed. node: rewrites confirmed to skip embedded SSH/CLI string payloads (AST-only); all toReversed sites verified to operate on fresh copies or write-once locals. * chore(lint): bump mobile oxlint to 1.71 so inherited rules parse mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile && oxlint' failed to parse the new rule. Bump mobile to match root (1.71). Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit pass, vitest 978 passed / 0 failed. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
154 lines
5.0 KiB
TypeScript
154 lines
5.0 KiB
TypeScript
import { execFile as execFileCb } from 'node:child_process'
|
|
import { readlink } from 'node:fs/promises'
|
|
|
|
/**
|
|
* Resolve the current working directory of a local process by pid.
|
|
*
|
|
* Why duplicated from `src/relay/pty-shell-utils.ts`: the relay and Electron
|
|
* main process have separate build graphs, and cross-importing across them
|
|
* is not a pattern used in this repo. The function is short and pure, and
|
|
* the duplication is cheaper than reshaping both bundle graphs.
|
|
*
|
|
* Tries `/proc/<pid>/cwd` on Linux, falls back to `lsof -d cwd` on macOS.
|
|
* Returns `''` when neither works (including Windows, where `/proc` is
|
|
* absent and `lsof` is not native).
|
|
*
|
|
* Results are coalesced and briefly cached per-pid: rapid repeat calls
|
|
* (e.g. chained Cmd+D on macOS) reuse a single `lsof` child rather than
|
|
* stacking concurrent subprocesses whose results the caller may discard
|
|
* after its own timeout.
|
|
*/
|
|
const CACHE_TTL_MS = 1500
|
|
const CACHE_MAX_ENTRIES = 256
|
|
const LSOF_TIMEOUT_MS = 1500
|
|
|
|
type CacheEntry = { value: string; at: number }
|
|
const resultCache = new Map<number, CacheEntry>()
|
|
const inflight = new Map<number, Promise<string>>()
|
|
|
|
export async function resolveProcessCwd(pid: number): Promise<string> {
|
|
// Assumes the caller holds a live reference to `pid` for the TTL window.
|
|
// If a pid is recycled within 1.5s of a prior query, the cache would
|
|
// return the previous process's cwd — acceptable because our callers
|
|
// (getCwd on an active pty/session) can't outlive their pid.
|
|
const now = Date.now()
|
|
sweepExpiredResultCache(now)
|
|
const cached = resultCache.get(pid)
|
|
if (cached && now - cached.at < CACHE_TTL_MS) {
|
|
return cached.value
|
|
}
|
|
const existing = inflight.get(pid)
|
|
if (existing) {
|
|
return existing
|
|
}
|
|
// Populate the cache inside the shared promise chain so every awaiter
|
|
// (including any second caller that joined via `inflight`) observes the
|
|
// result through the same write, rather than racing on a post-await set.
|
|
const promise = doResolve(pid).then((value) => {
|
|
rememberResult(pid, value, Date.now())
|
|
inflight.delete(pid)
|
|
return value
|
|
})
|
|
inflight.set(pid, promise)
|
|
return promise
|
|
}
|
|
|
|
function sweepExpiredResultCache(now: number): void {
|
|
for (const [cachedPid, entry] of resultCache) {
|
|
if (now - entry.at >= CACHE_TTL_MS) {
|
|
resultCache.delete(cachedPid)
|
|
}
|
|
}
|
|
}
|
|
|
|
function rememberResult(pid: number, value: string, now: number): void {
|
|
resultCache.set(pid, { value, at: now })
|
|
// Why: terminals can churn through many OS PIDs in one app session. A short
|
|
// TTL only helps if the old PID is queried again, so also bound unique keys.
|
|
while (resultCache.size > CACHE_MAX_ENTRIES) {
|
|
const oldest = resultCache.keys().next().value
|
|
if (oldest === undefined) {
|
|
break
|
|
}
|
|
resultCache.delete(oldest)
|
|
}
|
|
}
|
|
|
|
async function doResolve(pid: number): Promise<string> {
|
|
// Why: skip an existsSync gate and just try the readlink. The check+read
|
|
// pair races a concurrent process exit the same way the lsof+existsSync
|
|
// pair did, and the catch already falls through to lsof.
|
|
try {
|
|
return await readlink(`/proc/${pid}/cwd`)
|
|
} catch {
|
|
/* fall through */
|
|
}
|
|
|
|
try {
|
|
// Why: `-a` ANDs the -p and -d filters. Without it, macOS lsof ORs them
|
|
// and emits cwd records for every process on the system, so the n-line
|
|
// scan below picks up the first unrelated process (often pid ~391 with
|
|
// cwd `/`) and returns `/` regardless of the target pid's real cwd.
|
|
const stdout = await readCwdWithLsof(pid)
|
|
for (const line of stdout.split('\n')) {
|
|
if (line.startsWith('n') && line.includes('/')) {
|
|
// Why: lsof -d cwd is authoritative — don't second-guess it with
|
|
// existsSync. A concurrent rmdir would race the check and cause us
|
|
// to drop the correct answer; node-pty handles a missing cwd on
|
|
// spawn anyway.
|
|
return line.slice(1)
|
|
}
|
|
}
|
|
} catch {
|
|
/* fall through */
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
function readCwdWithLsof(pid: number): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
let settled = false
|
|
let child: ReturnType<typeof execFileCb> | undefined
|
|
const timer = setTimeout(() => {
|
|
if (settled) {
|
|
return
|
|
}
|
|
settled = true
|
|
child?.kill()
|
|
reject(new Error(`lsof timed out after ${LSOF_TIMEOUT_MS}ms`))
|
|
}, LSOF_TIMEOUT_MS)
|
|
|
|
const settle = (callback: () => void): void => {
|
|
if (settled) {
|
|
return
|
|
}
|
|
settled = true
|
|
clearTimeout(timer)
|
|
callback()
|
|
}
|
|
|
|
// Why: execFile's timeout only signals lsof; a missing callback would
|
|
// otherwise leave the shared per-pid cwd lookup promise cached forever.
|
|
try {
|
|
child = execFileCb(
|
|
'lsof',
|
|
['-a', '-p', String(pid), '-d', 'cwd', '-Fn'],
|
|
{
|
|
encoding: 'utf-8',
|
|
timeout: LSOF_TIMEOUT_MS
|
|
},
|
|
(error, stdout) => {
|
|
if (error) {
|
|
settle(() => reject(error))
|
|
return
|
|
}
|
|
settle(() => resolve(String(stdout)))
|
|
}
|
|
)
|
|
} catch (error) {
|
|
settle(() => reject(error))
|
|
}
|
|
})
|
|
}
|