Fix AppImage CLI launcher path
This commit is contained in:
parent
f097c81e5e
commit
38feb52864
|
|
@ -125,6 +125,7 @@ module.exports = {
|
|||
}
|
||||
prunePackagedRuntimeNodeModules(resourcesDir, context.electronPlatformName)
|
||||
verifyPackagedMainRuntimeDeps(resourcesDir)
|
||||
chmodUnixCliLaunchers(resourcesDir, context.electronPlatformName)
|
||||
for (const filename of readdirSync(resourcesDir)) {
|
||||
if (!filename.startsWith('agent-browser-')) {
|
||||
continue
|
||||
|
|
@ -286,6 +287,21 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
|
||||
function chmodUnixCliLaunchers(resourcesDir, electronPlatformName) {
|
||||
if (electronPlatformName === 'win32') {
|
||||
return
|
||||
}
|
||||
for (const launcherName of ['orca', 'orca-ide']) {
|
||||
const launcherPath = join(resourcesDir, 'bin', launcherName)
|
||||
if (!existsSync(launcherPath)) {
|
||||
continue
|
||||
}
|
||||
// Why: packaged Unix installs expose these extraResources as public shell
|
||||
// commands, and source/packager mode drift must not ship a non-executable CLI.
|
||||
chmodSync(launcherPath, 0o755)
|
||||
}
|
||||
}
|
||||
|
||||
async function signMacComputerUseHelper(helperAppPath, packager) {
|
||||
if (!existsSync(helperAppPath)) {
|
||||
if (isMacRelease) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, readdir, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
|
@ -89,10 +89,7 @@ describe('electron-builder config', () => {
|
|||
|
||||
const sources = new Map([
|
||||
['out\\main\\index.js', 'const z = require("zod")'],
|
||||
[
|
||||
'out\\main\\agent-hooks\\managed-agent-hook-controls.js',
|
||||
'const YAML = require("yaml")'
|
||||
]
|
||||
['out\\main\\agent-hooks\\managed-agent-hook-controls.js', 'const YAML = require("yaml")']
|
||||
])
|
||||
const asar = {
|
||||
listPackage: () => [...sources.keys()].map((entry) => `\\${entry}`),
|
||||
|
|
@ -136,9 +133,9 @@ describe('electron-builder config', () => {
|
|||
await expect(
|
||||
readdir(join(resourcesDir, 'node_modules', 'node-pty', 'third_party'))
|
||||
).resolves.toEqual([])
|
||||
await expect(readdir(join(resourcesDir, 'node_modules', 'node-pty', 'deps'))).resolves.toEqual(
|
||||
[]
|
||||
)
|
||||
await expect(
|
||||
readdir(join(resourcesDir, 'node_modules', 'node-pty', 'deps'))
|
||||
).resolves.toEqual([])
|
||||
} finally {
|
||||
await rm(resourcesDir, { recursive: true, force: true })
|
||||
}
|
||||
|
|
@ -197,4 +194,27 @@ describe('electron-builder config', () => {
|
|||
await rm(resourcesDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'marks packaged Unix CLI launchers executable',
|
||||
async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-electron-builder-config-'))
|
||||
try {
|
||||
const resourcesDir = join(root, 'linux-unpacked', 'resources')
|
||||
const launcherPath = join(resourcesDir, 'bin', 'orca-ide')
|
||||
await mkdir(join(resourcesDir, 'bin'), { recursive: true })
|
||||
await mkdir(join(resourcesDir, 'node_modules', 'zod', 'src'), { recursive: true })
|
||||
await writeFile(launcherPath, '#!/usr/bin/env bash\n', { encoding: 'utf8', mode: 0o644 })
|
||||
|
||||
await electronBuilderConfig.afterPack({
|
||||
appOutDir: join(root, 'linux-unpacked'),
|
||||
electronPlatformName: 'linux'
|
||||
})
|
||||
|
||||
expect((await stat(launcherPath)).mode & 0o111).not.toBe(0)
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { execFile } from 'node:child_process'
|
||||
import { copyFile, chmod, mkdir, mkdtemp, rm, stat, symlink, writeFile } from 'node:fs/promises'
|
||||
import { copyFile, mkdir, mkdtemp, rm, stat, symlink, writeFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
|
@ -62,7 +62,7 @@ describe('packaged CLI assets', () => {
|
|||
await mkdir(launcherDir, { recursive: true })
|
||||
await mkdir(cliDir, { recursive: true })
|
||||
await copyFile(linuxLauncherAsset, launcherPath)
|
||||
await chmod(launcherPath, 0o755)
|
||||
expect((await stat(launcherPath)).mode & 0o111).not.toBe(0)
|
||||
await writeFile(cliPath, '', 'utf8')
|
||||
await writeFile(
|
||||
electronPath,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import {
|
|||
patchPackagedProcessPath,
|
||||
shouldInstallManagedHooks
|
||||
} from './startup/configure-process'
|
||||
import { maybeRedirectAppImageCliLaunch } from './startup/appimage-cli-redirect'
|
||||
import { startFirstWindowStartupServices } from './startup/first-window-startup-services'
|
||||
import { getDevInstanceIdentity } from './startup/dev-instance-identity'
|
||||
import { hydrateShellPath, mergePathSegments } from './startup/hydrate-shell-path'
|
||||
|
|
@ -138,6 +139,14 @@ let automations: AutomationService | null = null
|
|||
let keybindings: KeybindingService | null = null
|
||||
let expectedRendererReload: { webContentsId: number; until: number } | null = null
|
||||
const isServeMode = process.argv.includes('--serve')
|
||||
const appImageCliRedirect = maybeRedirectAppImageCliLaunch({
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
execPath: process.execPath
|
||||
})
|
||||
if (appImageCliRedirect.redirected) {
|
||||
app.exit(appImageCliRedirect.status)
|
||||
}
|
||||
|
||||
// Why: the store/runtime singletons live here in index.ts; injecting them keeps
|
||||
// the rename orchestrator free of module-level state and unit-testable.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { getAppImageCliArgs, maybeRedirectAppImageCliLaunch } from './appimage-cli-redirect'
|
||||
|
||||
const commandNames = ['status', 'terminal']
|
||||
|
||||
describe('AppImage CLI redirect', () => {
|
||||
it('detects direct AppImage CLI commands', () => {
|
||||
expect(
|
||||
getAppImageCliArgs(
|
||||
['orca-linux.AppImage', 'status', '--json'],
|
||||
{ APPIMAGE: '/opt/orca' },
|
||||
{
|
||||
platform: 'linux',
|
||||
isPackaged: true,
|
||||
commandNames
|
||||
}
|
||||
)
|
||||
).toEqual(['status', '--json'])
|
||||
})
|
||||
|
||||
it('allows CLI global flags before the command', () => {
|
||||
expect(
|
||||
getAppImageCliArgs(
|
||||
['orca-linux.AppImage', '--pairing-code', 'abc123', '--json', 'terminal', 'list'],
|
||||
{
|
||||
APPIMAGE: '/opt/orca'
|
||||
},
|
||||
{
|
||||
platform: 'linux',
|
||||
isPackaged: true,
|
||||
commandNames
|
||||
}
|
||||
)
|
||||
).toEqual(['--pairing-code', 'abc123', '--json', 'terminal', 'list'])
|
||||
})
|
||||
|
||||
it('does not redirect normal desktop AppImage launches', () => {
|
||||
expect(
|
||||
getAppImageCliArgs(
|
||||
['AppRun', '--no-sandbox', 'file:///tmp/example.txt'],
|
||||
{
|
||||
APPIMAGE: '/opt/orca'
|
||||
},
|
||||
{
|
||||
platform: 'linux',
|
||||
isPackaged: true,
|
||||
commandNames
|
||||
}
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('spawns the unpacked CLI entrypoint with Electron node mode', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-appimage-cli-redirect-'))
|
||||
const cliEntryPath = join(root, 'app.asar.unpacked', 'out', 'cli', 'index.js')
|
||||
await mkdir(join(root, 'app.asar.unpacked', 'out', 'cli'), { recursive: true })
|
||||
await writeFile(cliEntryPath, '', 'utf8')
|
||||
const spawn = vi.fn((..._args: unknown[]) => ({ status: 0 }))
|
||||
|
||||
const result = maybeRedirectAppImageCliLaunch({
|
||||
argv: ['orca-linux.AppImage', 'status', '--json'],
|
||||
env: {
|
||||
APPIMAGE: '/opt/orca/orca-linux.AppImage',
|
||||
NODE_OPTIONS: '--inspect',
|
||||
NODE_REPL_EXTERNAL_MODULE: '/tmp/repl.js'
|
||||
},
|
||||
platform: 'linux',
|
||||
isPackaged: true,
|
||||
resourcesPath: root,
|
||||
execPath: '/opt/orca/orca-ide',
|
||||
commandNames,
|
||||
spawn: spawn as never
|
||||
})
|
||||
|
||||
expect(result).toEqual({ redirected: true, status: 0 })
|
||||
expect(spawn).toHaveBeenCalledWith('/opt/orca/orca-ide', [cliEntryPath, 'status', '--json'], {
|
||||
env: expect.objectContaining({
|
||||
APPIMAGE: '/opt/orca/orca-linux.AppImage',
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
ORCA_NODE_OPTIONS: '--inspect',
|
||||
ORCA_NODE_REPL_EXTERNAL_MODULE: '/tmp/repl.js'
|
||||
}),
|
||||
stdio: 'inherit'
|
||||
})
|
||||
const spawnOptions = spawn.mock.calls[0]?.[2] as { env: NodeJS.ProcessEnv } | undefined
|
||||
expect(spawnOptions?.env).not.toHaveProperty('NODE_OPTIONS')
|
||||
expect(spawnOptions?.env).not.toHaveProperty('NODE_REPL_EXTERNAL_MODULE')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
import { spawnSync, type SpawnSyncReturns } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
type RedirectResult =
|
||||
| {
|
||||
redirected: false
|
||||
}
|
||||
| {
|
||||
redirected: true
|
||||
status: number
|
||||
}
|
||||
|
||||
type RedirectOptions = {
|
||||
argv?: string[]
|
||||
env?: NodeJS.ProcessEnv
|
||||
platform?: NodeJS.Platform
|
||||
isPackaged?: boolean
|
||||
resourcesPath?: string
|
||||
execPath?: string
|
||||
commandNames?: readonly string[]
|
||||
spawn?: typeof spawnSync
|
||||
}
|
||||
|
||||
const HELP_FLAGS = new Set(['--help', '-h', 'help'])
|
||||
const APPIMAGE_DESKTOP_FLAGS = new Set(['--no-sandbox'])
|
||||
const CLI_FLAGS_WITH_VALUES = new Set(['--environment', '--pairing-code'])
|
||||
// Why: the main tsconfig cannot import the CLI project, but AppImage direct
|
||||
// launches need a conservative allow-list before bypassing the GUI startup.
|
||||
const APPIMAGE_CLI_COMMAND_NAMES = [
|
||||
'agent',
|
||||
'automations',
|
||||
'back',
|
||||
'capture',
|
||||
'check',
|
||||
'clear',
|
||||
'click',
|
||||
'clipboard',
|
||||
'computer',
|
||||
'console',
|
||||
'cookie',
|
||||
'dblclick',
|
||||
'dialog',
|
||||
'download',
|
||||
'drag',
|
||||
'environment',
|
||||
'eval',
|
||||
'exec',
|
||||
'file',
|
||||
'fill',
|
||||
'find',
|
||||
'focus',
|
||||
'forward',
|
||||
'full-screenshot',
|
||||
'geolocation',
|
||||
'get',
|
||||
'goto',
|
||||
'highlight',
|
||||
'hover',
|
||||
'inserttext',
|
||||
'intercept',
|
||||
'is',
|
||||
'keypress',
|
||||
'mouse',
|
||||
'network',
|
||||
'open',
|
||||
'orchestration',
|
||||
'pdf',
|
||||
'reload',
|
||||
'repo',
|
||||
'screenshot',
|
||||
'scroll',
|
||||
'scrollintoview',
|
||||
'select',
|
||||
'select-all',
|
||||
'serve',
|
||||
'set',
|
||||
'snapshot',
|
||||
'status',
|
||||
'storage',
|
||||
'tab',
|
||||
'terminal',
|
||||
'type',
|
||||
'uncheck',
|
||||
'upload',
|
||||
'viewport',
|
||||
'wait',
|
||||
'worktree'
|
||||
]
|
||||
|
||||
export function maybeRedirectAppImageCliLaunch(options: RedirectOptions = {}): RedirectResult {
|
||||
const argv = options.argv ?? process.argv
|
||||
const env = options.env ?? process.env
|
||||
const platform = options.platform ?? process.platform
|
||||
const isPackaged = options.isPackaged ?? false
|
||||
const resourcesPath = options.resourcesPath ?? process.resourcesPath
|
||||
const execPath = options.execPath ?? process.execPath
|
||||
const spawn = options.spawn ?? spawnSync
|
||||
const cliArgs = getAppImageCliArgs(argv, env, {
|
||||
platform,
|
||||
isPackaged,
|
||||
commandNames: options.commandNames ?? APPIMAGE_CLI_COMMAND_NAMES
|
||||
})
|
||||
|
||||
if (!cliArgs) {
|
||||
return { redirected: false }
|
||||
}
|
||||
|
||||
const cliEntryPath = join(resourcesPath, 'app.asar.unpacked', 'out', 'cli', 'index.js')
|
||||
if (!existsSync(cliEntryPath)) {
|
||||
process.stderr.write(`Unable to locate the Orca CLI entrypoint at ${cliEntryPath}\n`)
|
||||
return { redirected: true, status: 1 }
|
||||
}
|
||||
|
||||
const childEnv = buildElectronRunAsNodeEnv(env)
|
||||
const result = spawn(execPath, [cliEntryPath, ...cliArgs], {
|
||||
env: childEnv,
|
||||
stdio: 'inherit'
|
||||
}) as SpawnSyncReturns<Buffer>
|
||||
|
||||
if (result.error) {
|
||||
process.stderr.write(`${result.error.message}\n`)
|
||||
return { redirected: true, status: 1 }
|
||||
}
|
||||
|
||||
return { redirected: true, status: result.status ?? 1 }
|
||||
}
|
||||
|
||||
export function getAppImageCliArgs(
|
||||
argv: string[],
|
||||
env: NodeJS.ProcessEnv,
|
||||
options: {
|
||||
platform: NodeJS.Platform
|
||||
isPackaged: boolean
|
||||
commandNames: readonly string[]
|
||||
}
|
||||
): string[] | null {
|
||||
if (options.platform !== 'linux' || !options.isPackaged) {
|
||||
return null
|
||||
}
|
||||
if (!env.APPIMAGE && !env.APPDIR) {
|
||||
return null
|
||||
}
|
||||
|
||||
const args = argv.slice(1)
|
||||
if (args.length === 0 || args.some((arg) => APPIMAGE_DESKTOP_FLAGS.has(arg))) {
|
||||
return null
|
||||
}
|
||||
if (args.some((arg) => HELP_FLAGS.has(arg))) {
|
||||
return args
|
||||
}
|
||||
|
||||
const commandNames = new Set(options.commandNames)
|
||||
const firstPositional = findFirstCommandCandidate(args)
|
||||
return firstPositional && commandNames.has(firstPositional) ? args : null
|
||||
}
|
||||
|
||||
function findFirstCommandCandidate(args: string[]): string | null {
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index]
|
||||
if (!arg.startsWith('-')) {
|
||||
return arg
|
||||
}
|
||||
const flagName = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg
|
||||
if (CLI_FLAGS_WITH_VALUES.has(flagName) && !arg.includes('=')) {
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function buildElectronRunAsNodeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
const childEnv = { ...env }
|
||||
childEnv.ORCA_NODE_OPTIONS = env.NODE_OPTIONS ?? ''
|
||||
childEnv.ORCA_NODE_REPL_EXTERNAL_MODULE = env.NODE_REPL_EXTERNAL_MODULE ?? ''
|
||||
childEnv.ELECTRON_RUN_AS_NODE = '1'
|
||||
delete childEnv.NODE_OPTIONS
|
||||
delete childEnv.NODE_REPL_EXTERNAL_MODULE
|
||||
return childEnv
|
||||
}
|
||||
Loading…
Reference in New Issue