mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 00:03:59 +00:00
The four agent hook services, the main hooks module, and the two relay modules each carried a file-level `eslint-disable max-lines` and ran 365-628 counted lines against a 300-line budget. AGENTS.md calls for splitting rather than suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all seven suppressions and prunes their entries (341 -> 334). Pure move, no behavior change. Each hook service splits into its managed script source, its config/bundle serialization, and its remote-install path, keeping the per-agent integrations independent: copilot, amp, antigravity and hermes each retain their own getManagedScript rather than sharing one, because each emits a different script body for a different agent. Merging them by name would have been a behavior change, not a refactor. For antigravity the suppression's stated rationale -- that local install, Windows wrapper generation, status cleanup, and SSH remote install must share one event list and managed-command matcher so stale-hook cleanup cannot drift by platform -- is now enforced structurally instead: both install paths call buildInstalledConfig + createAntigravityManagedCommandMatcher over the single ANTIGRAVITY_EVENTS catalog, with the graph a strict DAG. Also registers the six new antigravity/ and copilot/ modules in config/tsconfig.cli.json. That project uses a curated `include` list rather than a glob, so an unlisted module fails `tsc -p config/tsconfig.tc.cli.json` with TS6307 even though the entire unit suite passes. Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green (remaining failures are pre-existing load flakes in untouched files, green when re-run serially), no new runtime import cycles, and no lint suppression added.
198 lines
8.7 KiB
TypeScript
198 lines
8.7 KiB
TypeScript
import { mkdirSync, writeFileSync, chmodSync } from 'node:fs'
|
|
import { dirname } from 'node:path'
|
|
import { shouldWaitForSetupBeforeAgentStartup } from '../shared/setup-agent-startup-policy'
|
|
import { nativeWindowsPathToPosixShellPath } from '../shared/setup-runner-command'
|
|
import { scriptDeclaresPosixShell } from '../shared/setup-script-shebang'
|
|
import { resolveWindowsShellStartupFamily } from '../shared/windows-terminal-shell'
|
|
import { resolveWindowsGitBashShellPath } from './git-bash'
|
|
import { gitExecFileSync } from './git/runner'
|
|
import { isWslPath, toWindowsWslPath, toLinuxPath } from './wsl'
|
|
import { getHookRuntimeTarget, getHookWslContext } from './hook-runtime-target'
|
|
import { SETUP_RUNNER_PATH_ENV_KEYS, getSetupRunnerEnvVars } from './setup-hook-env-vars'
|
|
import { buildPosixRunnerScript, buildWindowsRunnerScript } from './setup-runner-script-text'
|
|
import type { HookRuntimeTarget } from './hook-runtime-target'
|
|
import type { Repo } from '../shared/repo-types'
|
|
import type { WorktreeSetupLaunch } from '../shared/worktree/launch-types'
|
|
import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime'
|
|
import type { SetupRunnerShell } from '../shared/setup-runner-command'
|
|
|
|
type SetupRunnerShellSettings = Record<string, unknown> | undefined
|
|
|
|
function getGitPath(cwd: string, relativePath: string, runtimeTarget?: HookRuntimeTarget): string {
|
|
return gitExecFileSync(['rev-parse', '--git-path', relativePath], {
|
|
cwd,
|
|
...(runtimeTarget?.wslDistro ? { wslDistro: runtimeTarget.wslDistro } : {})
|
|
}).trim()
|
|
}
|
|
|
|
export function createSetupRunnerScript(
|
|
repo: Repo,
|
|
worktreePath: string,
|
|
script: string,
|
|
projectRuntime?: ProjectExecutionRuntimeResolution | HookRuntimeTarget,
|
|
setupShell?: SetupRunnerShell
|
|
): WorktreeSetupLaunch {
|
|
return createWorktreeRunnerScript({
|
|
repo,
|
|
worktreePath,
|
|
script,
|
|
runnerBaseName: 'setup-runner',
|
|
runtimeTarget: getHookRuntimeTarget(projectRuntime),
|
|
waitForAgentStartup: shouldWaitForSetupBeforeAgentStartup(
|
|
repo.hookSettings?.setupAgentStartupPolicy
|
|
),
|
|
setupShell
|
|
})
|
|
}
|
|
|
|
export function createIssueCommandRunnerScript(
|
|
repo: Repo,
|
|
worktreePath: string,
|
|
command: string,
|
|
projectRuntime?: ProjectExecutionRuntimeResolution | HookRuntimeTarget,
|
|
setupShell?: SetupRunnerShell
|
|
): WorktreeSetupLaunch {
|
|
// Why: writing long commands into a runner script avoids the PTY line editor wrapping/truncating them.
|
|
return createWorktreeRunnerScript({
|
|
repo,
|
|
worktreePath,
|
|
script: command,
|
|
runnerBaseName: 'issue-command-runner',
|
|
runtimeTarget: getHookRuntimeTarget(projectRuntime),
|
|
// Why: issue commands share the setup session's shell resolution; the runner still
|
|
// needs its own `#!` line to be treated as bash.
|
|
setupShell
|
|
})
|
|
}
|
|
|
|
function createWorktreeRunnerScript(args: {
|
|
repo: Repo
|
|
worktreePath: string
|
|
script: string
|
|
runnerBaseName: 'setup-runner' | 'issue-command-runner'
|
|
runtimeTarget?: HookRuntimeTarget
|
|
waitForAgentStartup?: boolean
|
|
setupShell?: SetupRunnerShell
|
|
}): WorktreeSetupLaunch {
|
|
const {
|
|
repo,
|
|
worktreePath,
|
|
script,
|
|
runnerBaseName,
|
|
runtimeTarget,
|
|
waitForAgentStartup,
|
|
setupShell
|
|
} = args
|
|
const envVars = getSetupRunnerEnvVars(repo, worktreePath)
|
|
// Why: WSL worktrees are Linux fs even though process.platform is 'win32'; use bash for WSL, .cmd for native Windows.
|
|
const wslWorktree = isWslPath(worktreePath) || Boolean(runtimeTarget?.wslDistro)
|
|
const nativeWindowsWorktree = process.platform === 'win32' && !wslWorktree
|
|
// Why: the terminal-shell preference says nothing about the language a project's script is
|
|
// written in, and every pre-existing Windows script was authored against the cmd runner. Only a
|
|
// `#!` line opts a script into bash, so the same orca.yaml runs identically for every Windows
|
|
// user of the repo instead of following whichever terminal each of them happens to prefer.
|
|
const runnerShell: SetupRunnerShell = nativeWindowsWorktree
|
|
? setupShell?.family === 'posix' && scriptDeclaresPosixShell(script)
|
|
? setupShell
|
|
: { family: 'cmd' }
|
|
: { family: 'posix' }
|
|
// Why: `shell` tells the launcher which shell types the command, not which format the runner
|
|
// file is in — the .cmd/.sh extension already carries that. Reporting the runner family here
|
|
// would make a Git Bash pane receive `cmd.exe /c ...`, whose `/c` MSYS rewrites into a drive
|
|
// path (issue #6896), so setup would open an interactive cmd and never run.
|
|
const launchShell: SetupRunnerShell | undefined = nativeWindowsWorktree
|
|
? (setupShell ?? { family: 'cmd' })
|
|
: process.platform === 'win32' && runtimeTarget?.wslDistro
|
|
? { family: 'posix', executable: 'wsl.exe' }
|
|
: undefined
|
|
// Why: linked worktrees use a `.git` file, so resolve the real per-worktree gitdir via git rev-parse --git-path.
|
|
const runnerExtension = runnerShell.family === 'cmd' ? 'cmd' : 'sh'
|
|
const gitRelPath = `orca/${runnerBaseName}.${runnerExtension}`
|
|
let runnerScriptPath = getGitPath(worktreePath, gitRelPath, runtimeTarget)
|
|
|
|
// Why: git runs inside WSL and returns a Linux path; convert to a UNC path so the Windows fs calls can reach it.
|
|
if (wslWorktree) {
|
|
const wslInfo = getHookWslContext(worktreePath, runtimeTarget)
|
|
if (wslInfo?.distro) {
|
|
runnerScriptPath = toWindowsWslPath(runnerScriptPath.trim(), wslInfo.distro)
|
|
}
|
|
}
|
|
|
|
mkdirSync(dirname(runnerScriptPath), { recursive: true })
|
|
|
|
if (runnerShell.family === 'cmd') {
|
|
writeFileSync(runnerScriptPath, buildWindowsRunnerScript(script), 'utf-8')
|
|
} else {
|
|
writeFileSync(runnerScriptPath, buildPosixRunnerScript(script), 'utf-8')
|
|
if (!nativeWindowsWorktree) {
|
|
// Why: chmod over a UNC path to the WSL filesystem sets the execute bit correctly inside WSL.
|
|
chmodSync(runnerScriptPath, 0o755)
|
|
}
|
|
}
|
|
|
|
// Why: setup script runs inside WSL bash, so translate the Windows UNC env-var paths to Linux paths.
|
|
if (wslWorktree) {
|
|
for (const key of Object.keys(envVars)) {
|
|
envVars[key] = toLinuxPath(envVars[key])
|
|
}
|
|
} else if (nativeWindowsWorktree && runnerShell.family === 'posix') {
|
|
// Why: a Git Bash runner already receives its own path as /c/..., and the shell exports HOME
|
|
// and PWD the same way, so leaving ORCA_* in C:\ form would make them the lone exception.
|
|
// Only path-valued keys convert; the workspace name and policy values are not paths.
|
|
for (const key of SETUP_RUNNER_PATH_ENV_KEYS) {
|
|
const value = envVars[key]
|
|
if (value) {
|
|
envVars[key] = nativeWindowsPathToPosixShellPath(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
runnerScriptPath,
|
|
envVars,
|
|
// Why: WSL git returns /mnt paths that Node converts back to C:\ for file
|
|
// writes; retain the runtime signal so launch converts them to /mnt again.
|
|
// On native Windows it is the terminal's family, i.e. the shell that types the command.
|
|
...(launchShell ? { shell: launchShell } : {}),
|
|
...(waitForAgentStartup === true ? { waitForAgentStartup: true } : {})
|
|
}
|
|
}
|
|
|
|
export function resolveSetupRunnerShell(
|
|
settings: SetupRunnerShellSettings,
|
|
platform: NodeJS.Platform = process.platform,
|
|
options: { resolveGitBashShellPath?: (shell: string) => string | null } = {}
|
|
): SetupRunnerShell | undefined {
|
|
if (platform !== 'win32') {
|
|
return undefined
|
|
}
|
|
|
|
const terminalWindowsShell = settings?.terminalWindowsShell
|
|
const configuredShell =
|
|
typeof terminalWindowsShell === 'string' && terminalWindowsShell.trim()
|
|
? terminalWindowsShell.trim()
|
|
: 'powershell.exe'
|
|
const shellBasename = configuredShell.replaceAll('\\', '/').split('/').pop()?.toLowerCase()
|
|
const family = resolveWindowsShellStartupFamily(configuredShell)
|
|
if (family === 'posix' && shellBasename !== 'wsl.exe' && shellBasename !== 'wsl') {
|
|
// Why: the PTY resolves Git Bash independently and falls back to PowerShell when it
|
|
// is missing, so gate the .sh runner on the same resolution. This also keeps
|
|
// non-Git bash flavors (Cygwin's /cygdrive, the System32 WSL shim's /mnt) off the
|
|
// MSYS-only /c/... form the posix runner emits.
|
|
const resolveGitBashShellPath =
|
|
options.resolveGitBashShellPath ??
|
|
((shell: string) => resolveWindowsGitBashShellPath(shell, { platform }))
|
|
if (resolveGitBashShellPath(configuredShell)) {
|
|
// Note: this reports the terminal's family — the shell that types the launch command, and
|
|
// therefore also that a bash runner *could* launch here. Whether one is written is decided
|
|
// per script by its `#!` line — see docs/reference/windows-setup-shell.md.
|
|
return { family: 'posix' }
|
|
}
|
|
}
|
|
|
|
// Why: existing Windows setup scripts were authored for Orca's cmd runner;
|
|
// PowerShell, wsl.exe-as-terminal, and Windows-host projects can invoke it
|
|
// without changing syntax, so they intentionally stay on the cmd runner.
|
|
return { family: 'cmd' }
|
|
}
|