import type { Snippet } from '@/domain/models'; import { isScriptSnippet, scriptContainsWriteOperations } from '@/domain/snippetScript.ts'; import { localStorageAdapter } from '@/infrastructure/persistence/localStorageAdapter.ts'; import { STORAGE_KEY_AI_PERMISSION_MODE } from '@/infrastructure/config/storageKeys.ts'; import type { AIPermissionMode } from '@/infrastructure/ai/types.ts'; import { netcattyBridge } from '@/infrastructure/services/netcattyBridge.ts'; import type { ScriptRun } from '@/types/global/netcatty-bridge-script.d.ts'; type RunsListener = (runs: ScriptRun[]) => void; let runs: ScriptRun[] = []; const runsListeners = new Set(); function readPermissionMode(): AIPermissionMode { const stored = localStorageAdapter.readString(STORAGE_KEY_AI_PERMISSION_MODE); if (stored === 'observer' || stored === 'confirm' || stored === 'auto') return stored; return 'confirm'; } export function subscribeScriptRuns(listener: RunsListener): () => void { runsListeners.add(listener); queueMicrotask(() => { if (runsListeners.has(listener)) { listener(runs); } }); return () => runsListeners.delete(listener); } export function setScriptRuns(nextRuns: ScriptRun[]) { runs = nextRuns; runsListeners.forEach((listener) => listener(runs)); } export function getActiveScriptRunForSession(sessionId: string): ScriptRun | undefined { return runs.find((run) => run.sessionId === sessionId && (run.status === 'running' || run.status === 'paused'), ); } export async function runAutomationScript(params: { snippet: Snippet; sessionId: string; sessionIds?: string[]; mode?: 'sequential' | 'parallel'; sessionMeta?: { connected?: boolean; hostname?: string; username?: string; }; }): Promise<{ runId: string; runIds: string[] }> { const permissionMode = readPermissionMode(); if (permissionMode === 'observer' && scriptContainsWriteOperations(params.snippet.command)) { throw new Error('Observer mode blocks scripts that write to the terminal.'); } const bridge = netcattyBridge.get(); if (!bridge?.scriptRun) { throw new Error('Script bridge unavailable'); } return bridge.scriptRun({ scriptId: params.snippet.id, scriptLabel: params.snippet.label, content: params.snippet.command, sessionId: params.sessionId, sessionIds: params.sessionIds, mode: params.mode, permissionMode, sessionMeta: params.sessionMeta, }); } const TERMINAL_SCRIPT_STATUSES = new Set(['completed', 'failed']); export function waitForScriptRun( runId: string, options: { signal?: AbortSignal; timeoutMs?: number } = {}, ): Promise { const existing = runs.find((entry) => entry.runId === runId); if (existing && TERMINAL_SCRIPT_STATUSES.has(existing.status)) { if (existing.status === 'completed') { return Promise.resolve(existing); } return Promise.reject(new Error(existing.error || 'Script failed')); } const timeoutMs = options.timeoutMs ?? 3_600_000; return new Promise((resolve, reject) => { let settled = false; let timeoutId: ReturnType | undefined; let unsubscribe: () => void = () => {}; const finish = (handler: () => void) => { if (settled) return; settled = true; if (timeoutId !== undefined) { clearTimeout(timeoutId); } unsubscribe(); options.signal?.removeEventListener('abort', onAbort); handler(); }; const onAbort = () => { finish(() => reject(new Error('Aborted'))); }; const settleRun = (run: ScriptRun | undefined) => { if (!run || !TERMINAL_SCRIPT_STATUSES.has(run.status)) return; if (run.status === 'completed') { finish(() => resolve(run)); return; } finish(() => reject(new Error(run.error || 'Script failed'))); }; unsubscribe = subscribeScriptRuns((currentRuns) => { settleRun(currentRuns.find((entry) => entry.runId === runId)); }); timeoutId = setTimeout(() => { finish(() => reject(new Error('Script run timed out'))); }, timeoutMs); options.signal?.addEventListener('abort', onAbort, { once: true }); }); } export async function runConnectScriptsSequential(params: { scripts: Snippet[]; sessionId: string; signal?: AbortSignal; onScriptStart?: (snippet: Snippet) => void; onScriptComplete?: (snippet: Snippet) => void; sessionMeta?: { connected?: boolean; hostname?: string; username?: string; }; }): Promise { for (const snippet of params.scripts) { if (params.signal?.aborted) { throw new Error('Aborted'); } params.onScriptStart?.(snippet); const { runId } = await runAutomationScript({ snippet, sessionId: params.sessionId, sessionMeta: params.sessionMeta, }); await waitForScriptRun(runId, { signal: params.signal }); params.onScriptComplete?.(snippet); } } export async function runSnippetOrScript(params: { snippet: Snippet; sessionId: string; runSnippetText: ( command: string, noAutoRun?: boolean, options?: { multiLineRunMode?: Snippet["multiLineRunMode"] }, ) => void; command: string; }) { if (isScriptSnippet(params.snippet)) { await runAutomationScript({ snippet: params.snippet, sessionId: params.sessionId, }); return; } params.runSnippetText(params.command, params.snippet.noAutoRun, { multiLineRunMode: params.snippet.multiLineRunMode, }); } export async function stopScriptRun(runId: string): Promise<{ ok: boolean }> { const result = await netcattyBridge.get()?.scriptStop?.(runId); return { ok: result?.ok !== false }; } export async function pauseScriptRun(runId: string): Promise<{ ok: boolean }> { const result = await netcattyBridge.get()?.scriptPause?.(runId); return { ok: result?.ok !== false }; } export async function resumeScriptRun(runId: string): Promise<{ ok: boolean }> { const result = await netcattyBridge.get()?.scriptResume?.(runId); return { ok: result?.ok !== false }; }