mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 08:07:18 +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.
87 lines
3.3 KiB
TypeScript
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
|
|
}
|
|
}
|