Files
ditto.site/.opencode/tools/capture.ts
T
2026-06-29 15:11:48 -07:00

98 lines
4.4 KiB
TypeScript

import { tool } from '@opencode-ai/plugin';
export default tool({
description:
'Run the Playwright-based site capture pipeline against a target URL. Produces a full capture bundle (DOM + computed styles per scroll position, screenshots per viewport, HAR, animation library dumps, shaders, CSS rules + url() asset refs, fonts, per-section cropped screenshots, alt-height pass for vh detection) at output_dir. Wraps scripts/capture.py. Set replay=true to skip capture entirely and only re-run the post-process (vh-flags + meta.json) against an existing capture bundle — use this when iterating on prompts/agents/skills.',
args: {
url: tool.schema
.string()
.nullable()
.describe('Absolute target URL, e.g. https://example.com. Required unless replay=true.'),
viewports: tool.schema
.array(tool.schema.number())
.nullable()
.describe('Viewport widths in pixels to capture, e.g. [375, 768, 1280, 1920]. Default [375,768,1280,1920].'),
output_dir: tool.schema
.string()
.describe('Directory where the capture bundle is written (absolute path or relative to cwd)'),
wait_strategy: tool.schema
.enum(['networkidle', 'load', 'domcontentloaded'])
.nullable()
.describe('Playwright wait strategy for navigation. Default networkidle.'),
skip_third_party: tool.schema
.boolean()
.nullable()
.describe('If true, blocks requests to known third-party widget domains. Default true.'),
replay: tool.schema
.boolean()
.nullable()
.describe(
'If true, skip browser capture entirely and only re-run post-process (vh-flags + meta.json) against existing capture data in output_dir. ~100x faster than a real capture.'
),
skip_alt_height: tool.schema
.boolean()
.nullable()
.describe('If true, skip the vh-detection alt-height pass at canonical width. Saves ~10s.'),
skip_section_shots: tool.schema
.boolean()
.nullable()
.describe('If true, skip the per-section cropped screenshot pass. Saves ~30s.'),
},
async execute(args, context) {
const { spawn } = await import('node:child_process');
const { resolve } = await import('node:path');
const { readFileSync, existsSync } = await import('node:fs');
const { resolvePython } = await import('./_python.js');
const scriptPath = resolve(context.worktree, '.opencode/scripts/capture.py');
const outputDir = resolve(context.worktree, args.output_dir);
const python = resolvePython(context.worktree);
const replay = args.replay ?? false;
if (!replay && !args.url) {
throw new Error('capture: url is required unless replay=true');
}
const viewports = args.viewports ?? [375, 768, 1280, 1920];
const cmdArgs = [scriptPath, '--output', outputDir];
if (args.url) cmdArgs.push('--url', args.url);
if (viewports.length) cmdArgs.push('--viewports', viewports.join(','));
cmdArgs.push('--wait-strategy', args.wait_strategy ?? 'networkidle');
if (args.skip_third_party ?? true) cmdArgs.push('--skip-third-party');
if (replay) cmdArgs.push('--replay');
if (args.skip_alt_height ?? false) cmdArgs.push('--skip-alt-height');
if (args.skip_section_shots ?? false) cmdArgs.push('--skip-section-shots');
await new Promise<void>((resolvePromise, reject) => {
const child = spawn(python, cmdArgs, { cwd: context.worktree, stdio: 'inherit' });
child.on('exit', (code) => {
if (code === 0) resolvePromise();
else reject(new Error(`capture.py exited with code ${code}`));
});
child.on('error', reject);
});
const metaPath = resolve(outputDir, 'meta.json');
if (!existsSync(metaPath)) {
throw new Error(`capture.py did not produce ${metaPath}`);
}
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
return JSON.stringify({
status: 'success',
mode: replay ? 'replay' : 'capture',
capture_dir: outputDir,
viewports: meta.viewports ?? viewports,
dom_snapshots: meta.dom_snapshots ?? 0,
screenshots: meta.screenshots ?? 0,
section_shots: meta.section_shots ?? 0,
assets_discovered: Array.isArray(meta.assets) ? meta.assets.length : 0,
asset_sources: meta.asset_sources ?? null,
libs_detected: meta.libs_detected ?? [],
vh_relative_count: meta.vh_relative_count ?? 0,
canvas_regions: meta.canvas_regions ?? 0,
});
},
});