From 4dd5329be7e53ae2b04e74baee6db373756f0948 Mon Sep 17 00:00:00 2001 From: Neil Date: Thu, 8 Oct 2026 02:59:04 -0700 Subject: [PATCH 1/2] fix(orcad): provide execution host CLI in Unix managed terminals --- config/scripts/build-orcad.mjs | 11 +++ config/scripts/orcad-entry-build.mjs | 15 +++ .../scripts/runtime-serve-terminal-smoke.mjs | 16 ++++ ...cli-command-privileged-transaction.test.ts | 4 +- src/main/cli/cli-dev-launcher.ts | 15 ++- src/main/cli/orca-cli-child-path.test.ts | 32 ++++++- src/main/cli/orca-cli-child-path.ts | 17 +++- src/main/orcad/orcad-cli-launcher.test.ts | 96 +++++++++++++++++++ src/main/orcad/orcad-cli-launcher.ts | 36 +++++++ src/main/orcad/orcad-entry.ts | 3 + ...uctured-session-child-identity-env.test.ts | 16 ++++ src/shared/app-environment.ts | 2 + src/shared/orcad-artifacts.ts | 4 + tests/e2e/ssh-orcad-auto-convert.spec.ts | 41 ++++++++ 14 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 src/main/orcad/orcad-cli-launcher.test.ts create mode 100644 src/main/orcad/orcad-cli-launcher.ts diff --git a/config/scripts/build-orcad.mjs b/config/scripts/build-orcad.mjs index 5d6d556bb5ec..5099a95994d7 100644 --- a/config/scripts/build-orcad.mjs +++ b/config/scripts/build-orcad.mjs @@ -4,6 +4,7 @@ import { fork, spawnSync } from 'node:child_process' import { build } from 'esbuild' import { buildOrcadEntry, + buildOrcadCli, buildOrcadLauncher, externalNativeAddons, ORCAD_EXTERNAL_MODULES, @@ -18,6 +19,7 @@ import { existsSync, mkdirSync, mkdtempSync, + readFileSync, rmSync, writeFileSync } from 'node:fs' @@ -31,6 +33,8 @@ import { materializeWatcherPackage } from './orcad-watcher-package.mjs' import { stageOrcadWindowsProcessTree } from './orcad-windows-process-tree.mjs' import { ORCAD_EMOJI_SHORTCODE_DATASET, + ORCAD_CLI_ENTRY_FILENAME, + ORCAD_CLI_PACKAGE_FILENAME, ORCAD_LAUNCHER_FILENAME, ORCAD_SERVER_ENTRY_FILENAME, ORCAD_NODE_PTY_DIR, @@ -216,6 +220,12 @@ const childResults = await Promise.all( ) const result = await buildOrcadEntry(SERVER_OUT_FILE) +const cliResult = await buildOrcadCli(join(OUT_DIR, ORCAD_CLI_ENTRY_FILENAME)) +const { version: cliVersion } = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')) +writeFileSync( + join(OUT_DIR, ORCAD_CLI_PACKAGE_FILENAME), + `${JSON.stringify({ type: 'commonjs', private: true, version: cliVersion })}\n` +) const launcherResult = await buildOrcadLauncher(OUT_FILE) const output = Object.values(result.metafile.outputs).find( @@ -244,6 +254,7 @@ function collectImporters(metafiles, matches) { const metafiles = [ launcherResult.metafile, result.metafile, + cliResult.metafile, ...childResults.map((child) => child.metafile) ] const electronImporters = collectImporters( diff --git a/config/scripts/orcad-entry-build.mjs b/config/scripts/orcad-entry-build.mjs index 1267ef9664a1..1f0de9a11b23 100644 --- a/config/scripts/orcad-entry-build.mjs +++ b/config/scripts/orcad-entry-build.mjs @@ -61,6 +61,21 @@ export function buildOrcadEntry(outfile) { }) } +export function buildOrcadCli(outfile) { + return build({ + entryPoints: [join(root, 'src/cli/index.ts')], + bundle: true, + platform: 'node', + target: 'node18', + format: 'cjs', + outfile, + metafile: true, + minify: true, + sourcemap: false, + logLevel: 'error' + }) +} + export function buildOrcadLauncher(outfile) { const serverSha256 = createHash('sha256') .update(readFileSync(join(dirname(outfile), ORCAD_SERVER_ENTRY_FILENAME))) diff --git a/config/scripts/runtime-serve-terminal-smoke.mjs b/config/scripts/runtime-serve-terminal-smoke.mjs index 0c43f7d60c1e..10248cb965e3 100644 --- a/config/scripts/runtime-serve-terminal-smoke.mjs +++ b/config/scripts/runtime-serve-terminal-smoke.mjs @@ -351,6 +351,22 @@ async function main() { ) } log('terminal round trip OK') + if (launch.env.ORCA_USER_DATA && process.platform !== 'win32') { + const cliNonce = randomBytes(8).toString('hex') + orca(pairingCode, [ + 'terminal', + 'send', + '--terminal', + terminal.handle, + '--text', + `orca status && orca worktree ps --json && printf 'ORCA_CLI_%s\\n' '${cliNonce}'`, + '--enter' + ]) + if (!(await waitForNonce(pairingCode, terminal.handle, `ORCA_CLI_${cliNonce}`))) { + throw new Error('The managed terminal could not run the execution host CLI') + } + log('execution host CLI round trip OK') + } } catch (error) { fail(error instanceof Error ? error.message : String(error)) } finally { diff --git a/src/main/cli/cli-command-privileged-transaction.test.ts b/src/main/cli/cli-command-privileged-transaction.test.ts index 63349cb70c9f..2238ffc8f06a 100644 --- a/src/main/cli/cli-command-privileged-transaction.test.ts +++ b/src/main/cli/cli-command-privileged-transaction.test.ts @@ -25,7 +25,7 @@ vi.mock('electron', () => ({ })) import { CliInstaller } from './cli-installer' -import { buildUnixDevLauncher } from './cli-dev-launcher' +import { buildUnixCliLauncher } from './cli-dev-launcher' const createdRoots: string[] = [] const protectedDirectories: string[] = [] @@ -140,7 +140,7 @@ describe.skipIf(process.platform !== 'darwin' || process.getuid?.() === 0)( const oldCliPath = join(fixture.root, 'old', 'out', 'cli', 'index.js') await writeFile( fixture.commandPath, - buildUnixDevLauncher('/Applications/Old.app/Contents/MacOS/Orca', oldCliPath, 'user-data') + buildUnixCliLauncher('/Applications/Old.app/Contents/MacOS/Orca', oldCliPath, 'user-data') ) const foreignContent = 'foreign command written into the inspected inode' let raced = false diff --git a/src/main/cli/cli-dev-launcher.ts b/src/main/cli/cli-dev-launcher.ts index 62884b2e0273..e7b0f75c4b55 100644 --- a/src/main/cli/cli-dev-launcher.ts +++ b/src/main/cli/cli-dev-launcher.ts @@ -34,7 +34,7 @@ export async function ensureDevLauncher(args: { const content = args.platform === 'win32' ? buildWindowsDevLauncher(args.execPath, args.cliEntryPath, args.userDataPath) - : buildUnixDevLauncher(args.execPath, args.cliEntryPath, args.userDataPath) + : buildUnixCliLauncher(args.execPath, args.cliEntryPath, args.userDataPath) await writeFile(launcherPath, content, { encoding: 'utf8', mode: args.platform === 'win32' ? undefined : 0o755 @@ -49,11 +49,20 @@ export async function ensureDevLauncher(args: { return launcherPath } -export function buildUnixDevLauncher( +export function buildUnixCliLauncher( execPathValue: string, cliEntryPath: string, - userDataPath: string + userDataPath: string, + runtime: 'electron' | 'node' = 'electron' ): string { + if (runtime === 'node') { + return `#!/usr/bin/env sh +set -eu +export ORCA_USER_DATA_PATH=${quoteShell(userDataPath)} +unset NODE_OPTIONS NODE_REPL_EXTERNAL_MODULE ELECTRON_RUN_AS_NODE +exec ${quoteShell(execPathValue)} ${quoteShell(cliEntryPath)} "$@" +` + } return `#!/usr/bin/env bash set -euo pipefail ELECTRON=${quoteShell(execPathValue)} diff --git a/src/main/cli/orca-cli-child-path.test.ts b/src/main/cli/orca-cli-child-path.test.ts index 7440b1a9ad82..ba9e8c6b7127 100644 --- a/src/main/cli/orca-cli-child-path.test.ts +++ b/src/main/cli/orca-cli-child-path.test.ts @@ -1,5 +1,6 @@ import { join } from 'node:path' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { installFakeAppEnvironment } from '../../../config/scripts/vitest-host-ports-setup' const shim = vi.hoisted(() => ({ ensureLinuxTerminalOrcaCliShimDir: vi.fn() })) vi.mock('./linux-terminal-orca-cli-shim', () => shim) @@ -11,11 +12,40 @@ const RESOURCES = '/app/Resources' const SHIM_DIR = join(USER_DATA, 'linux-orca-cli-shim') beforeEach(() => { + installFakeAppEnvironment() shim.ensureLinuxTerminalOrcaCliShimDir.mockReset() shim.ensureLinuxTerminalOrcaCliShimDir.mockReturnValue(SHIM_DIR) }) +afterEach(() => installFakeAppEnvironment()) + describe('prependOrcaCliDirToChildPath', () => { + it.each(['linux', 'darwin'] as const)( + 'uses the %s execution host CLI and profile over inherited values', + (platform) => { + const launcher = join(USER_DATA, 'cli', 'bin', 'orca') + installFakeAppEnvironment({ getCliLauncherPath: () => launcher }) + const env: Record = { + PATH: `/usr/bin:${join(USER_DATA, 'cli', 'bin')}::/bin`, + ORCA_USER_DATA_PATH: '/parent/profile', + ORCA_CLI_COMMAND: '/parent/orca', + ORCA_CLI_BIN_DIR: '/parent/bin' + } + expect( + prependOrcaCliDirToChildPath(env, { + isPackaged: true, + userDataPath: USER_DATA, + resourcesPath: null, + platform + }) + ).toBe(launcher) + expect(env.PATH).toBe(`${join(USER_DATA, 'cli', 'bin')}:/usr/bin:/bin`) + expect(env.ORCA_USER_DATA_PATH).toBe(USER_DATA) + expect(env.ORCA_CLI_COMMAND).toBe(launcher) + expect(env.ORCA_CLI_BIN_DIR).toBe(join(USER_DATA, 'cli', 'bin')) + expect(shim.ensureLinuxTerminalOrcaCliShimDir).not.toHaveBeenCalled() + } + ) it('leads packaged Linux PATH with the bare-orca shim dir', () => { // Why this matters at all: the Linux CLI installs as `orca-ide` so it never claims GNOME // Orca's /usr/bin/orca screen reader, so bare `orca` only works through this shim. diff --git a/src/main/cli/orca-cli-child-path.ts b/src/main/cli/orca-cli-child-path.ts index a15943827cc4..270f7bb411d3 100644 --- a/src/main/cli/orca-cli-child-path.ts +++ b/src/main/cli/orca-cli-child-path.ts @@ -13,7 +13,8 @@ * every branch behaves exactly as it did inside `buildPtyHostEnv`. */ -import { delimiter, join } from 'node:path' +import { delimiter, dirname, join } from 'node:path' +import { getAppEnvironment, hasAppEnvironment } from '../../shared/app-environment' import { readInheritedPath } from '../ipc/pty/host-env/path' import { resolvePathEnvKey } from '../pty/windows-environment-path' import { ensureLinuxTerminalOrcaCliShimDir } from './linux-terminal-orca-cli-shim' @@ -43,6 +44,20 @@ export function prependOrcaCliDirToChildPath( // Why: matches node:path's `delimiter` for the running platform, but stays correct when a test // drives a foreign platform through the seam. const pathDelimiter = platform === 'win32' ? ';' : delimiter + const hostLauncher = hasAppEnvironment() ? getAppEnvironment().getCliLauncherPath?.() : null + if (hostLauncher) { + const binDir = dirname(hostLauncher) + const inheritedEntries = readInheritedPath(env, platform) + .split(pathDelimiter) + .filter((entry) => entry.length > 0 && entry !== binDir) + env[resolvePathEnvKey(env, platform)] = [binDir, ...inheritedEntries].join(pathDelimiter) + if (platform !== 'win32') { + env.ORCA_CLI_BIN_DIR = binDir + } + env.ORCA_USER_DATA_PATH = opts.userDataPath + env.ORCA_CLI_COMMAND = hostLauncher + return hostLauncher + } // Why: dev mode needs the launcher PATH override so `orca` resolves to the dev build instead of the production binary at /usr/local/bin/orca. if (!opts.isPackaged) { const devCliBin = join(opts.userDataPath, 'cli', 'bin') diff --git a/src/main/orcad/orcad-cli-launcher.test.ts b/src/main/orcad/orcad-cli-launcher.test.ts new file mode 100644 index 000000000000..00c4c9e5b35b --- /dev/null +++ b/src/main/orcad/orcad-cli-launcher.test.ts @@ -0,0 +1,96 @@ +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { runProcess } from '../../shared/child-process/run-process' +import { ORCAD_CLI_ENTRY_FILENAME } from '../../shared/orcad-artifacts' +import { getOrcadCliLauncherPath, prepareOrcadCliLauncher } from './orcad-cli-launcher' + +const roots = vi.hoisted(() => ({ install: '', profile: '' })) +vi.mock('./orcad-app-paths', () => ({ + resolveOrcadInstallRoot: () => roots.install, + resolveUserDataPath: () => roots.profile +})) + +let directory = '' +const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform')! + +beforeEach(async () => { + directory = await mkdtemp(join(tmpdir(), 'orca-node-cli-')) + roots.install = join(directory, "server ' slot") + roots.profile = join(directory, "profile ' data") +}) + +afterEach(async () => { + Object.defineProperty(process, 'platform', platformDescriptor) + await rm(directory, { recursive: true, force: true }) +}) + +async function writeCli(): Promise { + const path = join(roots.install, ...ORCAD_CLI_ENTRY_FILENAME.split('/')) + await mkdir(dirname(path), { recursive: true }) + await writeFile( + path, + 'console.log(JSON.stringify({profile:process.env.ORCA_USER_DATA_PATH, args:process.argv.slice(2), electron:process.env.ELECTRON_RUN_AS_NODE}))' + ) +} + +describe('orcad profile CLI launcher', () => { + it.skipIf(process.platform === 'win32')( + 'launches pinned Node with intact arguments and the execution host profile', + async () => { + await writeCli() + await prepareOrcadCliLauncher() + const launcher = getOrcadCliLauncherPath() + expect(launcher).toBe(join(roots.profile, 'cli', 'bin', 'orca')) + if (!launcher) { + throw new Error('CLI launcher is missing') + } + const args = ['orchestration', 'send', 'a body\nwith "quotes" and $HOME'] + const result = await runProcess({ + program: launcher, + args, + env: { + ...process.env, + ORCA_USER_DATA_PATH: '/another/owner', + ELECTRON_RUN_AS_NODE: '1', + NODE_OPTIONS: '--require /missing/inherited/preload' + }, + timeoutMs: 5000 + }) + expect(result.code).toBe(0) + expect(JSON.parse(result.stdout)).toEqual({ profile: roots.profile, args }) + } + ) + + it.skipIf(process.platform === 'win32')( + 'refreshes the launcher when the host switches server slots', + async () => { + await writeCli() + await prepareOrcadCliLauncher() + roots.install = join(directory, 'updated server') + await writeCli() + await prepareOrcadCliLauncher() + const launcher = getOrcadCliLauncherPath() + if (!launcher) { + throw new Error('CLI launcher is missing') + } + expect(await readFile(launcher, 'utf8')).toContain(roots.install) + } + ) + + it('leaves old slots without a CLI usable and clears a previous launcher', async () => { + await writeCli() + await prepareOrcadCliLauncher() + roots.install = join(directory, 'old server') + await prepareOrcadCliLauncher() + expect(getOrcadCliLauncherPath()).toBeNull() + }) + + it('does not create a cmd.exe message proxy on Windows', async () => { + Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) + await writeCli() + await prepareOrcadCliLauncher() + expect(getOrcadCliLauncherPath()).toBeNull() + }) +}) diff --git a/src/main/orcad/orcad-cli-launcher.ts b/src/main/orcad/orcad-cli-launcher.ts new file mode 100644 index 000000000000..01660164e0a2 --- /dev/null +++ b/src/main/orcad/orcad-cli-launcher.ts @@ -0,0 +1,36 @@ +import { chmod, mkdir, readFile } from 'node:fs/promises' +import { existsSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { ORCAD_CLI_ENTRY_FILENAME } from '../../shared/orcad-artifacts' +import { buildUnixCliLauncher } from '../cli/cli-dev-launcher' +import { writeFileAtomically } from '../codex-accounts/fs-utils' +import { resolveOrcadInstallRoot, resolveUserDataPath } from './orcad-app-paths' + +let launcherPath: string | null = null + +export function getOrcadCliLauncherPath(): string | null { + return launcherPath +} + +export async function prepareOrcadCliLauncher(): Promise { + launcherPath = null + // Windows needs a native argv-preserving launcher; never proxy message bodies through cmd.exe. + if (process.platform === 'win32') { + return + } + const entry = join(resolveOrcadInstallRoot(), ...ORCAD_CLI_ENTRY_FILENAME.split('/')) + // Older server slots and source-only runs may not include the CLI yet. + if (!existsSync(entry)) { + return + } + const userDataPath = resolveUserDataPath() + const path = join(userDataPath, 'cli', 'bin', 'orca') + const script = buildUnixCliLauncher(process.execPath, entry, userDataPath, 'node') + await mkdir(dirname(path), { recursive: true }) + const current = await readFile(path, 'utf8').catch(() => null) + if (current !== script) { + writeFileAtomically(path, script, { mode: 0o700 }) + } + await chmod(path, 0o700) + launcherPath = path +} diff --git a/src/main/orcad/orcad-entry.ts b/src/main/orcad/orcad-entry.ts index 55f6ace305f2..55feca1733ed 100644 --- a/src/main/orcad/orcad-entry.ts +++ b/src/main/orcad/orcad-entry.ts @@ -15,6 +15,7 @@ import { setAppEnvironment, type AppEnvironment } from '../../shared/app-environ import { setSecretStore, type SecretStore } from '../../shared/secret-store' import type { ServeReadiness } from '../server/serve-readiness' import { resolveOrcadInstallRoot, resolveOrcadPath, resolveUserDataPath } from './orcad-app-paths' +import { getOrcadCliLauncherPath, prepareOrcadCliLauncher } from './orcad-cli-launcher' import { describeOrcadBindExposure, resolveOrcadBindHost } from './orcad-bind-address' import { flushOrcadProfileStoreForShutdown, @@ -66,6 +67,7 @@ function createNodeAppEnvironment(): AppEnvironment { // posture. Layout questions must ask whether the app root is an asar archive // instead (see parcel-watcher-entry-path.ts). isPackaged: () => true, + getCliLauncherPath: getOrcadCliLauncherPath, onWillQuit: (handler) => quitHandlers.push(handler), exit: (code = 0) => process.exit(code), // Why []: there are no Chromium processes on this host to measure. @@ -199,6 +201,7 @@ async function startOrcadRuntime( const { resolvePushGatewayOrigin } = await import('../runtime/push/push-gateway-origin') const runtimeUserDataPath = getAppEnvironment().getPath('userData') + await prepareOrcadCliLauncher() const idleExitStartup = beginOrcadIdleExit(runtimeUserDataPath) const { store: profileStore, authority: profileStateAuthority } = await createOrcadProfileStateStartup(runtimeUserDataPath) diff --git a/src/main/runtime/structured-session-child-identity-env.test.ts b/src/main/runtime/structured-session-child-identity-env.test.ts index 095aad2a8a5c..6d773fc08527 100644 --- a/src/main/runtime/structured-session-child-identity-env.test.ts +++ b/src/main/runtime/structured-session-child-identity-env.test.ts @@ -100,6 +100,22 @@ describe('structuredSessionChildIdentityEnv', () => { }) describe.each(['chat', 'worker'] as const)("reaches this app's CLI as a %s", (kind) => { + it('reaches a headless host CLI without Electron resources', () => { + const launcher = join(USER_DATA, 'cli', 'bin', 'orca') + installFakeAppEnvironment({ + isPackaged: () => true, + getPath: () => USER_DATA, + getCliLauncherPath: () => launcher + }) + const env = structuredSessionChildIdentityEnv(SESSION_ID, { + PATH: '/usr/bin', + ORCA_USER_DATA_PATH: '/other/profile' + }) + expect(env.ORCA_CLI_COMMAND).toBe(launcher) + expect(env.ORCA_USER_DATA_PATH).toBe(USER_DATA) + expect(env.ORCA_AGENT_SESSION_ID).toBe(SESSION_ID) + expect(env.PATH).toBe(`${join(USER_DATA, 'cli', 'bin')}:/usr/bin`) + }) beforeEach(() => { if (kind === 'worker') { registerWorker() diff --git a/src/shared/app-environment.ts b/src/shared/app-environment.ts index 86aca4f8182d..bbb194e20ff6 100644 --- a/src/shared/app-environment.ts +++ b/src/shared/app-environment.ts @@ -24,6 +24,8 @@ export type AppEnvironment = { getAppPath(): string getVersion(): string isPackaged(): boolean + /** A headless host's profile-scoped CLI, when it has no Electron resources directory. */ + getCliLauncherPath?(): string | null /** Shutdown hook: electron `will-quit`, or SIGTERM/SIGINT on a Node host. */ onWillQuit(handler: () => void): void exit(code?: number): void diff --git a/src/shared/orcad-artifacts.ts b/src/shared/orcad-artifacts.ts index b245232410d4..4e34b4f34dc4 100644 --- a/src/shared/orcad-artifacts.ts +++ b/src/shared/orcad-artifacts.ts @@ -59,6 +59,8 @@ export function orcadNodeRuntimeRelativePath(target: string, executableSha256: s export const ORCAD_ADDON_NAPI_VERSION = 8 export const ORCAD_NODE_PTY_DIR = 'node_modules/node-pty' +export const ORCAD_CLI_ENTRY_FILENAME = 'out/cli/index.js' +export const ORCAD_CLI_PACKAGE_FILENAME = 'out/package.json' // Test files and sources stay out; these are every module the runtime path requires. export const ORCAD_NODE_PTY_JS_ARTIFACTS = [ 'package.json', @@ -161,6 +163,8 @@ export type OrcadArtifact = { } export const ORCAD_ARTIFACTS: readonly OrcadArtifact[] = [ + { filename: ORCAD_CLI_ENTRY_FILENAME }, + { filename: ORCAD_CLI_PACKAGE_FILENAME }, { filename: ORCAD_LAUNCHER_FILENAME }, { filename: ORCAD_SERVER_ENTRY_FILENAME }, // Forked so a native @parcel/watcher fault kills the child, not the server. diff --git a/tests/e2e/ssh-orcad-auto-convert.spec.ts b/tests/e2e/ssh-orcad-auto-convert.spec.ts index 7f9f7f0c3f27..baf5274f551f 100644 --- a/tests/e2e/ssh-orcad-auto-convert.spec.ts +++ b/tests/e2e/ssh-orcad-auto-convert.spec.ts @@ -46,6 +46,12 @@ import { startOrcadConvertHost, type OrcadConvertHost } from './helpers/orcad-convert-host' +import { + callEnvironment, + createPairedHostTerminal, + openPairedClientTab +} from './helpers/paired-host-terminal' +import { expectTerminalAccessibilityText } from './helpers/terminal-accessibility-tree' import { toSshExecutionHostId } from '../../src/shared/execution-host' const HOST = process.env[ORCAD_CONVERT_HOST_ENV] @@ -244,6 +250,41 @@ test('a relay-era profile converts its SSH host on the first connect after upgra app = upgraded.app await waitForSessionReady(upgraded.page) await convertAndRetain(upgraded.page, session.userDataDir, seeded) + if (!host.exec) { + return + } + const environment = ( + await upgraded.page.evaluate(() => window.api.runtimeEnvironments.list()) + ).find((entry) => entry.orcadDeployment?.sshTargetId === seeded.targetId) + if (!environment) { + throw new Error('Managed server is not registered') + } + await upgraded.page.evaluate( + ({ worktreeId, environmentId }) => { + window.__store!.getState().setActiveWorktree(worktreeId, `runtime:${environmentId}`) + }, + { worktreeId: seeded.worktreeId, environmentId: environment.id } + ) + const terminal = await createPairedHostTerminal( + upgraded.page, + environment.id, + seeded.worktreeId, + 'bash' + ) + await openPairedClientTab(upgraded.page, seeded.worktreeId, terminal.webTabId) + await callEnvironment(upgraded.page, environment.id, 'terminal.send', { + terminal: terminal.terminal, + text: "orca status --json > /tmp/orca-cli-status.json && orca worktree ps --json > /tmp/orca-cli-workers.json && printf 'QA_%s\\n' 'CLI_READY'", + enter: true + }) + await expectTerminalAccessibilityText(upgraded.page, terminal.webTabId, 'QA_CLI_READY') + const status = JSON.parse(host.exec('cat /tmp/orca-cli-status.json')) + expect(status.result.runtime).toMatchObject({ + reachable: true, + runtimeId: environment.runtimeId + }) + expect(status.result.app.desktopWindowStatus).toBe('blocked') + await upgraded.page.screenshot({ path: testInfo.outputPath('managed-cli-ready.png') }) } finally { if (app) { await session.close(app) From 91f12d06cea284cf004b614d0b785dc1d7663d51 Mon Sep 17 00:00:00 2001 From: m4air Date: Thu, 8 Oct 2026 16:51:56 -0700 Subject: [PATCH 2/2] fix(orcad): keep the server starting when the CLI launcher cannot be written --- src/main/orcad/orcad-entry.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/orcad/orcad-entry.ts b/src/main/orcad/orcad-entry.ts index 55feca1733ed..7bbb6ed10336 100644 --- a/src/main/orcad/orcad-entry.ts +++ b/src/main/orcad/orcad-entry.ts @@ -201,7 +201,10 @@ async function startOrcadRuntime( const { resolvePushGatewayOrigin } = await import('../runtime/push/push-gateway-origin') const runtimeUserDataPath = getAppEnvironment().getPath('userData') - await prepareOrcadCliLauncher() + // A missing `orca` command must never keep the server from starting. + await prepareOrcadCliLauncher().catch((error: unknown) => { + console.warn('[orcad] Could not prepare the profile CLI launcher', error) + }) const idleExitStartup = beginOrcadIdleExit(runtimeUserDataPath) const { store: profileStore, authority: profileStateAuthority } = await createOrcadProfileStateStartup(runtimeUserDataPath)