Files
orca/src/main/cli/cli-command-inspection.ts
T
Neil 83ffc0df24 refactor Electron facilities modules (#16333)
* refactor oversized Electron facilities

* fix interactive process timeout and shortcut repeat guard

* chore(child-process): drop stale cli-installer allowlist entry

cli-installer.ts now routes privileged spawns through runProcess via
cli-privileged-processes.ts, so the shrink-only ratchet flags it as stale.

* refactor(child-process): extract the bounded output sink

runProcess's timeoutMs opt-out (required to preserve the unbounded osascript
admin prompt) pushed run-process.ts past the 300-line cap. Move createOutputSink
to its own module rather than add a max-lines bypass, which AGENTS.md forbids.
Moved verbatim; no behavior change.
2026-08-25 00:31:31 -07:00

279 lines
9.0 KiB
TypeScript

import { lstat, readFile, readlink } from 'node:fs/promises'
import { basename, dirname, resolve } from 'node:path'
import type { CliInstallMethod, CliInstallStatus } from '../../shared/cli-install-types'
import { buildAppImageCliWrapper } from './appimage-cli-wrapper'
import { DEV_COMMAND_NAME, DEV_LAUNCHER_DIR } from './cli-install-constants'
import { buildWindowsForwarder, extractManagedUnixLauncherTarget } from './cli-dev-launcher'
import { isMissingError } from './cli-install-errors'
import { CliInstallLocation } from './cli-install-location'
import { isPathInsideOrEqual, samePathEntry } from './cli-install-path-format'
export class CliCommandInspection extends CliInstallLocation {
protected async inspectAppImageWrapper(
commandPath: string,
appImagePath: string
): Promise<CliInstallStatus> {
try {
const stats = await lstat(commandPath)
if (!stats.isFile()) {
return this.buildStatus({
commandPath,
launcherPath: appImagePath,
installMethod: 'wrapper',
supported: true,
state: 'conflict',
currentTarget: null,
detail: `${commandPath} exists but is not an Orca launcher script.`
})
}
const currentContent = await readFile(commandPath, 'utf8')
const expectedContent = buildAppImageCliWrapper(appImagePath)
return this.buildStatus({
commandPath,
launcherPath: appImagePath,
installMethod: 'wrapper',
supported: true,
state: currentContent === expectedContent ? 'installed' : 'stale',
currentTarget: appImagePath,
detail:
currentContent === expectedContent
? `Registered at ${commandPath}.`
: `${commandPath} points to a different launcher.`
})
} catch (error) {
if (isMissingError(error)) {
return this.buildStatus({
commandPath,
launcherPath: appImagePath,
installMethod: 'wrapper',
supported: true,
state: 'not_installed',
currentTarget: null,
detail: `Register ${commandPath} to use Orca from the terminal.`
})
}
throw error
}
}
protected async inspectSymlink(
commandPath: string,
launcherPath: string
): Promise<CliInstallStatus> {
try {
const stats = await lstat(commandPath)
if (!stats.isSymbolicLink()) {
if (stats.isFile()) {
const currentContent = await readFile(commandPath, 'utf8')
const managedTarget = extractManagedUnixLauncherTarget(currentContent)
if (managedTarget) {
return this.buildStatus({
commandPath,
launcherPath,
installMethod: 'symlink',
supported: true,
state: 'stale',
currentTarget: managedTarget,
detail: `${commandPath} contains an older Orca launcher.`
})
}
}
return this.buildStatus({
commandPath,
launcherPath,
installMethod: 'symlink',
supported: true,
state: 'conflict',
currentTarget: null,
detail: `${commandPath} exists but is not an Orca symlink.`
})
}
const currentTarget = await readlink(commandPath)
const resolvedCurrentTarget = resolve(dirname(commandPath), currentTarget)
const resolvedLauncher = resolve(launcherPath)
const isInstalled = resolvedCurrentTarget === resolvedLauncher
const isManagedStaleTarget =
!isInstalled && this.isManagedSymlinkTarget(resolvedCurrentTarget, launcherPath)
return this.buildStatus({
commandPath,
launcherPath,
installMethod: 'symlink',
supported: true,
state: isInstalled ? 'installed' : isManagedStaleTarget ? 'stale' : 'conflict',
currentTarget: resolvedCurrentTarget,
detail: isInstalled
? `Registered at ${commandPath}.`
: isManagedStaleTarget
? `${commandPath} points to an older Orca launcher.`
: `${commandPath} points to a non-Orca launcher.`
})
} catch (error) {
if (isMissingError(error)) {
return this.buildStatus({
commandPath,
launcherPath,
installMethod: 'symlink',
supported: true,
state: 'not_installed',
currentTarget: null,
detail: `Register ${commandPath} to use Orca from the terminal.`
})
}
throw error
}
}
protected isManagedSymlinkTarget(resolvedTarget: string, launcherPath: string): boolean {
const expectedName = basename(launcherPath)
if (this.isPackaged && this.isSiblingDevLauncherTarget(resolvedTarget, expectedName)) {
return true
}
if (basename(resolvedTarget) !== expectedName) {
return false
}
const devLauncherDir = resolve(this.userDataPath, ...DEV_LAUNCHER_DIR)
if (isPathInsideOrEqual(devLauncherDir, resolvedTarget)) {
return true
}
if (this.platform === 'darwin') {
// Why: reclaim symlinks to an older Orca.app launcher, but never replace arbitrary user-owned symlinks.
return /(?:^|[/\\])[^/\\]+\.app[/\\]Contents[/\\]Resources[/\\]bin[/\\][^/\\]+$/.test(
resolvedTarget
)
}
if (this.platform === 'linux') {
return /(?:^|[/\\])resources[/\\]bin[/\\][^/\\]+$/.test(resolvedTarget)
}
return false
}
protected isSiblingDevLauncherTarget(
resolvedTarget: string,
packagedLauncherName: string
): boolean {
if (![packagedLauncherName, DEV_COMMAND_NAME].includes(basename(resolvedTarget))) {
return false
}
const packagedUserDataPath = resolve(this.userDataPath)
const siblingDevUserDataPath = `${packagedUserDataPath}-dev`
const siblingDevLauncherDir = resolve(siblingDevUserDataPath, ...DEV_LAUNCHER_DIR)
// Why: dev builds generate launchers under the sibling `*-dev` profile; packaged Orca must reclaim that command.
return (
basename(siblingDevUserDataPath) === `${basename(packagedUserDataPath)}-dev` &&
isPathInsideOrEqual(siblingDevLauncherDir, resolvedTarget)
)
}
protected isLinuxAppImage(): boolean {
return this.platform === 'linux' && Boolean(this.appImagePath)
}
protected isWindowsPackagedBundledCommand(
commandPath: string | null,
launcherPath: string | null
): boolean {
return (
this.platform === 'win32' &&
this.isPackaged &&
commandPath !== null &&
launcherPath !== null &&
samePathEntry('win32', commandPath, launcherPath)
)
}
protected async inspectWindowsWrapper(
commandPath: string,
launcherPath: string
): Promise<CliInstallStatus> {
try {
const stats = await lstat(commandPath)
if (!stats.isFile()) {
return this.buildStatus({
commandPath,
launcherPath,
installMethod: 'wrapper',
supported: true,
state: 'conflict',
currentTarget: null,
detail: `${commandPath} exists but is not an Orca launcher script.`
})
}
if (this.isWindowsPackagedBundledCommand(commandPath, launcherPath)) {
return this.buildStatus({
commandPath,
launcherPath,
installMethod: 'wrapper',
supported: true,
state: 'installed',
currentTarget: launcherPath,
detail: `Registered at ${commandPath}.`
})
}
const currentContent = await readFile(commandPath, 'utf8')
const expectedContent = buildWindowsForwarder(launcherPath)
return this.buildStatus({
commandPath,
launcherPath,
installMethod: 'wrapper',
supported: true,
state: currentContent === expectedContent ? 'installed' : 'stale',
currentTarget: launcherPath,
detail:
currentContent === expectedContent
? `Registered at ${commandPath}.`
: `${commandPath} points to a different launcher.`
})
} catch (error) {
if (isMissingError(error)) {
return this.buildStatus({
commandPath,
launcherPath,
installMethod: 'wrapper',
supported: true,
state: 'not_installed',
currentTarget: null,
detail: `Register ${commandPath} to use Orca from Command Prompt or PowerShell.`
})
}
throw error
}
}
protected buildStatus(args: {
commandPath: string
launcherPath: string
installMethod: CliInstallMethod
supported: boolean
state: CliInstallStatus['state']
currentTarget: string | null
detail: string | null
}): CliInstallStatus {
return {
platform: this.platform,
commandName: this.commandName,
commandPath: args.commandPath,
pathDirectory: dirname(args.commandPath),
pathConfigured: false,
launcherPath: args.launcherPath,
installMethod: args.installMethod,
supported: args.supported,
state: args.state,
currentTarget: args.currentTarget,
unsupportedReason: null,
detail: args.detail
}
}
}