Files
orca/src/relay/fs-path-mutation-requests.ts
T
Neil bc28107864 refactor(hooks,relay): split agent hook services and relay under the max-lines budget (#14725)
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.
2026-08-15 18:25:37 -07:00

87 lines
3.3 KiB
TypeScript

import { writeFile, stat, lstat, mkdir, rename, cp, rm } from 'node:fs/promises'
import { expandTilde } from './context'
import { assertNoClobberRenameDestinationAvailable } from '../shared/filesystem-rename-collision'
import type { RelayFilesystemWatchRegistry } from './relay-filesystem-watch-registry'
export async function writeRelayFile(params: Record<string, unknown>) {
const filePath = expandTilde(params.filePath as string)
const content = params.content as string
try {
const fileStats = await lstat(filePath)
if (fileStats.isDirectory()) {
throw new Error('Cannot write to a directory')
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error
}
}
await writeFile(filePath, content, 'utf-8')
}
export async function deleteRelayPath(
params: Record<string, unknown>,
watchRegistry: RelayFilesystemWatchRegistry
) {
const targetPath = expandTilde(params.targetPath as string)
const recursive = params.recursive as boolean | undefined
const stats = await stat(targetPath)
if (stats.isDirectory() && !recursive) {
throw new Error('Cannot delete directory without recursive flag')
}
const remove = () => rm(targetPath, { recursive: !!recursive, force: true })
if (stats.isDirectory()) {
// Why: forced orphan cleanup bypasses git.removeWorktree but must hold
// the same relay-wide watcher fence through recursive deletion.
await watchRegistry.runWithRemovalFence(targetPath, remove)
return
}
await remove()
}
export async function createRelayFile(params: Record<string, unknown>) {
const filePath = expandTilde(params.filePath as string)
const { dirname } = await import('node:path')
await mkdir(dirname(filePath), { recursive: true })
await writeFile(filePath, '', { encoding: 'utf-8', flag: 'wx' })
}
export async function createRelayDir(params: Record<string, unknown>) {
const dirPath = expandTilde(params.dirPath as string)
await mkdir(dirPath, { recursive: true })
}
export async function createRelayDirNoClobber(params: Record<string, unknown>) {
const dirPath = expandTilde(params.dirPath as string)
await mkdir(dirPath, { recursive: false })
}
export async function renameRelayPath(params: Record<string, unknown>) {
const oldPath = expandTilde(params.oldPath as string)
const newPath = expandTilde(params.newPath as string)
await rename(oldPath, newPath)
}
export async function renameRelayPathNoClobber(params: Record<string, unknown>) {
const oldPath = expandTilde(params.oldPath as string)
const newPath = expandTilde(params.newPath as string)
// Why: user-facing file renames must not inherit fs.rename's overwrite
// behavior; keep the guard inside the relay so SSH checks the remote FS.
await assertNoClobberRenameDestinationAvailable(oldPath, newPath)
await rename(oldPath, newPath)
}
export async function copyRelayPath(params: Record<string, unknown>) {
const source = expandTilde(params.source as string)
const destination = expandTilde(params.destination as string)
try {
await cp(source, destination, { recursive: true, force: false, errorOnExist: true })
} catch (error) {
const code = error instanceof Error ? (error as NodeJS.ErrnoException).code : undefined
if (code === 'EEXIST' || code === 'ERR_FS_CP_EEXIST') {
throw new Error('EEXIST: destination already exists')
}
throw error
}
}