/** * Clipboard via OSC 52. * * OSC 52 (`\x1b]52;c;\x07`) is an ANSI sequence that asks * the terminal emulator to set the system clipboard. It works in * iTerm2, kitty, Alacritty, WezTerm, modern Windows Terminal, and * tunnels naturally over SSH (the local terminal does the work, so * we don't need a remote helper). macOS Terminal.app needs the * "Allow applications on this Mac to access the clipboard" toggle. * * `c` selects the system clipboard (vs. `p` for primary selection * on X11). BEL (`\x07`) terminates the sequence — works in every * common terminal; some also accept ST (`\x1b\\`). */ import type { DriftFinding } from '../../services/audit/types'; import type { CommandLogEntry } from './audit-state'; /** Base64-encode UTF-8 input — Node/Bun's Buffer is the simplest path. */ function base64(text: string): string { return Buffer.from(text, 'utf8').toString('base64'); } /** * Strip ANSI escape sequences (CSI color/style codes, OSC strings, * single-char escapes). Module output streamed into the command log * keeps its colors so the in-TUI render stays pretty, but pasting * those into a chat or issue tracker is just noise. * * Built from a string at runtime so biome's "control character in * regex" lint doesn't fire on a literal `\x1b` in source. */ const ESC = '\x1b'; const ANSI_PATTERN = new RegExp( // CSI: ESC [ ... letter, e.g. \x1b[34m, \x1b[2;3H `${ESC}\\[[0-9;?]*[A-Za-z]|` + // OSC: ESC ] ... BEL or ST `${ESC}\\][^${ESC}\\x07]*(?:${ESC}\\\\|\\x07)|` + // Single-char escapes `${ESC}[@-Z\\\\-_]`, 'g', ); function stripAnsi(text: string): string { return text.replace(ANSI_PATTERN, ''); } /** * Copy `text` to the system clipboard via OSC 52. * * Writes directly to stdout — Ink's renderer doesn't mind extra * escape sequences interleaved between its frames. The terminal * intercepts OSC 52 before it reaches the Ink output. */ export function copyToClipboard(text: string): void { const seq = `\x1b]52;c;${base64(text)}\x07`; process.stdout.write(seq); } /** * Format a finding as plain text suitable for pasting into a chat, * issue tracker, or notes app. Mirrors the Detail pane's structure. * Strips ANSI codes from any field that might carry them (terraform * stderr in `details`, etc.) so the paste is clean text. */ export function formatFindingForClipboard(f: DriftFinding): string { const lines: string[] = [stripAnsi(f.message)]; if (f.details) { lines.push('', stripAnsi(f.details)); } if (f.remediation) { lines.push('', `→ ${stripAnsi(f.remediation)}`); } lines.push('', `${f.subject} · ${f.code} (${f.category}/${f.severity})`); return lines.join('\n'); } /** * Format the entire command log as plain text suitable for pasting. * Mirrors the Command-log pane's rendering — separator headers, * stdout/stderr lines (`[err]` prefix on stderr to disambiguate * once colors are gone), and exit-code footers. ANSI escape codes * in streamed module output are stripped — they're useful in the * TUI but render as garbage when pasted elsewhere. */ export function formatCommandLogForClipboard(log: CommandLogEntry[]): string { if (log.length === 0) return '(empty)'; const lines: string[] = []; for (const entry of log) { const time = new Date(entry.startedAt).toISOString(); lines.push(`─── ${time} · ${entry.cmd} ───`); for (const ln of entry.lines) { const clean = stripAnsi(ln.text); lines.push(ln.stream === 'stderr' ? `[err] ${clean}` : clean); } lines.push(entry.exitCode === null ? 'running…' : `exit ${entry.exitCode}`); lines.push(''); } // Drop the trailing blank line. if (lines[lines.length - 1] === '') lines.pop(); return lines.join('\n'); }