Bridge WSL Codex sessions into runtime home (#7477)
This commit is contained in:
parent
6aed5c1122
commit
377bde351c
|
|
@ -484,6 +484,134 @@ describe('CodexRuntimeHomeService', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('starts WSL session bridging after materializing the WSL launch home', async () => {
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
const startWslCodexSessionBridgeInBackground = vi.fn(() => Promise.resolve())
|
||||
vi.doMock('../codex/wsl-codex-session-bridge', () => ({
|
||||
startWslCodexSessionBridgeInBackground
|
||||
}))
|
||||
const wslHome = join(testState.userDataDir, 'wsl-home')
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
}))
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
activeCodexManagedAccountId: null,
|
||||
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: null } }
|
||||
})
|
||||
)
|
||||
|
||||
try {
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
const wslRuntimeHomePath = join(
|
||||
wslHome,
|
||||
'.local',
|
||||
'share',
|
||||
'orca',
|
||||
'codex-runtime-home',
|
||||
'home'
|
||||
)
|
||||
|
||||
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
|
||||
wslRuntimeHomePath
|
||||
)
|
||||
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledTimes(1)
|
||||
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledWith({
|
||||
distro: 'Ubuntu',
|
||||
systemCodexHomePath: join(wslHome, '.codex'),
|
||||
managedCodexHomePath: wslRuntimeHomePath
|
||||
})
|
||||
} finally {
|
||||
vi.doUnmock('../codex/wsl-codex-session-bridge')
|
||||
vi.doUnmock('../wsl')
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('starts WSL session bridging for the distro used by the materialized runtime home', async () => {
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
const startWslCodexSessionBridgeInBackground = vi.fn(() => Promise.resolve())
|
||||
vi.doMock('../codex/wsl-codex-session-bridge', () => ({
|
||||
startWslCodexSessionBridgeInBackground
|
||||
}))
|
||||
const wslHome = join(testState.userDataDir, 'debian-wsl-home')
|
||||
const wslRuntimeHomePath = join(
|
||||
wslHome,
|
||||
'.local',
|
||||
'share',
|
||||
'orca',
|
||||
'codex-runtime-home',
|
||||
'home'
|
||||
)
|
||||
vi.doMock('../wsl', () => ({
|
||||
getDefaultWslDistro: () => null,
|
||||
getWslHome: (distro: string) => (distro === 'Debian' ? wslHome : null)
|
||||
}))
|
||||
vi.doMock('../../shared/wsl-paths', () => ({
|
||||
parseWslUncPath: (candidate: string) =>
|
||||
candidate === wslRuntimeHomePath
|
||||
? {
|
||||
distro: 'Debian',
|
||||
linuxPath: '/home/alice/.local/share/orca/codex-runtime-home/home'
|
||||
}
|
||||
: null
|
||||
}))
|
||||
const managedHomePath = createManagedAuth(
|
||||
testState.userDataDir,
|
||||
'debian-account',
|
||||
'{"account":"debian"}\n'
|
||||
)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'debian-account',
|
||||
email: 'debian@example.com',
|
||||
managedHomePath,
|
||||
managedHomeRuntime: 'wsl',
|
||||
wslDistro: 'Debian',
|
||||
wslLinuxHomePath: '/home/alice/.local/share/orca/codex-accounts/debian/home',
|
||||
providerAccountId: null,
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: null,
|
||||
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Debian: 'debian-account' } }
|
||||
})
|
||||
)
|
||||
|
||||
try {
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: null })).toBe(
|
||||
wslRuntimeHomePath
|
||||
)
|
||||
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledWith({
|
||||
distro: 'Debian',
|
||||
systemCodexHomePath: join(wslHome, '.codex'),
|
||||
managedCodexHomePath: wslRuntimeHomePath
|
||||
})
|
||||
} finally {
|
||||
vi.doUnmock('../codex/wsl-codex-session-bridge')
|
||||
vi.doUnmock('../wsl')
|
||||
vi.doUnmock('../../shared/wsl-paths')
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('restores the system-default snapshot when no managed account is selected', async () => {
|
||||
const runtimeAuthPath = getRuntimeCodexAuthPath()
|
||||
writeFileSync(getSystemCodexAuthPath(), '{"account":"system"}\n', 'utf-8')
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
syncSystemCodexResourcesIntoManagedHome
|
||||
} from '../codex/codex-home-paths'
|
||||
import { startSystemCodexSessionBridgeInBackground } from '../codex/codex-session-bridge'
|
||||
import { startWslCodexSessionBridgeInBackground } from '../codex/wsl-codex-session-bridge'
|
||||
import {
|
||||
prepareSystemConfigForFreshRuntimeMirror,
|
||||
syncSystemConfigIntoManagedCodexHome
|
||||
|
|
@ -134,10 +135,11 @@ export class CodexRuntimeHomeService {
|
|||
prepareForCodexLaunch(target?: CodexAccountSelectionTarget): string | null {
|
||||
if (target?.runtime === 'wsl') {
|
||||
const wslTarget = this.resolveWslDefaultTarget(target)
|
||||
return (
|
||||
const runtimeHomePath =
|
||||
this.syncWslRuntimeForCurrentSelection(wslTarget) ??
|
||||
this.getWslSystemCodexHomePath(wslTarget)
|
||||
)
|
||||
this.startWslSessionBridgeForLaunch(wslTarget, runtimeHomePath)
|
||||
return runtimeHomePath
|
||||
}
|
||||
this.syncForCurrentSelection()
|
||||
syncSystemCodexResourcesIntoManagedHome()
|
||||
|
|
@ -148,6 +150,34 @@ export class CodexRuntimeHomeService {
|
|||
return this.getRuntimeHomePath()
|
||||
}
|
||||
|
||||
private startWslSessionBridgeForLaunch(
|
||||
target: CodexAccountSelectionTarget,
|
||||
runtimeHomePath: string | null
|
||||
): void {
|
||||
if (process.platform !== 'win32' || !runtimeHomePath) {
|
||||
return
|
||||
}
|
||||
const runtimeHomeWsl = parseWslUncPath(runtimeHomePath)
|
||||
const distro = target.wslDistro?.trim() || runtimeHomeWsl?.distro || getDefaultWslDistro()
|
||||
if (!distro) {
|
||||
return
|
||||
}
|
||||
const systemCodexHomePath = this.getWslSystemCodexHomePath({
|
||||
runtime: 'wsl',
|
||||
wslDistro: distro
|
||||
})
|
||||
if (!systemCodexHomePath || systemCodexHomePath === runtimeHomePath) {
|
||||
return
|
||||
}
|
||||
// Why: WSL history must be hardlinked inside the distro; host-side links
|
||||
// cannot bridge Windows and WSL filesystems in a resume-visible way.
|
||||
void startWslCodexSessionBridgeInBackground({
|
||||
distro,
|
||||
systemCodexHomePath,
|
||||
managedCodexHomePath: runtimeHomePath
|
||||
})
|
||||
}
|
||||
|
||||
getHostRuntimeHomePath(): string {
|
||||
return this.getRuntimeHomePath()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ChildProcess } from 'node:child_process'
|
||||
|
||||
const { execFileMock } = vi.hoisted(() => ({
|
||||
execFileMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('node:child_process', () => ({
|
||||
execFile: execFileMock
|
||||
}))
|
||||
|
||||
import {
|
||||
buildWslCodexSessionBridgeShellCommand,
|
||||
resolveWslCodexSessionBridgeLinuxPaths,
|
||||
startWslCodexSessionBridgeInBackground,
|
||||
syncWslCodexSessionsIntoManagedHome
|
||||
} from './wsl-codex-session-bridge'
|
||||
|
||||
function mockExecFileSuccess(stdout = '{"scannedFiles":2,"linkedFiles":1}\n'): void {
|
||||
execFileMock.mockImplementation(
|
||||
(
|
||||
_command: string,
|
||||
_args: string[],
|
||||
_options: unknown,
|
||||
callback: (error: Error | null, stdout: string, stderr: string) => void
|
||||
): ChildProcess => {
|
||||
callback(null, stdout, '')
|
||||
return {} as ChildProcess
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
execFileMock.mockReset()
|
||||
})
|
||||
|
||||
describe('syncWslCodexSessionsIntoManagedHome', () => {
|
||||
it('runs a WSL hardlink bridge from the WSL system sessions into the managed home', async () => {
|
||||
mockExecFileSuccess()
|
||||
|
||||
const summary = await syncWslCodexSessionsIntoManagedHome({
|
||||
distro: 'Ubuntu',
|
||||
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
|
||||
managedCodexHomePath:
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
|
||||
})
|
||||
|
||||
expect(summary).toEqual({ scannedFiles: 2, linkedFiles: 1 })
|
||||
expect(execFileMock).toHaveBeenCalledTimes(1)
|
||||
const firstCall = execFileMock.mock.calls[0]
|
||||
expect(firstCall).toBeDefined()
|
||||
const [command, args, options] = firstCall as [
|
||||
string,
|
||||
string[],
|
||||
{ timeout?: number; windowsHide?: boolean }
|
||||
]
|
||||
expect(command).toBe('wsl.exe')
|
||||
expect(args.slice(0, 5)).toEqual(['-d', 'Ubuntu', '--', 'bash', '-lc'])
|
||||
expect(args).toHaveLength(6)
|
||||
expect(options.timeout).toBe(30_000)
|
||||
expect(options.windowsHide).toBe(true)
|
||||
|
||||
const shellCommand = args[5]
|
||||
expect(shellCommand).toContain("source_sessions_root='/home/alice/.codex/sessions'")
|
||||
expect(shellCommand).toContain(
|
||||
"managed_sessions_root='/home/alice/.local/share/orca/codex-runtime-home/home/sessions'"
|
||||
)
|
||||
expect(shellCommand).toContain(`find "\\$source_sessions_root" -type f -name '*.jsonl' -print0`)
|
||||
expect(shellCommand).toContain('ln -- "\\$source_file" "\\$target_file"')
|
||||
expect(shellCommand).toContain('if [ -e "\\$target_file" ] || [ -L "\\$target_file" ]; then')
|
||||
expect(shellCommand).not.toContain('ln -s')
|
||||
expect(shellCommand).not.toContain('cp ')
|
||||
expect(shellCommand).not.toContain('sqlite')
|
||||
})
|
||||
|
||||
it('does not invoke WSL when paths are not resolvable inside the distro', async () => {
|
||||
const summary = await syncWslCodexSessionsIntoManagedHome({
|
||||
distro: 'Ubuntu',
|
||||
systemCodexHomePath: 'C:\\Users\\alice\\.codex',
|
||||
managedCodexHomePath: 'C:\\Users\\alice\\AppData\\Roaming\\orca\\codex-runtime-home\\home'
|
||||
})
|
||||
|
||||
expect(summary).toEqual({ scannedFiles: 0, linkedFiles: 0 })
|
||||
expect(execFileMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('parses the summary after WSL profile stdout', async () => {
|
||||
mockExecFileSuccess('Welcome to Ubuntu\nprofile output\n{"scannedFiles":4,"linkedFiles":3}\n')
|
||||
|
||||
const summary = await syncWslCodexSessionsIntoManagedHome({
|
||||
distro: 'Ubuntu',
|
||||
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
|
||||
managedCodexHomePath:
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
|
||||
})
|
||||
|
||||
expect(summary).toEqual({ scannedFiles: 4, linkedFiles: 3 })
|
||||
})
|
||||
|
||||
it('coalesces duplicate background bridges for the same WSL target', async () => {
|
||||
mockExecFileSuccess()
|
||||
const target = {
|
||||
distro: 'Ubuntu',
|
||||
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
|
||||
managedCodexHomePath:
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
|
||||
}
|
||||
|
||||
const firstTask = startWslCodexSessionBridgeInBackground(target)
|
||||
const secondTask = startWslCodexSessionBridgeInBackground(target)
|
||||
|
||||
expect(firstTask).toBe(secondTask)
|
||||
await firstTask
|
||||
expect(execFileMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveWslCodexSessionBridgeLinuxPaths', () => {
|
||||
it('requires both homes to belong to the requested distro', () => {
|
||||
expect(
|
||||
resolveWslCodexSessionBridgeLinuxPaths({
|
||||
distro: 'Ubuntu',
|
||||
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
|
||||
managedCodexHomePath:
|
||||
'\\\\wsl.localhost\\Debian\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('accepts Linux paths for direct script construction tests', () => {
|
||||
expect(
|
||||
resolveWslCodexSessionBridgeLinuxPaths({
|
||||
distro: 'Ubuntu',
|
||||
systemCodexHomePath: '/home/alice/.codex',
|
||||
managedCodexHomePath: '/home/alice/.local/share/orca/codex-runtime-home/home'
|
||||
})
|
||||
).toEqual({
|
||||
systemSessionsRoot: '/home/alice/.codex/sessions',
|
||||
managedSessionsRoot: '/home/alice/.local/share/orca/codex-runtime-home/home/sessions'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildWslCodexSessionBridgeShellCommand', () => {
|
||||
it('only targets JSONL session files under sessions', () => {
|
||||
const shellCommand = buildWslCodexSessionBridgeShellCommand({
|
||||
systemSessionsRoot: "/home/alice/.codex/sessions with 'quote'",
|
||||
managedSessionsRoot: '/home/alice/.local/share/orca/codex-runtime-home/home/sessions'
|
||||
})
|
||||
|
||||
expect(shellCommand).toContain(
|
||||
`source_sessions_root='/home/alice/.codex/sessions with '\\''quote'\\'''`
|
||||
)
|
||||
expect(shellCommand).toContain(`-name '*.jsonl'`)
|
||||
expect(shellCommand).not.toContain('.sqlite')
|
||||
})
|
||||
|
||||
it('escapes Linux-side shell variable expansion for wsl.exe argv', () => {
|
||||
const shellCommand = buildWslCodexSessionBridgeShellCommand({
|
||||
systemSessionsRoot: '/home/alice/.codex/sessions',
|
||||
managedSessionsRoot: '/home/alice/.local/share/orca/codex-runtime-home/home/sessions'
|
||||
})
|
||||
|
||||
expect(shellCommand).toContain('\\$source_sessions_root')
|
||||
expect(shellCommand).toContain('\\$source_file')
|
||||
expect(shellCommand).toContain('\\$((scanned_files + 1))')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
import { execFile } from 'node:child_process'
|
||||
import { posix as pathPosix } from 'node:path'
|
||||
import { escapeWslShCommandForWindows } from '../../shared/wsl-login-shell-command'
|
||||
import { parseWslUncPath } from '../../shared/wsl-paths'
|
||||
|
||||
export type WslCodexSessionBridgeTarget = {
|
||||
distro: string
|
||||
systemCodexHomePath: string
|
||||
managedCodexHomePath: string
|
||||
}
|
||||
|
||||
export type WslCodexSessionBridgeLinuxPaths = {
|
||||
systemSessionsRoot: string
|
||||
managedSessionsRoot: string
|
||||
}
|
||||
|
||||
export type WslCodexSessionBridgeSummary = {
|
||||
scannedFiles: number
|
||||
linkedFiles: number
|
||||
}
|
||||
|
||||
const emptySummary: WslCodexSessionBridgeSummary = { scannedFiles: 0, linkedFiles: 0 }
|
||||
const backgroundWslSessionBridgeTasks = new Map<string, Promise<void>>()
|
||||
const WSL_SESSION_BRIDGE_TIMEOUT_MS = 30_000
|
||||
|
||||
export function startWslCodexSessionBridgeInBackground(
|
||||
target: WslCodexSessionBridgeTarget
|
||||
): Promise<void> {
|
||||
const taskKey = getWslSessionBridgeTaskKey(target)
|
||||
const existingTask = backgroundWslSessionBridgeTasks.get(taskKey)
|
||||
if (existingTask) {
|
||||
return existingTask
|
||||
}
|
||||
|
||||
const task = syncWslCodexSessionsIntoManagedHome(target)
|
||||
.catch((error: unknown) => {
|
||||
console.warn('[codex-session-bridge] Background WSL session bridge failed:', error)
|
||||
})
|
||||
.then(() => undefined)
|
||||
backgroundWslSessionBridgeTasks.set(taskKey, task)
|
||||
void task.finally(() => {
|
||||
if (backgroundWslSessionBridgeTasks.get(taskKey) === task) {
|
||||
backgroundWslSessionBridgeTasks.delete(taskKey)
|
||||
}
|
||||
})
|
||||
return task
|
||||
}
|
||||
|
||||
export async function syncWslCodexSessionsIntoManagedHome(
|
||||
target: WslCodexSessionBridgeTarget
|
||||
): Promise<WslCodexSessionBridgeSummary> {
|
||||
const paths = resolveWslCodexSessionBridgeLinuxPaths(target)
|
||||
if (!paths) {
|
||||
return emptySummary
|
||||
}
|
||||
|
||||
const stdout = await execFileUtf8('wsl.exe', [
|
||||
'-d',
|
||||
target.distro,
|
||||
'--',
|
||||
'bash',
|
||||
'-lc',
|
||||
buildWslCodexSessionBridgeShellCommand(paths)
|
||||
])
|
||||
return parseWslSessionBridgeSummary(stdout)
|
||||
}
|
||||
|
||||
export function resolveWslCodexSessionBridgeLinuxPaths(
|
||||
target: WslCodexSessionBridgeTarget
|
||||
): WslCodexSessionBridgeLinuxPaths | null {
|
||||
const systemHomePath = getLinuxPathForWslDistro(target.systemCodexHomePath, target.distro)
|
||||
const managedHomePath = getLinuxPathForWslDistro(target.managedCodexHomePath, target.distro)
|
||||
if (!systemHomePath || !managedHomePath) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
systemSessionsRoot: joinLinuxPath(systemHomePath, 'sessions'),
|
||||
managedSessionsRoot: joinLinuxPath(managedHomePath, 'sessions')
|
||||
}
|
||||
}
|
||||
|
||||
export function buildWslCodexSessionBridgeShellCommand(
|
||||
paths: WslCodexSessionBridgeLinuxPaths
|
||||
): string {
|
||||
const shellCommand = [
|
||||
'set -u',
|
||||
`source_sessions_root=${quoteBashString(paths.systemSessionsRoot)}`,
|
||||
`managed_sessions_root=${quoteBashString(paths.managedSessionsRoot)}`,
|
||||
'scanned_files=0',
|
||||
'linked_files=0',
|
||||
'if [ ! -d "$source_sessions_root" ]; then',
|
||||
` printf '{"scannedFiles":0,"linkedFiles":0}\\n'`,
|
||||
' exit 0',
|
||||
'fi',
|
||||
"while IFS= read -r -d '' source_file; do",
|
||||
' scanned_files=$((scanned_files + 1))',
|
||||
' relative_path=${source_file#"$source_sessions_root"/}',
|
||||
' target_file="$managed_sessions_root/$relative_path"',
|
||||
' if [ -e "$target_file" ] || [ -L "$target_file" ]; then',
|
||||
' continue',
|
||||
' fi',
|
||||
' target_dir=${target_file%/*}',
|
||||
' mkdir -p -- "$target_dir" || continue',
|
||||
// Why: Codex resume ignores symlinked JSONL, so WSL links must be
|
||||
// Linux hardlinks created inside the distro filesystem.
|
||||
' if ln -- "$source_file" "$target_file"; then',
|
||||
' linked_files=$((linked_files + 1))',
|
||||
' fi',
|
||||
`done < <(find "$source_sessions_root" -type f -name '*.jsonl' -print0 2>/dev/null)`,
|
||||
`printf '{"scannedFiles":%s,"linkedFiles":%s}\\n' "$scanned_files" "$linked_files"`
|
||||
].join('\n')
|
||||
return escapeWslShCommandForWindows(shellCommand)
|
||||
}
|
||||
|
||||
function getWslSessionBridgeTaskKey(target: WslCodexSessionBridgeTarget): string {
|
||||
return [target.distro, target.systemCodexHomePath, target.managedCodexHomePath].join('\0')
|
||||
}
|
||||
|
||||
function getLinuxPathForWslDistro(path: string, distro: string): string | null {
|
||||
const wslPath = parseWslUncPath(path)
|
||||
if (wslPath) {
|
||||
return wslDistroNamesMatch(wslPath.distro, distro) ? wslPath.linuxPath : null
|
||||
}
|
||||
return path.startsWith('/') ? path : null
|
||||
}
|
||||
|
||||
function wslDistroNamesMatch(left: string, right: string): boolean {
|
||||
return left.toLowerCase() === right.toLowerCase()
|
||||
}
|
||||
|
||||
function joinLinuxPath(basePath: string, ...segments: string[]): string {
|
||||
return pathPosix.join(basePath, ...segments)
|
||||
}
|
||||
|
||||
function quoteBashString(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`
|
||||
}
|
||||
|
||||
function execFileUtf8(command: string, args: string[]): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
command,
|
||||
args,
|
||||
{
|
||||
encoding: 'utf-8',
|
||||
maxBuffer: 1024 * 1024,
|
||||
timeout: WSL_SESSION_BRIDGE_TIMEOUT_MS,
|
||||
windowsHide: true
|
||||
},
|
||||
(error, stdout) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(stdout)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function parseWslSessionBridgeSummary(stdout: string): WslCodexSessionBridgeSummary {
|
||||
try {
|
||||
// Why: login/profile scripts may write stdout before the bridge summary.
|
||||
const summaryLine =
|
||||
stdout
|
||||
.split(/\r?\n/)
|
||||
.findLast((line) => line.trim().length > 0)
|
||||
?.trim() ?? ''
|
||||
const parsed: unknown = JSON.parse(summaryLine)
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return emptySummary
|
||||
}
|
||||
const summary = parsed as Record<string, unknown>
|
||||
if (typeof summary.scannedFiles !== 'number' || typeof summary.linkedFiles !== 'number') {
|
||||
return emptySummary
|
||||
}
|
||||
return {
|
||||
scannedFiles: summary.scannedFiles,
|
||||
linkedFiles: summary.linkedFiles
|
||||
}
|
||||
} catch {
|
||||
return emptySummary
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue