fix(orcad): show help before server startup

Make orcad --help print usage and exit before native preflight or server startup, preserving existing flag parsing and managed-runtime startup. Verified against the built server and launcher.

Adapted from #26621 by @espetro.

Co-authored-by: Joaquin Terrasa <josocjoq+dev@pm.me>
This commit is contained in:
Neil
2026-10-09 11:33:44 -07:00
committed by GitHub
co-authored by Joaquin Terrasa
parent 65580c5315
commit d2d17a3e47
6 changed files with 184 additions and 9 deletions
+34 -4
View File
@@ -13,11 +13,14 @@ import {
* catchable, so a preflight that lands after `main()` has already reached
* `await import('../ipc/pty')` prevents nothing.
*/
const { order, profileProbe } = vi.hoisted(() => {
const { order, profileProbe, reserveStdout } = vi.hoisted(() => {
const order: string[] = []
return { order, profileProbe: vi.fn(async () => {}) }
return { order, profileProbe: vi.fn(async () => {}), reserveStdout: vi.fn() }
})
vi.mock('../server/serve-stdout-boundary', () => ({
reserveServeStdoutForReadiness: reserveStdout
}))
vi.mock('./orcad-bundled-runtime', () => ({ assertOrcadServerRuntime: () => {} }))
vi.mock('./orcad-profile-preflight', () => ({
preflightBundledOrcadStartup: async () => {
@@ -49,12 +52,39 @@ vi.mock('./orcad-managed-stop-command', () => ({
}))
vi.mock('./orcad-entry', () => ({
main: async () => {
main: vi.fn(async () => {
order.push('main')
}
})
}))
describe('orcad entry', () => {
it.each([['--help'], ['-h'], ['--json', '--help'], ['--bind', '--help', '-h']])(
'handles help before startup probes or stdout redirection: %j',
async (...argv) => {
vi.spyOn(process, 'argv', 'get').mockReturnValue(['runtime', 'orcad.js', ...argv])
await import('./main')
const { main } = await import('./orcad-entry')
expect(main).toHaveBeenCalledWith(argv)
expect(order).toEqual(['main'])
expect(profileProbe).not.toHaveBeenCalled()
expect(reserveStdout).not.toHaveBeenCalled()
}
)
it.each([
['--bind', '--help'],
['--pairing-address', '-h'],
['--project-root', '--help']
])('keeps startup probes when help is consumed as a value: %j', async (...argv) => {
vi.spyOn(process, 'argv', 'get').mockReturnValue(['runtime', 'orcad.js', ...argv])
await import('./main')
await vi.waitFor(() => expect(order).toContain('main'))
expect(order).toEqual(['profile-admission', 'preflight', 'main'])
expect(reserveStdout).toHaveBeenCalledOnce()
})
it.each([
{ flag: ORCAD_PROFILE_PREFLIGHT_FLAG, nativeFeatures: true },
{ flag: ORCAD_STARTUP_PREFLIGHT_FLAG, nativeFeatures: false }
+6
View File
@@ -23,6 +23,7 @@ import {
WINDOWS_BREAKAWAY_LAUNCH_FLAG
} from '../../shared/windows-breakaway-launch'
import { runWindowsBreakawayLaunchIfRequested } from '../../shared/windows-breakaway-launcher'
import { parseArgs } from './orcad-command-arguments'
// Why exit before the preflight: reaching this line means the whole module graph resolved
// under plain Node, which is all the build guard needs to prove. Probing natives or
@@ -56,6 +57,11 @@ if (process.argv[2] === WINDOWS_BREAKAWAY_LAUNCH_FLAG) {
function startOrcadProcess(): void {
try {
const argv = process.argv.slice(2)
if ((argv.includes('--help') || argv.includes('-h')) && parseArgs(argv).help) {
void main(argv).catch(failStartup)
return
}
assertOrcadServerRuntime()
const flag = process.argv[2]
if (flag === ORCAD_NATIVE_PREFLIGHT_FLAG && process.argv.length === 3) {
+33 -3
View File
@@ -1,15 +1,45 @@
import type { OrcadOptions } from './orcad-entry'
import { DEFAULT_WS_PORT } from '../runtime/runtime-rpc/runtime-rpc-pairing-types'
import { ORCAD_LOOPBACK_BIND_HOST } from './orcad-bind-address'
export const ORCAD_USAGE = `orcad — the Orca runtime served without Electron.
Usage: orcad [options]
Options:
--port <n> WebSocket port, 0-65535; 0 picks a free port.
Defaults to ${DEFAULT_WS_PORT}, with a fallback if unavailable.
--bind <ip> Literal IP to bind (default ${ORCAD_LOOPBACK_BIND_HOST}, local only).
Use an SSH port-forward or a network IP for remote access.
--pairing-address <addr> Address advertised in pairing offers when different
from the bind address (for example, a public hostname).
--json Print the one-line server readiness JSON to stdout.
--no-pairing Do not create a client pairing offer at startup.
--mobile-pairing Create a mobile-scoped pairing offer with a QR code.
--grant-desktop-control Allow desktop control in the default runtime offer;
cannot combine with --no-pairing or --mobile-pairing.
--recipe-json Print only the VM recipe line; requires --project-root.
--project-root <dir> Existing absolute project directory for --recipe-json.
--help, -h Print this text and exit without starting the server.
Environment:
ORCA_USER_DATA Data directory for this instance's profile and state.
`
type OrcadCommandArguments = OrcadOptions & { help?: boolean }
/**
* orcad's flags. A value-taking flag consumes the next token whatever it looks
* like, so `--bind --json` binds to the literal `--json`; only a missing token
* is an error. Pinned by orcad-launch-contract.test.ts.
*/
export function parseArgs(argv: string[]): OrcadOptions {
const options: OrcadOptions = {}
export function parseArgs(argv: string[]): OrcadCommandArguments {
const options: OrcadCommandArguments = {}
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i]
if (arg === '--port') {
if (arg === '--help' || arg === '-h') {
options.help = true
} else if (arg === '--port') {
const raw = argv[i + 1]
const port = Number(raw)
if (!Number.isInteger(port) || port < 0 || port > 65535) {
+7 -2
View File
@@ -26,7 +26,7 @@ import {
installOrcadShutdownSignals,
startOrcadWithHost
} from './orcad-lifecycle'
import { parseArgs } from './orcad-command-arguments'
import { ORCAD_USAGE, parseArgs } from './orcad-command-arguments'
import type { OrcadRuntimeCleanup } from './orcad-runtime-lifetime'
import { installOrcadStopRequestListeners } from './orcad-stop-request-listener'
import { prepareOrcadManagedStop } from './orcad-managed-stop-admission'
@@ -399,7 +399,12 @@ async function startOrcadRuntime(
}
export async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
const startup = startOrcad(parseArgs(argv))
const options = parseArgs(argv)
if (options.help) {
console.log(ORCAD_USAGE)
return
}
const startup = startOrcad(options)
const requestShutdown = installOrcadShutdownSignals(async () => (await startup).stop())
const handle = await startup
// Why after startup: a managed request must name the runtime and instance this run became.
@@ -0,0 +1,73 @@
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { runProcess } from '../../shared/child-process/run-process'
import { ORCAD_LAUNCHER_FILENAME, ORCAD_SERVER_ENTRY_FILENAME } from '../../shared/orcad-artifacts'
let directory = ''
let launcher = ''
beforeAll(async () => {
directory = await mkdtemp(join(tmpdir(), 'orcad-help-'))
launcher = join(directory, ORCAD_LAUNCHER_FILENAME)
const builder = pathToFileURL(join(process.cwd(), 'config/scripts/orcad-entry-build.mjs')).href
const built = await runProcess({
program: process.execPath,
args: [
'--input-type=module',
'-e',
`import {buildOrcadEntry,buildOrcadLauncher} from ${JSON.stringify(builder)};` +
`await buildOrcadEntry(${JSON.stringify(join(directory, ORCAD_SERVER_ENTRY_FILENAME))});` +
`await buildOrcadLauncher(${JSON.stringify(launcher)});`
],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs: 60_000
})
expect(built.code, built.stderr).toBe(0)
})
afterAll(async () => {
await rm(directory, { recursive: true, force: true })
})
describe('real orcad executable help', () => {
it.each([['--help'], ['-h'], ['--json', '--help'], ['--help', '--json']])(
'prints usage to stdout and exits without creating a profile: %j',
async (...argv) => {
const dataRoot = join(directory, `absent-${argv.join('-')}`)
const result = await runProcess({
program: process.execPath,
args: [launcher, ...argv],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', ORCA_USER_DATA: dataRoot },
timeoutMs: 10_000
})
expect(result.code, result.stderr).toBe(0)
expect(result.timedOut).toBe(false)
expect(result.stderr).toBe('')
expect(result.stdout).toContain('Usage: orcad [options]')
expect(result.stdout).toContain('--bind <ip>')
expect(result.stdout).not.toContain('"type":"orca_server_ready"')
await expect(readdir(dataRoot)).rejects.toMatchObject({ code: 'ENOENT' })
}
)
it('leaves an existing instance lock untouched', async () => {
const dataRoot = join(directory, 'locked-profile')
await mkdir(dataRoot)
const lock = join(dataRoot, 'orcad.lock')
const content = 'an existing instance owns this profile\n'
await writeFile(lock, content)
const result = await runProcess({
program: process.execPath,
args: [launcher, '--help'],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', ORCA_USER_DATA: dataRoot },
timeoutMs: 10_000
})
expect(result.code, result.stderr).toBe(0)
expect(result.stderr).toBe('')
expect(await readFile(lock, 'utf8')).toBe(content)
expect(await readdir(dataRoot)).toEqual(['orcad.lock'])
})
})
@@ -23,6 +23,37 @@ import { ProfileStateAccessError } from '../persistence/profile-state/profile-st
import { OrcadBundledRuntimeError } from './orcad-bundled-runtime'
describe('parseArgs', () => {
it('accepts help alongside valid server flags', () => {
expect(parseArgs(['--help'])).toEqual({ help: true })
expect(parseArgs(['-h'])).toEqual({ help: true })
expect(parseArgs(['--json', '--help', '--port', '0'])).toEqual({
json: true,
help: true,
port: 0
})
})
it('keeps help-looking values as values', () => {
expect(parseArgs(['--bind', '--help'])).toEqual({ bind: '--help' })
expect(parseArgs(['--pairing-address', '-h'])).toEqual({ pairingAddress: '-h' })
expect(parseArgs(['--project-root', '--help'])).toEqual({ projectRoot: '--help' })
expect(parseArgs(['--bind', '--help', '-h'])).toEqual({ bind: '--help', help: true })
})
it('still rejects invalid arguments and incompatible flags with help', () => {
expect(() => parseArgs(['--help', '--unknown'])).toThrow('Unknown argument: --unknown')
expect(() => parseArgs(['--help', '--port', 'bad'])).toThrow('--port expects an integer')
expect(() => parseArgs(['--help', '--bind'])).toThrow('--bind expects a value')
expect(() => parseArgs(['--help', '--recipe-json'])).toThrow(
'--recipe-json requires --project-root'
)
for (const flag of ['--no-pairing', '--mobile-pairing']) {
expect(() => parseArgs(['--help', '--grant-desktop-control', flag])).toThrow(
'--grant-desktop-control applies only to the default runtime pairing offer'
)
}
})
it('accepts --bind and leaves it unset when absent', () => {
expect(parseArgs(['--bind', '0.0.0.0'])).toEqual({ bind: '0.0.0.0' })
expect(parseArgs([])).toEqual({})