From 25bfa39fcf7589d52c28dafcf73b254cf41479e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 2 Sep 2026 22:10:57 -0400 Subject: [PATCH] chore(node-sdk): audit providers kirocli kiroide opencode Kiro and OpenCode files use common.ts, shell-command.ts, and managedScriptReference; exactKeys shared from kiroide-contract. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq --- src/plugins/kirocli-contract.ts | 44 +++----- src/plugins/kirocli-installation.ts | 10 +- src/plugins/kirocli-io.ts | 7 +- src/plugins/kiroide-command.ts | 149 +++------------------------ src/plugins/kiroide-contract.ts | 16 +-- src/plugins/kiroide-installation.ts | 14 +-- src/plugins/opencode-contract.ts | 16 +-- src/plugins/opencode-installation.ts | 14 +-- src/plugins/opencode-io.ts | 15 +-- 9 files changed, 57 insertions(+), 228 deletions(-) diff --git a/src/plugins/kirocli-contract.ts b/src/plugins/kirocli-contract.ts index 521cdd7..2a7d5a3 100644 --- a/src/plugins/kirocli-contract.ts +++ b/src/plugins/kirocli-contract.ts @@ -1,14 +1,14 @@ import os from 'node:os'; import path from 'node:path'; +import { managedScriptReference, sameAgentId, samePath } from './common.js'; import { buildKiroIdeCommand, kiroIdeRuntimeReference, - sameKiroIdeAgentId, - sameKiroIdePath, type KiroIdeRuntimeReference, } from './kiroide-command.js'; import { buildKiroIdeHook, + exactKeys, kiroIdeRuntimeContracts, renderKiroIdeDocument, type KiroIdeDocument, @@ -16,6 +16,7 @@ import { type RenderedKiroIdeDocument, type KiroIdeRuntimeContract, } from './kiroide-contract.js'; +import { isNodeExecutable, parseLegacyWindowsCommand } from './shell-command.js'; import { isObject, parseStrictJsonObject, type JsonObject } from './strict-json.js'; export const AGENT_KEY = 'kirocli'; @@ -162,27 +163,14 @@ function eventHooks(document: KiroCliV2Document, event: string): readonly KiroCl return (document.hooks[event] as readonly KiroCliV2Hook[] | undefined) ?? []; } -function exactKeys(value: JsonObject, keys: readonly string[]): boolean { - return Object.keys(value).sort().join('|') === [...keys].sort().join('|'); -} - function legacyWindowsReference( command: unknown, scriptName: string, ): KiroIdeRuntimeReference | undefined { if (typeof command !== 'string') return undefined; - const match = /^"([^"\r\n]+)" "([^"\r\n]+)"$/.exec(command); - if (!match - || !path.isAbsolute(match[1]) - || !['node', 'node.exe'].includes(path.basename(match[1]).toLowerCase()) - || !path.isAbsolute(match[2]) - || path.basename(match[2]) !== scriptName) return undefined; - const agentDirectory = path.dirname(match[2]); - if (!sameKiroIdePath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))) { - return undefined; - } - const agentId = path.basename(agentDirectory); - return agentId ? { agentId, scriptPath: match[2] } : undefined; + const parsed = parseLegacyWindowsCommand(command); + if (!parsed || !path.isAbsolute(parsed[0]) || !isNodeExecutable(parsed[0])) return undefined; + return managedScriptReference(parsed[1], scriptName); } function ownedReference(command: unknown, scriptName: string): KiroIdeRuntimeReference | undefined { @@ -219,7 +207,7 @@ function hasOwnedV2Hooks(document: KiroCliV2Document, agentId?: string): boolean return ['preToolUse', 'postToolUse'].some((event) => eventHooks(document, event).some((hook) => { const reference = v2OwnedReference(hook, event); return reference !== undefined - && (agentId === undefined || sameKiroIdeAgentId(reference.agentId, agentId)); + && (agentId === undefined || sameAgentId(reference.agentId, agentId)); })); } @@ -237,7 +225,7 @@ export function withoutManagedKiroCliV2Hooks( for (const event of ['preToolUse', 'postToolUse']) { const remaining = eventHooks(document, event).filter((hook) => { const reference = v2OwnedReference(hook, event); - return !reference || (agentId !== undefined && !sameKiroIdeAgentId(reference.agentId, agentId)); + return !reference || (agentId !== undefined && !sameAgentId(reference.agentId, agentId)); }); if (remaining.length > 0) next[event] = remaining; else delete next[event]; @@ -288,7 +276,7 @@ function entirelyManagedV2Document(document: KiroCliV2Document, agentId?: string const hooks = eventHooks(document, event); if (hooks.length !== 1) return false; const reference = v2OwnedReference(hooks[0], event); - if (!reference || (agentId !== undefined && !sameKiroIdeAgentId(reference.agentId, agentId))) { + if (!reference || (agentId !== undefined && !sameAgentId(reference.agentId, agentId))) { return false; } } @@ -324,7 +312,7 @@ export function kiroCliV2RuntimeContracts( .map((hook) => v2ManagedReference(hook, 'postToolUse')) .filter((value): value is KiroIdeRuntimeReference => value !== undefined); if (guards.length !== 1 || audits.length !== 1 - || !sameKiroIdeAgentId(guards[0].agentId, audits[0].agentId)) return []; + || !sameAgentId(guards[0].agentId, audits[0].agentId)) return []; return [{ agentId: guards[0].agentId, guardPath: guards[0].scriptPath, auditPath: audits[0].scriptPath }]; } @@ -355,7 +343,7 @@ export function withoutManagedKiroCliV3Hooks( ): KiroIdeHook[] { return hooks.filter((hook) => { const reference = v3OwnedReference(hook); - return !reference || (agentId !== undefined && !sameKiroIdeAgentId(reference.agentId, agentId)); + return !reference || (agentId !== undefined && !sameAgentId(reference.agentId, agentId)); }); } @@ -363,7 +351,7 @@ function hasOwnedV3Hooks(hooks: readonly KiroIdeHook[], agentId?: string): boole return hooks.some((hook) => { const reference = v3OwnedReference(hook); return reference !== undefined - && (agentId === undefined || sameKiroIdeAgentId(reference.agentId, agentId)); + && (agentId === undefined || sameAgentId(reference.agentId, agentId)); }); } @@ -374,7 +362,7 @@ function entirelyManagedV3Document(document: KiroIdeDocument, agentId?: string): && document.hooks.every((hook) => { const reference = v3OwnedReference(hook); return reference !== undefined - && (agentId === undefined || sameKiroIdeAgentId(reference.agentId, agentId)); + && (agentId === undefined || sameAgentId(reference.agentId, agentId)); }); } @@ -409,8 +397,8 @@ export function commonKiroCliRuntimeContract( const v2 = kiroCliV2RuntimeContracts(v2Document); const v3 = kiroIdeRuntimeContracts(v3Hooks); if (v2.length !== 1 || v3.length !== 1 - || !sameKiroIdeAgentId(v2[0].agentId, v3[0].agentId) - || !sameKiroIdePath(v2[0].guardPath, v3[0].guardPath) - || !sameKiroIdePath(v2[0].auditPath, v3[0].auditPath)) return undefined; + || !sameAgentId(v2[0].agentId, v3[0].agentId) + || !samePath(v2[0].guardPath, v3[0].guardPath) + || !samePath(v2[0].auditPath, v3[0].auditPath)) return undefined; return v2[0]; } diff --git a/src/plugins/kirocli-installation.ts b/src/plugins/kirocli-installation.ts index b1c1aea..8d6c414 100644 --- a/src/plugins/kirocli-installation.ts +++ b/src/plugins/kirocli-installation.ts @@ -27,10 +27,6 @@ const DISPLAY_NAME = 'Kiro CLI'; const V2_DIRECTORY_LABEL = 'Kiro CLI agents directory'; const V3_DIRECTORY_LABEL = 'Kiro CLI global hooks directory'; -export type KiroCliRuntimePaths = ManagedRuntimePaths; -export type PreparedKiroCliInstallation = PreparedManagedInstallation; -export type { RenameFile }; - function hookLocations(sources: KiroCliSources) { return [ { directoryLabel: V2_DIRECTORY_LABEL, filePath: sources.paths.v2Path }, @@ -41,7 +37,7 @@ function hookLocations(sources: KiroCliSources) { export async function preflightKiroCliInstallation( config: InstallConfig, sources: KiroCliSources, -): Promise { +): Promise { return preflightManagedInstallation({ agentKey: AGENT_KEY, hookLocations: hookLocations(sources), @@ -54,7 +50,7 @@ export async function prepareKiroCliInstallation( sources: KiroCliSources, v2: RenderedKiroCliV2Document, v3: RenderedKiroIdeDocument, -): Promise { +): Promise { if (v2.next === undefined || v3.next === undefined) { throw new Error('Kiro CLI installation requires both v2 and v3 hook documents'); } @@ -83,7 +79,7 @@ export async function prepareKiroCliInstallation( } export async function commitKiroCliInstallation( - prepared: PreparedKiroCliInstallation, + prepared: PreparedManagedInstallation, renameFile?: RenameFile, ): Promise { await commitManagedInstallation(prepared, renameFile); diff --git a/src/plugins/kirocli-io.ts b/src/plugins/kirocli-io.ts index f4540d6..d292cbc 100644 --- a/src/plugins/kirocli-io.ts +++ b/src/plugins/kirocli-io.ts @@ -12,11 +12,10 @@ import { type KiroIdeDocument, type KiroIdeRuntimeContract, } from './kiroide-contract.js'; +import { MAX_CONFIG_BYTES } from './common.js'; import { inspectPhysicalDirectory, readPhysicalFile } from './managed-files.js'; import { managedRuntimeFilesExist } from './managed-runtime-status.js'; -const MAX_HOOK_BYTES = 512 * 1024; - export interface KiroCliSources { readonly paths: KiroCliPaths; readonly v2: KiroCliV2Document; @@ -36,7 +35,7 @@ async function readV2Document(paths: KiroCliPaths): Promise { const snapshot = await readPhysicalFile( paths.v2Path, 'Kiro CLI v2 agent config', - MAX_HOOK_BYTES, + MAX_CONFIG_BYTES, ); return snapshot ? parseKiroCliV2Document(paths.v2Path, snapshot.contents) @@ -47,7 +46,7 @@ async function readV3Document(paths: KiroCliPaths): Promise { const snapshot = await readPhysicalFile( paths.v3Path, 'Kiro CLI v3 global hooks', - MAX_HOOK_BYTES, + MAX_CONFIG_BYTES, ); return snapshot ? parseKiroIdeDocument(paths.v3Path, snapshot.contents, 'Kiro CLI v3 global hooks') diff --git a/src/plugins/kiroide-command.ts b/src/plugins/kiroide-command.ts index 02d3187..ecde59f 100644 --- a/src/plugins/kiroide-command.ts +++ b/src/plugins/kiroide-command.ts @@ -1,132 +1,20 @@ -import os from 'node:os'; import path from 'node:path'; +import { managedScriptReference, type ManagedScriptReference } from './common.js'; +import { + encodedWindowsCommand, + isNodeExecutable, + parseEncodedWindowsCommand, + parsePosixCommand, + posixSource, +} from './shell-command.js'; -interface ParsedArgument { - readonly value: string; - readonly next: number; -} - -export interface KiroIdeRuntimeReference { - readonly agentId: string; - readonly scriptPath: string; -} - -export function sameKiroIdePath(left: string, right: string): boolean { - const normalizedLeft = path.resolve(left); - const normalizedRight = path.resolve(right); - return process.platform === 'win32' - ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() - : normalizedLeft === normalizedRight; -} - -export function sameKiroIdeAgentId(left: string, right: string): boolean { - return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right; -} - -function quotePosix(value: string): string { - return `'${value.replaceAll("'", `'"'"'`)}'`; -} - -function quotePowerShell(value: string): string { - return `'${value.replaceAll("'", "''")}'`; -} - -function windowsPowerShellPath(): string { - const configuredRoot = process.env.SystemRoot; - const systemRoot = configuredRoot - && path.win32.isAbsolute(configuredRoot) - && !/["%\r\n]/.test(configuredRoot) - ? configuredRoot - : 'C:\\Windows'; - return path.win32.join( - systemRoot, - 'System32', - 'WindowsPowerShell', - 'v1.0', - 'powershell.exe', - ); -} - -function windowsCommand(scriptPath: string): string { - const source = `& ${quotePowerShell(process.execPath)} ${quotePowerShell(scriptPath)}; exit $LASTEXITCODE`; - const encoded = Buffer.from(source, 'utf16le').toString('base64'); - return `"${windowsPowerShellPath()}" -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encoded}`; -} +export type KiroIdeRuntimeReference = ManagedScriptReference; export function buildKiroIdeCommand(scriptPath: string): string { if (!path.isAbsolute(process.execPath) || !path.isAbsolute(scriptPath)) { throw new Error('Kiro IDE hook commands require absolute executable and script paths'); } - return process.platform === 'win32' - ? windowsCommand(scriptPath) - : `${quotePosix(process.execPath)} ${quotePosix(scriptPath)}`; -} - -function readPosixArgument(command: string, start: number): ParsedArgument | undefined { - if (command[start] !== "'") return undefined; - const apostrophe = `'"'"'`; - let value = ''; - for (let index = start + 1; index < command.length;) { - if (command.startsWith(apostrophe, index)) { - value += "'"; - index += apostrophe.length; - continue; - } - if (command[index] === "'") return { value, next: index + 1 }; - value += command[index]; - index += 1; - } - return undefined; -} - -function parsePosixCommand(command: string): readonly [string, string] | undefined { - const executable = readPosixArgument(command, 0); - if (!executable || command[executable.next] !== ' ') return undefined; - const script = readPosixArgument(command, executable.next + 1); - return script && script.next === command.length ? [executable.value, script.value] : undefined; -} - -function readPowerShellArgument(command: string, start: number): ParsedArgument | undefined { - if (command[start] !== "'") return undefined; - let value = ''; - for (let index = start + 1; index < command.length; index += 1) { - if (command[index] !== "'") { - value += command[index]; - continue; - } - if (command[index + 1] === "'") { - value += "'"; - index += 1; - continue; - } - return { value, next: index + 1 }; - } - return undefined; -} - -function parsePowerShellSource(source: string): readonly [string, string] | undefined { - if (!source.startsWith('& ')) return undefined; - const executable = readPowerShellArgument(source, 2); - if (!executable || source[executable.next] !== ' ') return undefined; - const script = readPowerShellArgument(source, executable.next + 1); - return script && source.slice(script.next) === '; exit $LASTEXITCODE' - ? [executable.value, script.value] - : undefined; -} - -function parseWindowsCommand(command: string): readonly [string, string] | undefined { - const match = /^"([^"\r\n]+)" -NoLogo -NoProfile -NonInteractive -EncodedCommand ([A-Za-z0-9+/]+={0,2})$/.exec(command); - if (!match - || !path.win32.isAbsolute(match[1]) - || path.win32.basename(match[1]).toLowerCase() !== 'powershell.exe') return undefined; - const buffer = Buffer.from(match[2], 'base64'); - if (buffer.length % 2 !== 0 || buffer.toString('base64') !== match[2]) return undefined; - return parsePowerShellSource(buffer.toString('utf16le')); -} - -function isNodeExecutable(filePath: string): boolean { - const basename = path.basename(filePath).toLowerCase(); - return basename === 'node' || basename === 'node.exe'; + return process.platform === 'win32' ? encodedWindowsCommand(scriptPath) : posixSource(scriptPath); } export function kiroIdeRuntimeReference( @@ -134,19 +22,8 @@ export function kiroIdeRuntimeReference( scriptName: string, ): KiroIdeRuntimeReference | undefined { const parsed = process.platform === 'win32' - ? parseWindowsCommand(command) + ? parseEncodedWindowsCommand(command) : parsePosixCommand(command); - if (!parsed - || !path.isAbsolute(parsed[0]) - || !isNodeExecutable(parsed[0]) - || !path.isAbsolute(parsed[1]) - || path.basename(parsed[1]) !== scriptName) return undefined; - const agentDirectory = path.dirname(parsed[1]); - if (!sameKiroIdePath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))) { - return undefined; - } - const agentId = path.basename(agentDirectory); - return agentId && agentId !== '.' && agentId !== '..' - ? { agentId, scriptPath: parsed[1] } - : undefined; + if (!parsed || !path.isAbsolute(parsed[0]) || !isNodeExecutable(parsed[0])) return undefined; + return managedScriptReference(parsed[1], scriptName); } diff --git a/src/plugins/kiroide-contract.ts b/src/plugins/kiroide-contract.ts index ca84478..fc1f66d 100644 --- a/src/plugins/kiroide-contract.ts +++ b/src/plugins/kiroide-contract.ts @@ -1,10 +1,9 @@ import os from 'node:os'; import path from 'node:path'; +import { managedScriptReference, sameAgentId } from './common.js'; import { buildKiroIdeCommand, kiroIdeRuntimeReference, - sameKiroIdeAgentId, - sameKiroIdePath, type KiroIdeRuntimeReference, } from './kiroide-command.js'; import { isObject, parseStrictJsonObject, type JsonObject } from './strict-json.js'; @@ -154,7 +153,7 @@ export function createKiroIdeDocument(filePath: string): KiroIdeDocument { return { exists: false, filePath, root: {}, hooks: [] }; } -function exactKeys(value: JsonObject, keys: readonly string[]): boolean { +export function exactKeys(value: JsonObject, keys: readonly string[]): boolean { return Object.keys(value).sort().join('|') === [...keys].sort().join('|'); } @@ -228,7 +227,7 @@ export function withoutManagedKiroIdeHooks( ): KiroIdeHook[] { return hooks.filter((hook) => { const reference = ownedReference(hook); - return !reference || (agentId !== undefined && !sameKiroIdeAgentId(reference.agentId, agentId)); + return !reference || (agentId !== undefined && !sameAgentId(reference.agentId, agentId)); }); } @@ -285,12 +284,7 @@ export function kiroIdeRuntimeContracts( function legacyReference(command: unknown, scriptName: string) { if (typeof command !== 'string') return undefined; const match = /^node "([^"\r\n]+)"$/.exec(command); - if (!match || !path.isAbsolute(match[1]) || path.basename(match[1]) !== scriptName) return undefined; - const agentDirectory = path.dirname(match[1]); - if (!sameKiroIdePath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))) { - return undefined; - } - return { agentId: path.basename(agentDirectory), scriptPath: match[1] }; + return match ? managedScriptReference(match[1], scriptName) : undefined; } export function legacyKiroIdeRuntimeContract(raw: string, filePath: string): KiroIdeRuntimeContract | undefined { @@ -311,6 +305,6 @@ export function legacyKiroIdeRuntimeContract(raw: string, filePath: string): Kir || auditHook.timeout_ms !== 5000) return undefined; const guard = legacyReference(guardHook.command, GUARD_SCRIPT); const audit = legacyReference(auditHook.command, AUDIT_SCRIPT); - if (!guard || !audit || !sameKiroIdeAgentId(guard.agentId, audit.agentId)) return undefined; + if (!guard || !audit || !sameAgentId(guard.agentId, audit.agentId)) return undefined; return { agentId: guard.agentId, guardPath: guard.scriptPath, auditPath: audit.scriptPath }; } diff --git a/src/plugins/kiroide-installation.ts b/src/plugins/kiroide-installation.ts index e9eccc4..791f662 100644 --- a/src/plugins/kiroide-installation.ts +++ b/src/plugins/kiroide-installation.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import type { InstallConfig } from './base.js'; -import { sameKiroIdeAgentId } from './kiroide-command.js'; +import { sameAgentId } from './common.js'; import { AGENT_KEY, AUDIT_SCRIPT, @@ -31,17 +31,13 @@ import { const DISPLAY_NAME = 'Kiro IDE'; const DIRECTORY_LABEL = 'Kiro IDE hooks directory'; -export type KiroIdeRuntimePaths = ManagedRuntimePaths; -export type PreparedKiroIdeInstallation = PreparedManagedInstallation; -export type { RenameFile }; - function hookLocation(paths: KiroIdePaths) { return [{ directoryLabel: DIRECTORY_LABEL, filePath: paths.configPath }]; } function removesLegacy(legacy: LegacyKiroIdeDocument, agentId?: string): boolean { return legacy.contract !== undefined - && (agentId === undefined || sameKiroIdeAgentId(legacy.contract.agentId, agentId)); + && (agentId === undefined || sameAgentId(legacy.contract.agentId, agentId)); } async function prepareLegacyRemoval( @@ -64,7 +60,7 @@ async function prepareLegacyRemoval( export async function preflightKiroIdeInstallation( config: InstallConfig, paths: KiroIdePaths, -): Promise { +): Promise { return preflightManagedInstallation({ agentKey: AGENT_KEY, hookLocations: hookLocation(paths), @@ -76,7 +72,7 @@ export async function prepareKiroIdeInstallation( config: InstallConfig, sources: KiroIdeSources, rendered: RenderedKiroIdeDocument, -): Promise { +): Promise { if (rendered.next === undefined) throw new Error('Kiro IDE installation requires a hook document'); const prepared = await prepareManagedInstallation({ agentKey: AGENT_KEY, @@ -107,7 +103,7 @@ export async function prepareKiroIdeInstallation( } export async function commitKiroIdeInstallation( - prepared: PreparedKiroIdeInstallation, + prepared: PreparedManagedInstallation, renameFile?: RenameFile, ): Promise { await commitManagedInstallation(prepared, renameFile); diff --git a/src/plugins/opencode-contract.ts b/src/plugins/opencode-contract.ts index fa02c9a..d3b4e2b 100644 --- a/src/plugins/opencode-contract.ts +++ b/src/plugins/opencode-contract.ts @@ -1,7 +1,8 @@ import os from 'node:os'; import path from 'node:path'; import type { HookScriptOptions } from './hook-template.js'; -import { samePath } from './common.js'; +import { errorMessage, samePath } from './common.js'; +import { isNodeExecutable } from './shell-command.js'; import { parseStrictJsonObject } from './strict-json.js'; export const AGENT_KEY = 'opencode'; @@ -51,10 +52,6 @@ interface OpenCodePluginMetadata { readonly auditPath: string; } -export function sameOpenCodeAgentId(left: string, right: string): boolean { - return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right; -} - export function resolveOpenCodePaths( homeDirectory = os.homedir(), xdgConfigHome = process.env.XDG_CONFIG_HOME, @@ -83,11 +80,6 @@ function validAgentSegment(agentId: string): boolean { && path.basename(agentId) === agentId; } -function isNodeExecutable(filePath: string): boolean { - const basename = path.basename(filePath).toLowerCase(); - return basename === 'node' || basename === 'node.exe'; -} - function validateRuntimePaths(metadata: OpenCodePluginMetadata): void { if (!validAgentSegment(metadata.agentId)) { throw new Error('OpenCode plugin metadata contains an invalid agentId'); @@ -287,10 +279,6 @@ export const ElydoraAuditPlugin = async () => ({ `); } -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - export function parseOpenCodePlugin( filePath: string, source: string, diff --git a/src/plugins/opencode-installation.ts b/src/plugins/opencode-installation.ts index 2e0c0e9..ad77700 100644 --- a/src/plugins/opencode-installation.ts +++ b/src/plugins/opencode-installation.ts @@ -7,8 +7,8 @@ import { OPENCODE_AUDIT_OPTIONS, buildOpenCodeMetadata, buildOpenCodePlugin, - sameOpenCodeAgentId, } from './opencode-contract.js'; +import { sameAgentId } from './common.js'; import type { OpenCodePluginFile, OpenCodeSources } from './opencode-io.js'; import { commitManagedInstallation, @@ -29,14 +29,10 @@ import { const DISPLAY_NAME = 'OpenCode'; const DIRECTORY_LABEL = 'OpenCode plugins directory'; -export type OpenCodeRuntimePaths = ManagedRuntimePaths; -export type PreparedOpenCodeInstallation = PreparedManagedInstallation; -export type { RenameFile }; - export async function preflightOpenCodeInstallation( config: InstallConfig, sources: OpenCodeSources, -): Promise { +): Promise { return preflightManagedInstallation({ agentKey: AGENT_KEY, hookLocations: [{ @@ -50,7 +46,7 @@ export async function preflightOpenCodeInstallation( export async function prepareOpenCodeInstallation( config: InstallConfig, sources: OpenCodeSources, -): Promise { +): Promise { const paths = await preflightOpenCodeInstallation(config, sources); const metadata = buildOpenCodeMetadata( config.agentId, @@ -85,7 +81,7 @@ export async function prepareOpenCodeInstallation( } export async function commitOpenCodeInstallation( - prepared: PreparedOpenCodeInstallation, + prepared: PreparedManagedInstallation, renameFile?: RenameFile, ): Promise { await commitManagedInstallation(prepared, renameFile); @@ -96,7 +92,7 @@ function ownedByAgent( agentId?: string, ): boolean { return file.contract !== undefined - && (agentId === undefined || sameOpenCodeAgentId(file.contract.agentId, agentId)); + && (agentId === undefined || sameAgentId(file.contract.agentId, agentId)); } export async function prepareOpenCodeUninstall( diff --git a/src/plugins/opencode-io.ts b/src/plugins/opencode-io.ts index e0f1617..9c4f463 100644 --- a/src/plugins/opencode-io.ts +++ b/src/plugins/opencode-io.ts @@ -9,11 +9,10 @@ import { type OpenCodePaths, type OpenCodeRuntimeContract, } from './opencode-contract.js'; +import { errorMessage, hasCode, MAX_CONFIG_BYTES } from './common.js'; import { inspectPhysicalDirectory, readPhysicalFile, type FileSnapshot } from './managed-files.js'; import { managedRuntimeFilesExist } from './managed-runtime-status.js'; -const MAX_PLUGIN_BYTES = 512 * 1024; - export interface OpenCodePluginFile { readonly exists: boolean; readonly filePath: string; @@ -40,7 +39,7 @@ async function readPluginFile( label: string, parse: (filePath: string, source: string) => TContract | undefined, ): Promise> { - const snapshot = await readPhysicalFile(filePath, label, MAX_PLUGIN_BYTES); + const snapshot = await readPhysicalFile(filePath, label, MAX_CONFIG_BYTES); if (!snapshot) return { exists: false, filePath }; return { exists: true, @@ -73,18 +72,14 @@ export function requireAvailableOpenCodePlugin( } } -function hasCode(error: unknown, code: string): boolean { - return typeof error === 'object' && error !== null && 'code' in error && error.code === code; -} - async function executableExists(filePath: string): Promise { try { return (await fsp.stat(filePath)).isFile(); } catch (error) { if (hasCode(error, 'ENOENT') || hasCode(error, 'ENOTDIR')) return false; - throw new Error(`Inspect OpenCode Node.js runtime at ${filePath}: ${ - error instanceof Error ? error.message : String(error) - }`, { cause: error instanceof Error ? error : new Error(String(error)) }); + throw new Error(`Inspect OpenCode Node.js runtime at ${filePath}: ${errorMessage(error)}`, { + cause: error instanceof Error ? error : new Error(String(error)), + }); } }