UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

129 lines 5.01 kB
/** * Codex hook translator — emits TOML hook entries for ~/.codex/config.toml. * * Per ADR-3 §5 backup-and-rollback: the operator-managed config file * (~/.codex/config.toml) is read, AIWG-tagged entries are merged in * (via _aiwg_managed: true), and the file is written atomically with a * timestamped backup. * * Codex hook events map per the assessment matrix; this translator * supports a conservative subset known to exist in the loader. */ import { promises as fs } from 'node:fs'; import * as path from 'node:path'; import { homedir } from 'node:os'; import { substituteEnvVars } from './shim.js'; /** Codex-native event names (subset; extend when verified against codex-rs). */ const CODEX_EVENT_MAP = { PreToolUse: 'before_tool', PostToolUse: 'after_tool', UserPromptSubmit: 'before_prompt', SessionStart: 'session_start', SessionEnd: 'session_end', Stop: 'session_end', }; /** * Render a single TOML hook entry for a Codex config block. * * Codex's hook TOML shape (per the assessment): * [[hooks.<event>]] * command = "..." * args = ["..."] * _aiwg_managed = true * * Operators add their own hooks at the same shape; the _aiwg_managed flag * lets `aiwg remove --addon aiwg-hooks` distinguish AIWG entries from * operator-authored ones. */ export function renderCodexHookToml(source) { const lines = []; for (const event of source.events) { const codexEvent = CODEX_EVENT_MAP[event]; if (!codexEvent) continue; const subCommand = substituteEnvVars(source.command, 'codex'); const subArgs = (source.args || []).map((a) => substituteEnvVars(a, 'codex')); lines.push(`[[hooks.${codexEvent}]]`); lines.push(`# AIWG-managed hook: ${source.id} — ${source.description}`); lines.push(`command = "${subCommand.replace(/"/g, '\\"')}"`); if (subArgs.length > 0) { lines.push(`args = [${subArgs.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ')}]`); } if (source.safetyCritical) { lines.push(`# safety-critical: cannot be auto-removed without --allow-unsafe-shadow`); } lines.push(`_aiwg_managed = true`); lines.push(`_aiwg_id = "${source.id}"`); lines.push(''); } return lines.join('\n'); } const SPILLOVER_START = '# >>> AIWG-managed hooks (do not edit between markers)'; const SPILLOVER_END = '# <<< AIWG-managed hooks'; /** * Inject AIWG-managed hook block into existing TOML config, preserving * everything outside the markers. Idempotent — replaces an existing block * rather than appending. */ export function injectHookBlock(existingToml, hookBlock) { const startIdx = existingToml.indexOf(SPILLOVER_START); const endIdx = existingToml.indexOf(SPILLOVER_END); const aiwgSection = `${SPILLOVER_START}\n${hookBlock}\n${SPILLOVER_END}\n`; if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) { const trimmed = existingToml.replace(/\n+$/, ''); return (trimmed.length > 0 ? trimmed + '\n\n' : '') + aiwgSection; } const before = existingToml.slice(0, startIdx); const after = existingToml.slice(endIdx + SPILLOVER_END.length).replace(/^\n+/, '\n'); return `${before}${aiwgSection}${after}`; } /** * Translate one HookSource to Codex TOML config. * * Writes to ~/.codex/config.toml (user-global), backing up the existing * file if no AIWG signature is found per ADR-3 §5. */ export async function translateForCodex(source, options) { const result = { provider: 'codex', emittedPaths: [], warnings: [], skipped: false, }; if (source.degradeOn?.includes('codex')) { result.skipped = true; result.skipReason = 'degrade-on declared codex'; return result; } const configPath = path.join(homedir(), '.codex', 'config.toml'); const block = renderCodexHookToml(source); if (block.trim().length === 0) { result.skipped = true; result.skipReason = 'no Codex-mapped events'; return result; } if (options.dryRun) { result.emittedPaths.push(configPath + ' (dry-run)'); return result; } let existing = ''; try { existing = await fs.readFile(configPath, 'utf8'); } catch (err) { if (err.code !== 'ENOENT') throw err; } // Backup-and-rollback per ADR-3 §5. if (existing && !existing.includes(SPILLOVER_START)) { const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`; await fs.writeFile(backupPath, existing, 'utf8'); result.warnings.push(`Backed up pre-existing config to ${backupPath}`); } const updated = injectHookBlock(existing, block); await fs.mkdir(path.dirname(configPath), { recursive: true }); await fs.writeFile(configPath, updated, 'utf8'); result.emittedPaths.push(configPath); return result; } //# sourceMappingURL=codex-translator.js.map