mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
refactor: extract command registry to commands.ts, add SNAPSHOT_FLAGS metadata
- NEW: browse/src/commands.ts — command sets + COMMAND_DESCRIPTIONS + load-time validation (zero side effects) - server.ts imports from commands.ts instead of declaring sets inline - snapshot.ts: SNAPSHOT_FLAGS array drives parseSnapshotArgs (metadata-driven, no duplication) - All 186 existing tests pass
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Command registry — single source of truth for all browse commands.
|
||||
*
|
||||
* Dependency graph:
|
||||
* commands.ts ──▶ server.ts (runtime dispatch)
|
||||
* ──▶ gen-skill-docs.ts (doc generation)
|
||||
* ──▶ skill-parser.ts (validation)
|
||||
* ──▶ skill-check.ts (health reporting)
|
||||
*
|
||||
* Zero side effects. Safe to import from build scripts and tests.
|
||||
*/
|
||||
|
||||
export const READ_COMMANDS = new Set([
|
||||
'text', 'html', 'links', 'forms', 'accessibility',
|
||||
'js', 'eval', 'css', 'attrs',
|
||||
'console', 'network', 'cookies', 'storage', 'perf',
|
||||
'dialog', 'is',
|
||||
]);
|
||||
|
||||
export const WRITE_COMMANDS = new Set([
|
||||
'goto', 'back', 'forward', 'reload',
|
||||
'click', 'fill', 'select', 'hover', 'type', 'press', 'scroll', 'wait',
|
||||
'viewport', 'cookie', 'cookie-import', 'cookie-import-browser', 'header', 'useragent',
|
||||
'upload', 'dialog-accept', 'dialog-dismiss',
|
||||
]);
|
||||
|
||||
export const META_COMMANDS = new Set([
|
||||
'tabs', 'tab', 'newtab', 'closetab',
|
||||
'status', 'stop', 'restart',
|
||||
'screenshot', 'pdf', 'responsive',
|
||||
'chain', 'diff',
|
||||
'url', 'snapshot',
|
||||
]);
|
||||
|
||||
export const ALL_COMMANDS = new Set([...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS]);
|
||||
|
||||
export const COMMAND_DESCRIPTIONS: Record<string, { category: string; description: string; usage?: string }> = {
|
||||
// Navigation
|
||||
'goto': { category: 'Navigation', description: 'Navigate to URL', usage: 'goto <url>' },
|
||||
'back': { category: 'Navigation', description: 'History back' },
|
||||
'forward': { category: 'Navigation', description: 'History forward' },
|
||||
'reload': { category: 'Navigation', description: 'Reload page' },
|
||||
'url': { category: 'Navigation', description: 'Print current URL' },
|
||||
// Reading
|
||||
'text': { category: 'Reading', description: 'Cleaned page text' },
|
||||
'html': { category: 'Reading', description: 'innerHTML', usage: 'html [selector]' },
|
||||
'links': { category: 'Reading', description: 'All links as "text → href"' },
|
||||
'forms': { category: 'Reading', description: 'Form fields as JSON' },
|
||||
'accessibility': { category: 'Reading', description: 'Full ARIA tree' },
|
||||
// Inspection
|
||||
'js': { category: 'Inspection', description: 'Run JavaScript', usage: 'js <expr>' },
|
||||
'eval': { category: 'Inspection', description: 'Run JS file', usage: 'eval <file>' },
|
||||
'css': { category: 'Inspection', description: 'Computed CSS value', usage: 'css <sel> <prop>' },
|
||||
'attrs': { category: 'Inspection', description: 'Element attributes as JSON', usage: 'attrs <sel>' },
|
||||
'is': { category: 'Inspection', description: 'State check', usage: 'is <prop> <sel>' },
|
||||
'console': { category: 'Inspection', description: 'Console messages', usage: 'console [--clear|--errors]' },
|
||||
'network': { category: 'Inspection', description: 'Network requests', usage: 'network [--clear]' },
|
||||
'dialog': { category: 'Inspection', description: 'Dialog messages', usage: 'dialog [--clear]' },
|
||||
'cookies': { category: 'Inspection', description: 'All cookies as JSON' },
|
||||
'storage': { category: 'Inspection', description: 'localStorage + sessionStorage', usage: 'storage [set k v]' },
|
||||
'perf': { category: 'Inspection', description: 'Page load timings' },
|
||||
// Interaction
|
||||
'click': { category: 'Interaction', description: 'Click element', usage: 'click <sel>' },
|
||||
'fill': { category: 'Interaction', description: 'Fill input', usage: 'fill <sel> <val>' },
|
||||
'select': { category: 'Interaction', description: 'Select dropdown option', usage: 'select <sel> <val>' },
|
||||
'hover': { category: 'Interaction', description: 'Hover element', usage: 'hover <sel>' },
|
||||
'type': { category: 'Interaction', description: 'Type into focused element', usage: 'type <text>' },
|
||||
'press': { category: 'Interaction', description: 'Press key', usage: 'press <key>' },
|
||||
'scroll': { category: 'Interaction', description: 'Scroll element into view', usage: 'scroll [sel]' },
|
||||
'wait': { category: 'Interaction', description: 'Wait for element/condition', usage: 'wait <sel|--networkidle|--load>' },
|
||||
'upload': { category: 'Interaction', description: 'Upload file(s)', usage: 'upload <sel> <file...>' },
|
||||
'viewport':{ category: 'Interaction', description: 'Set viewport size', usage: 'viewport <WxH>' },
|
||||
'cookie': { category: 'Interaction', description: 'Set cookie' },
|
||||
'cookie-import': { category: 'Interaction', description: 'Import cookies from JSON file', usage: 'cookie-import <json>' },
|
||||
'cookie-import-browser': { category: 'Interaction', description: 'Import cookies from real browser', usage: 'cookie-import-browser [browser] [--domain d]' },
|
||||
'header': { category: 'Interaction', description: 'Set custom request header', usage: 'header <name> <value>' },
|
||||
'useragent': { category: 'Interaction', description: 'Set user agent', usage: 'useragent <string>' },
|
||||
'dialog-accept': { category: 'Interaction', description: 'Auto-accept next dialog', usage: 'dialog-accept [text]' },
|
||||
'dialog-dismiss': { category: 'Interaction', description: 'Auto-dismiss next dialog' },
|
||||
// Visual
|
||||
'screenshot': { category: 'Visual', description: 'Save screenshot', usage: 'screenshot [path]' },
|
||||
'pdf': { category: 'Visual', description: 'Save as PDF', usage: 'pdf [path]' },
|
||||
'responsive': { category: 'Visual', description: 'Mobile/tablet/desktop screenshots', usage: 'responsive [prefix]' },
|
||||
'diff': { category: 'Visual', description: 'Text diff between pages', usage: 'diff <url1> <url2>' },
|
||||
// Tabs
|
||||
'tabs': { category: 'Tabs', description: 'List open tabs' },
|
||||
'tab': { category: 'Tabs', description: 'Switch to tab', usage: 'tab <id>' },
|
||||
'newtab': { category: 'Tabs', description: 'Open new tab', usage: 'newtab [url]' },
|
||||
'closetab':{ category: 'Tabs', description: 'Close tab', usage: 'closetab [id]' },
|
||||
// Server
|
||||
'status': { category: 'Server', description: 'Health check' },
|
||||
'stop': { category: 'Server', description: 'Shutdown server' },
|
||||
'restart': { category: 'Server', description: 'Restart server' },
|
||||
// Meta
|
||||
'snapshot':{ category: 'Snapshot', description: 'Accessibility tree with @refs', usage: 'snapshot [flags]' },
|
||||
'chain': { category: 'Meta', description: 'Multi-command from JSON stdin' },
|
||||
};
|
||||
|
||||
// Load-time validation: descriptions must cover exactly the command sets
|
||||
const allCmds = new Set([...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS]);
|
||||
const descKeys = new Set(Object.keys(COMMAND_DESCRIPTIONS));
|
||||
for (const cmd of allCmds) {
|
||||
if (!descKeys.has(cmd)) throw new Error(`COMMAND_DESCRIPTIONS missing entry for: ${cmd}`);
|
||||
}
|
||||
for (const key of descKeys) {
|
||||
if (!allCmds.has(key)) throw new Error(`COMMAND_DESCRIPTIONS has unknown command: ${key}`);
|
||||
}
|
||||
+3
-22
@@ -110,28 +110,9 @@ const idleCheckInterval = setInterval(() => {
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
// ─── Command Sets (exported for chain command) ──────────────────
|
||||
export const READ_COMMANDS = new Set([
|
||||
'text', 'html', 'links', 'forms', 'accessibility',
|
||||
'js', 'eval', 'css', 'attrs',
|
||||
'console', 'network', 'cookies', 'storage', 'perf',
|
||||
'dialog', 'is',
|
||||
]);
|
||||
|
||||
export const WRITE_COMMANDS = new Set([
|
||||
'goto', 'back', 'forward', 'reload',
|
||||
'click', 'fill', 'select', 'hover', 'type', 'press', 'scroll', 'wait',
|
||||
'viewport', 'cookie', 'cookie-import', 'cookie-import-browser', 'header', 'useragent',
|
||||
'upload', 'dialog-accept', 'dialog-dismiss',
|
||||
]);
|
||||
|
||||
export const META_COMMANDS = new Set([
|
||||
'tabs', 'tab', 'newtab', 'closetab',
|
||||
'status', 'stop', 'restart',
|
||||
'screenshot', 'pdf', 'responsive',
|
||||
'chain', 'diff',
|
||||
'url', 'snapshot',
|
||||
]);
|
||||
// ─── Command Sets (from commands.ts — single source of truth) ───
|
||||
import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from './commands';
|
||||
export { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS };
|
||||
|
||||
// ─── Server ────────────────────────────────────────────────────
|
||||
const browserManager = new BrowserManager();
|
||||
|
||||
+37
-38
@@ -40,6 +40,30 @@ interface SnapshotOptions {
|
||||
cursorInteractive?: boolean; // -C / --cursor-interactive: scan cursor:pointer etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot flag metadata — single source of truth for CLI parsing and doc generation.
|
||||
*
|
||||
* Imported by:
|
||||
* - gen-skill-docs.ts (generates {{SNAPSHOT_FLAGS}} tables)
|
||||
* - skill-parser.ts (validates flags in SKILL.md examples)
|
||||
*/
|
||||
export const SNAPSHOT_FLAGS: Array<{
|
||||
short: string;
|
||||
long: string;
|
||||
description: string;
|
||||
takesValue?: boolean;
|
||||
optionKey: keyof SnapshotOptions;
|
||||
}> = [
|
||||
{ short: '-i', long: '--interactive', description: 'Interactive elements only', optionKey: 'interactive' },
|
||||
{ short: '-c', long: '--compact', description: 'Remove empty structural elements', optionKey: 'compact' },
|
||||
{ short: '-d', long: '--depth', description: 'Limit tree depth', takesValue: true, optionKey: 'depth' },
|
||||
{ short: '-s', long: '--selector', description: 'Scope to CSS selector', takesValue: true, optionKey: 'selector' },
|
||||
{ short: '-D', long: '--diff', description: 'Diff against previous snapshot', optionKey: 'diff' },
|
||||
{ short: '-a', long: '--annotate', description: 'Annotated screenshot with ref labels', optionKey: 'annotate' },
|
||||
{ short: '-o', long: '--output', description: 'Output path for annotated screenshot', takesValue: true, optionKey: 'outputPath' },
|
||||
{ short: '-C', long: '--cursor-interactive', description: 'Scan cursor:pointer/onclick/tabindex elements', optionKey: 'cursorInteractive' },
|
||||
];
|
||||
|
||||
interface ParsedNode {
|
||||
indent: number;
|
||||
role: string;
|
||||
@@ -50,49 +74,24 @@ interface ParsedNode {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse CLI args into SnapshotOptions
|
||||
* Parse CLI args into SnapshotOptions — driven by SNAPSHOT_FLAGS metadata.
|
||||
*/
|
||||
export function parseSnapshotArgs(args: string[]): SnapshotOptions {
|
||||
const opts: SnapshotOptions = {};
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '-i':
|
||||
case '--interactive':
|
||||
opts.interactive = true;
|
||||
break;
|
||||
case '-c':
|
||||
case '--compact':
|
||||
opts.compact = true;
|
||||
break;
|
||||
case '-d':
|
||||
case '--depth':
|
||||
opts.depth = parseInt(args[++i], 10);
|
||||
const flag = SNAPSHOT_FLAGS.find(f => f.short === args[i] || f.long === args[i]);
|
||||
if (!flag) throw new Error(`Unknown snapshot flag: ${args[i]}`);
|
||||
if (flag.takesValue) {
|
||||
const value = args[++i];
|
||||
if (!value) throw new Error(`Usage: snapshot ${flag.short} <value>`);
|
||||
if (flag.optionKey === 'depth') {
|
||||
(opts as any)[flag.optionKey] = parseInt(value, 10);
|
||||
if (isNaN(opts.depth!)) throw new Error('Usage: snapshot -d <number>');
|
||||
break;
|
||||
case '-s':
|
||||
case '--selector':
|
||||
opts.selector = args[++i];
|
||||
if (!opts.selector) throw new Error('Usage: snapshot -s <selector>');
|
||||
break;
|
||||
case '-D':
|
||||
case '--diff':
|
||||
opts.diff = true;
|
||||
break;
|
||||
case '-a':
|
||||
case '--annotate':
|
||||
opts.annotate = true;
|
||||
break;
|
||||
case '-o':
|
||||
case '--output':
|
||||
opts.outputPath = args[++i];
|
||||
if (!opts.outputPath) throw new Error('Usage: snapshot -o <path>');
|
||||
break;
|
||||
case '-C':
|
||||
case '--cursor-interactive':
|
||||
opts.cursorInteractive = true;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown snapshot flag: ${args[i]}`);
|
||||
} else {
|
||||
(opts as any)[flag.optionKey] = value;
|
||||
}
|
||||
} else {
|
||||
(opts as any)[flag.optionKey] = true;
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
|
||||
Reference in New Issue
Block a user