mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +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>
176 lines
4.8 KiB
TypeScript
176 lines
4.8 KiB
TypeScript
import { spawn, type ChildProcess, type SpawnOptions } from 'node:child_process'
|
|
import {
|
|
RELAY_SENTINEL,
|
|
FrameDecoder,
|
|
encodeJsonRpcFrame,
|
|
parseJsonRpcMessage,
|
|
MessageType,
|
|
type JsonRpcRequest,
|
|
type JsonRpcResponse,
|
|
type JsonRpcNotification
|
|
} from './protocol'
|
|
|
|
export type RelayProcess = {
|
|
proc: ChildProcess
|
|
responses: (JsonRpcResponse | JsonRpcNotification)[]
|
|
sentinelReceived: Promise<void>
|
|
send: (method: string, params?: Record<string, unknown>) => number
|
|
sendNotification: (method: string, params?: Record<string, unknown>) => void
|
|
waitForResponse: (id: number, timeoutMs?: number) => Promise<JsonRpcResponse>
|
|
waitForNotification: (method: string, timeoutMs?: number) => Promise<JsonRpcNotification>
|
|
kill: (signal?: NodeJS.Signals) => void
|
|
waitForExit: (timeoutMs?: number) => Promise<number | null>
|
|
}
|
|
|
|
export function spawnRelay(
|
|
entryPath: string,
|
|
args: string[] = [],
|
|
options: Pick<SpawnOptions, 'env'> = {}
|
|
): RelayProcess {
|
|
const proc = spawn('node', [entryPath, ...args], {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
...options
|
|
})
|
|
|
|
const responses: (JsonRpcResponse | JsonRpcNotification)[] = []
|
|
let nextSeq = 1
|
|
let sentinelResolved = false
|
|
let stdoutBuffer = Buffer.alloc(0)
|
|
let sentinelResolve: () => void
|
|
let decoderActive = false
|
|
|
|
const sentinelReceived = new Promise<void>((resolve) => {
|
|
sentinelResolve = resolve
|
|
})
|
|
|
|
const decoder = new FrameDecoder((frame) => {
|
|
if (frame.type !== MessageType.Regular) {
|
|
return
|
|
}
|
|
try {
|
|
const msg = parseJsonRpcMessage(frame.payload)
|
|
responses.push(msg as JsonRpcResponse | JsonRpcNotification)
|
|
} catch {
|
|
/* skip malformed */
|
|
}
|
|
})
|
|
|
|
proc.stdout!.on('data', (chunk: Buffer) => {
|
|
if (!sentinelResolved) {
|
|
stdoutBuffer = Buffer.concat([stdoutBuffer, chunk])
|
|
const sentinelBuf = Buffer.from(RELAY_SENTINEL, 'utf-8')
|
|
const idx = stdoutBuffer.indexOf(sentinelBuf)
|
|
if (idx !== -1) {
|
|
sentinelResolved = true
|
|
decoderActive = true
|
|
sentinelResolve()
|
|
const remainder = stdoutBuffer.subarray(idx + sentinelBuf.length)
|
|
if (remainder.length > 0) {
|
|
decoder.feed(remainder)
|
|
}
|
|
}
|
|
} else if (decoderActive) {
|
|
decoder.feed(chunk)
|
|
}
|
|
})
|
|
|
|
proc.stderr!.on('data', () => {
|
|
/* drain */
|
|
})
|
|
|
|
const send = (method: string, params?: Record<string, unknown>): number => {
|
|
const id = nextSeq++
|
|
const req: JsonRpcRequest = {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
method,
|
|
...(params !== undefined ? { params } : {})
|
|
}
|
|
proc.stdin!.write(encodeJsonRpcFrame(req, id, 0))
|
|
return id
|
|
}
|
|
|
|
const sendNotification = (method: string, params?: Record<string, unknown>): void => {
|
|
const seq = nextSeq++
|
|
const notif: JsonRpcNotification = {
|
|
jsonrpc: '2.0',
|
|
method,
|
|
...(params !== undefined ? { params } : {})
|
|
}
|
|
proc.stdin!.write(encodeJsonRpcFrame(notif, seq, 0))
|
|
}
|
|
|
|
const waitForResponse = (id: number, timeoutMs = 5000): Promise<JsonRpcResponse> => {
|
|
return new Promise((resolve, reject) => {
|
|
const deadline = Date.now() + timeoutMs
|
|
const check = () => {
|
|
const found = responses.find((r) => 'id' in r && r.id === id) as JsonRpcResponse | undefined
|
|
if (found) {
|
|
resolve(found)
|
|
return
|
|
}
|
|
if (Date.now() > deadline) {
|
|
reject(new Error(`Timed out waiting for response id=${id}`))
|
|
return
|
|
}
|
|
setTimeout(check, 10)
|
|
}
|
|
check()
|
|
})
|
|
}
|
|
|
|
const waitForNotification = (method: string, timeoutMs = 5000): Promise<JsonRpcNotification> => {
|
|
return new Promise((resolve, reject) => {
|
|
const deadline = Date.now() + timeoutMs
|
|
const seen = responses.length
|
|
const check = () => {
|
|
for (let i = seen; i < responses.length; i++) {
|
|
const r = responses[i]
|
|
if ('method' in r && r.method === method) {
|
|
resolve(r as JsonRpcNotification)
|
|
return
|
|
}
|
|
}
|
|
if (Date.now() > deadline) {
|
|
reject(new Error(`Timed out waiting for notification "${method}"`))
|
|
return
|
|
}
|
|
setTimeout(check, 10)
|
|
}
|
|
check()
|
|
})
|
|
}
|
|
|
|
const kill = (signal: NodeJS.Signals = 'SIGTERM') => {
|
|
proc.kill(signal)
|
|
}
|
|
|
|
const waitForExit = (timeoutMs = 5000): Promise<number | null> => {
|
|
return new Promise((resolve, reject) => {
|
|
if (proc.exitCode !== null) {
|
|
resolve(proc.exitCode)
|
|
return
|
|
}
|
|
const timer = setTimeout(() => {
|
|
reject(new Error('Timed out waiting for process exit'))
|
|
}, timeoutMs)
|
|
proc.once('exit', (code) => {
|
|
clearTimeout(timer)
|
|
resolve(code)
|
|
})
|
|
})
|
|
}
|
|
|
|
return {
|
|
proc,
|
|
responses,
|
|
sentinelReceived,
|
|
send,
|
|
sendNotification,
|
|
waitForResponse,
|
|
waitForNotification,
|
|
kill,
|
|
waitForExit
|
|
}
|
|
}
|