cssprobe-cli 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,9 +44,34 @@ cssprobe-cli login https://mysite.com
44
44
 
45
45
  # Then inspect with state
46
46
  cssprobe-cli inspect https://mysite.com/page ".target" --state ~/.cssprobe-cli/states/mysite.json
47
+
48
+ # Wait for user interaction before collecting (e.g. click a button to open a dialog)
49
+ cssprobe-cli inspect https://mysite.com/page ".dialog" --state ~/.cssprobe-cli/states/mysite.json --wait
50
+ ```
51
+
52
+ ### 3. Interactive Mode
53
+
54
+ ```bash
55
+ # Open browser and inspect interactively (REPL mode)
56
+ cssprobe-cli interactive https://mysite.com --state ~/.cssprobe-cli/states/mysite.json
57
+ ```
58
+
59
+ Runtime commands:
60
+ ```
61
+ inspect> tree .dialog-A # DOM tree
62
+ inspect> layout .sidebar-B # ASCII layout diagram
63
+ inspect> sketch .content # Tree sketch (issues only)
64
+ inspect> findings .modal # Findings only
65
+ inspect> json .dialog-A # JSON output
66
+ inspect> report .dialog-A # Full report
67
+ inspect> .dialog-A # Shorthand for report
68
+ inspect> navigate https://other.com # Navigate to new URL
69
+ inspect> depth 10 # Set depth
70
+ inspect> help # Show help
71
+ inspect> quit # Exit
47
72
  ```
48
73
 
49
- ### 3. JSON Output
74
+ ### 4. JSON Output
50
75
 
51
76
  ```bash
52
77
  cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --json
@@ -61,6 +86,8 @@ cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --
61
86
  |------|--------|----------|
62
87
  | (default) | Markdown report with ancestor chain, DOM tree, findings | Terminal viewing |
63
88
  | `--json` | Structured JSON with snapshot, findings, confidence summary | Scripts, `jq` pipes, AI agent consumption |
89
+ | `--brief` | Compact output: tree sketch + warnings/errors only | Quick overview |
90
+ | `--layout` | ASCII layout diagram showing element positions and sizes | Visual layout inspection |
64
91
 
65
92
  ---
66
93
 
@@ -70,6 +97,7 @@ cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --
70
97
 
71
98
  ```bash
72
99
  cssprobe-cli inspect <url> [selector] # Inspect CSS layout
100
+ cssprobe-cli interactive <url> # Interactive REPL mode
73
101
  cssprobe-cli login <url> # Interactive login, save state
74
102
  cssprobe-cli state-import [file] # Import cookies from Netscape format
75
103
  ```
@@ -112,6 +140,8 @@ Options:
112
140
  --up-to <tag> Ancestor stop tag (default html)
113
141
  --state <file> Load saved state (cookies + localStorage)
114
142
  --brief Compact output: tree sketch + warnings/errors only
143
+ --layout ASCII layout diagram showing element positions and sizes
144
+ --wait Wait for user interaction before collecting (implies --headed)
115
145
  ```
116
146
 
117
147
  ---
@@ -15,6 +15,10 @@ export declare class BrowserLauncher {
15
15
  collect(cfg: CollectConfig): Promise<Snapshot>;
16
16
  /** Measure the DOM tree depth from a root selector to the deepest leaf. */
17
17
  measureDepth(selector: string): Promise<number>;
18
+ /** Wait for user to interact with the page. Press Enter to continue. */
19
+ waitForUser(): Promise<void>;
20
+ /** Navigate to a new URL in the existing page. */
21
+ navigate(url: string): Promise<void>;
18
22
  /** Scan the page for likely root elements (modals, dialogs, scroll containers). */
19
23
  autoDetectRoot(): Promise<string[]>;
20
24
  /**
@@ -125,6 +125,33 @@ class BrowserLauncher {
125
125
  return maxDepth;
126
126
  }, selector);
127
127
  }
128
+ /** Wait for user to interact with the page. Press Enter to continue. */
129
+ async waitForUser() {
130
+ console.error('');
131
+ console.error(' Browser is open. Perform your actions (login, click buttons, open dialogs, etc.).');
132
+ console.error(' Press Enter here when ready to inspect...');
133
+ console.error('');
134
+ await new Promise(resolve => {
135
+ process.stdin.once('data', () => resolve());
136
+ });
137
+ }
138
+ /** Navigate to a new URL in the existing page. */
139
+ async navigate(url) {
140
+ if (!this.page)
141
+ throw new Error('Browser not opened. Call open() first.');
142
+ if (url.startsWith('http://') || url.startsWith('https://')) {
143
+ await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
144
+ }
145
+ else if (url.startsWith('file://')) {
146
+ await this.page.goto(url, { waitUntil: 'load', timeout: 30000 });
147
+ }
148
+ else {
149
+ const resolved = path_1.default.resolve(url);
150
+ if (!fs_1.default.existsSync(resolved))
151
+ throw new Error(`File not found: ${resolved}`);
152
+ await this.page.goto(`file://${resolved}`, { waitUntil: 'load', timeout: 30000 });
153
+ }
154
+ }
128
155
  /** Scan the page for likely root elements (modals, dialogs, scroll containers). */
129
156
  async autoDetectRoot() {
130
157
  if (!this.page)
@@ -22,6 +22,8 @@ export declare const inspectCommand: import("./command").CommandSchema<z.ZodObje
22
22
  'up-to': z.ZodOptional<z.ZodString>;
23
23
  state: z.ZodOptional<z.ZodString>;
24
24
  brief: z.ZodOptional<z.ZodBoolean>;
25
+ layout: z.ZodOptional<z.ZodBoolean>;
26
+ wait: z.ZodOptional<z.ZodBoolean>;
25
27
  }, "strip", z.ZodTypeAny, {
26
28
  json?: boolean | undefined;
27
29
  headed?: boolean | undefined;
@@ -32,6 +34,8 @@ export declare const inspectCommand: import("./command").CommandSchema<z.ZodObje
32
34
  'up-to'?: string | undefined;
33
35
  state?: string | undefined;
34
36
  brief?: boolean | undefined;
37
+ layout?: boolean | undefined;
38
+ wait?: boolean | undefined;
35
39
  }, {
36
40
  json?: boolean | undefined;
37
41
  headed?: boolean | undefined;
@@ -42,4 +46,6 @@ export declare const inspectCommand: import("./command").CommandSchema<z.ZodObje
42
46
  'up-to'?: string | undefined;
43
47
  state?: string | undefined;
44
48
  brief?: boolean | undefined;
49
+ layout?: boolean | undefined;
50
+ wait?: boolean | undefined;
45
51
  }>>;
@@ -63,6 +63,8 @@ const inspect = (0, command_1.declareCommand)({
63
63
  'up-to': z.string().optional().describe('ancestor stop tag (default html)'),
64
64
  state: z.string().optional().describe('path to saved state file (cookies + localStorage)'),
65
65
  brief: z.boolean().optional().describe('compact output: tree sketch + warnings/errors only'),
66
+ layout: z.boolean().optional().describe('ASCII layout diagram showing element positions and sizes'),
67
+ wait: z.boolean().optional().describe('wait for user interaction before collecting (implies --headed)'),
66
68
  }),
67
69
  });
68
70
  const login = (0, command_1.declareCommand)({
@@ -157,8 +159,21 @@ const stateImport = (0, command_1.declareCommand)({
157
159
  merge: z.string().optional().describe('existing state file to merge into'),
158
160
  }),
159
161
  });
162
+ const interactive = (0, command_1.declareCommand)({
163
+ name: 'interactive',
164
+ category: 'core',
165
+ description: 'Open browser and inspect interactively (REPL mode)',
166
+ args: z.object({
167
+ url: urlArg,
168
+ }),
169
+ options: z.object({
170
+ browser: z.enum(['chromium', 'firefox', 'webkit']).optional().describe('browser engine (default chromium)'),
171
+ state: z.string().optional().describe('path to saved state file (cookies + localStorage)'),
172
+ depth: numberArg.optional().describe('DOM tree depth (default: auto, max 20)'),
173
+ }),
174
+ });
160
175
  const commandsArray = [
161
- inspect, login, stateImport,
176
+ inspect, login, stateImport, interactive,
162
177
  configShow, configSet, configList, configUse, configNew, configPath,
163
178
  skillInstall, skillUninstall,
164
179
  ];
@@ -47,6 +47,10 @@ async function program() {
47
47
  await handleStateImport(command, cmdEntry, rawArgs, output);
48
48
  return;
49
49
  }
50
+ if (commandName === 'interactive') {
51
+ await handleInteractive(command, cmdEntry, rawArgs, output);
52
+ return;
53
+ }
50
54
  await handleInspect(command, cmdEntry, rawArgs, output);
51
55
  }
52
56
  function parseArgs(help) {
@@ -114,6 +118,12 @@ async function handleInspect(command, cmdEntry, args, output) {
114
118
  const browser = parsed.browser || config.browser;
115
119
  const state = parsed.state || undefined;
116
120
  const brief = !!parsed.brief;
121
+ const layout = !!parsed.layout;
122
+ const wait = !!parsed.wait;
123
+ // --wait implies --headed
124
+ if (wait) {
125
+ args.headed = true;
126
+ }
117
127
  if (!url)
118
128
  output.error('URL is required. Usage: cssprobe-cli inspect <url> [selector]');
119
129
  // Auto-detect root selector if not provided
@@ -129,6 +139,10 @@ async function handleInspect(command, cmdEntry, args, output) {
129
139
  }
130
140
  const launcher = new launcher_1.BrowserLauncher({ browser, headed, state });
131
141
  await launcher.open(url);
142
+ // Wait for user interaction if --wait
143
+ if (wait) {
144
+ await launcher.waitForUser();
145
+ }
132
146
  // Resolve auto depth
133
147
  if (depth === 0) {
134
148
  const measured = await launcher.measureDepth(rootSelector);
@@ -148,7 +162,7 @@ async function handleInspect(command, cmdEntry, args, output) {
148
162
  console.log(output.format((0, renderer_1.renderJSON)(snapshot, findings, brief)));
149
163
  }
150
164
  else {
151
- console.log((0, renderer_1.renderMarkdown)(snapshot, findings, brief));
165
+ console.log((0, renderer_1.renderMarkdown)(snapshot, findings, brief, layout));
152
166
  }
153
167
  }
154
168
  catch (e) {
@@ -392,4 +406,274 @@ async function handleStateImport(command, cmdEntry, args, output) {
392
406
  output.error(e instanceof Error ? e.message : String(e));
393
407
  }
394
408
  }
409
+ // ── interactive ──
410
+ const REPL_COMMANDS = {
411
+ tree: '<selector> Show DOM tree',
412
+ layout: '<selector> Show ASCII layout diagram',
413
+ sketch: '<selector> Show tree sketch (issues only)',
414
+ findings: '<selector> Show findings only',
415
+ json: '<selector> Show JSON output',
416
+ report: '<selector> Show full report',
417
+ navigate: '<url> Navigate to a new URL',
418
+ depth: '<n> Set depth (default: auto)',
419
+ help: ' Show this help',
420
+ quit: ' Close browser and exit',
421
+ };
422
+ async function handleInteractive(command, cmdEntry, args, _output) {
423
+ const cmdArgs = splitArgs(args);
424
+ const parsed = (0, command_1.parseCommand)(command, cmdArgs);
425
+ const url = parsed.url;
426
+ const browser = parsed.browser || 'chromium';
427
+ const state = parsed.state || undefined;
428
+ const depthRaw = parsed.depth;
429
+ let depth = depthRaw === 'auto' || depthRaw === undefined ? 0 : Number(depthRaw);
430
+ if (!url) {
431
+ console.error('Error: URL is required. Usage: cssprobe-cli interactive <url>');
432
+ process.exit(1);
433
+ }
434
+ const launcher = new launcher_1.BrowserLauncher({ browser, headed: true, state });
435
+ await launcher.open(url);
436
+ let currentUrl = url;
437
+ // Resolve auto depth
438
+ if (depth === 0) {
439
+ try {
440
+ const candidates = await launcher.autoDetectRoot();
441
+ const selector = candidates[0] || 'body';
442
+ const measured = await launcher.measureDepth(selector);
443
+ depth = Math.min(measured, 20) || 6;
444
+ console.error(`Auto-detected depth: ${measured} (using ${depth})`);
445
+ }
446
+ catch {
447
+ depth = 6;
448
+ }
449
+ }
450
+ console.error('');
451
+ console.error(`Browser opened at ${currentUrl}`);
452
+ console.error("Type 'help' for available commands, 'quit' to exit.");
453
+ console.error('');
454
+ const rl = require('readline').createInterface({
455
+ input: process.stdin,
456
+ output: process.stderr,
457
+ prompt: 'inspect> ',
458
+ });
459
+ rl.prompt();
460
+ rl.on('line', async (line) => {
461
+ const trimmed = line.trim();
462
+ if (!trimmed) {
463
+ rl.prompt();
464
+ return;
465
+ }
466
+ // Parse: command [selector] [--options]
467
+ const parts = trimmed.split(/\s+/);
468
+ const cmd = parts[0];
469
+ const rest = parts.slice(1);
470
+ try {
471
+ switch (cmd) {
472
+ case 'help': {
473
+ console.error('');
474
+ console.error('Commands:');
475
+ for (const [name, desc] of Object.entries(REPL_COMMANDS)) {
476
+ console.error(` ${name.padEnd(12)} ${desc}`);
477
+ }
478
+ console.error('');
479
+ break;
480
+ }
481
+ case 'quit':
482
+ case 'exit': {
483
+ await launcher.close();
484
+ console.error('Browser closed.');
485
+ rl.close();
486
+ process.exit(0);
487
+ }
488
+ case 'navigate': {
489
+ const newUrl = rest[0];
490
+ if (!newUrl) {
491
+ console.error('Usage: navigate <url>');
492
+ break;
493
+ }
494
+ await launcher.navigate(newUrl);
495
+ currentUrl = newUrl;
496
+ console.error(`Navigated to ${currentUrl}`);
497
+ break;
498
+ }
499
+ case 'depth': {
500
+ const n = parseInt(rest[0], 10);
501
+ if (isNaN(n) || n < 1) {
502
+ console.error('Usage: depth <n>');
503
+ break;
504
+ }
505
+ depth = Math.min(n, 20);
506
+ console.error(`Depth set to ${depth}`);
507
+ break;
508
+ }
509
+ case 'tree':
510
+ case 'layout':
511
+ case 'sketch':
512
+ case 'findings':
513
+ case 'json':
514
+ case 'report': {
515
+ // Parse selector + options from remaining args
516
+ let selector = '';
517
+ let useBrief = false;
518
+ let useLayout = false;
519
+ let useJson = false;
520
+ for (const arg of rest) {
521
+ if (arg === '--brief')
522
+ useBrief = true;
523
+ else if (arg === '--layout')
524
+ useLayout = true;
525
+ else if (arg === '--json')
526
+ useJson = true;
527
+ else if (!arg.startsWith('-') && !selector)
528
+ selector = arg;
529
+ }
530
+ // Command-specific defaults
531
+ if (cmd === 'layout')
532
+ useLayout = true;
533
+ if (cmd === 'sketch')
534
+ useBrief = true;
535
+ if (cmd === 'json')
536
+ useJson = true;
537
+ if (!selector) {
538
+ console.error('Usage: ' + cmd + ' <selector> [--brief] [--layout] [--json]');
539
+ break;
540
+ }
541
+ // Measure depth if needed
542
+ let useDepth = depth;
543
+ if (useDepth === 0) {
544
+ try {
545
+ const measured = await launcher.measureDepth(selector);
546
+ useDepth = Math.min(measured, 20) || 6;
547
+ }
548
+ catch {
549
+ useDepth = 6;
550
+ }
551
+ }
552
+ const collectCfg = {
553
+ rootSelector: selector,
554
+ upTo: 'html',
555
+ downDepth: useDepth,
556
+ maxNodes: 60,
557
+ };
558
+ const snapshot = await launcher.collect(collectCfg);
559
+ if (snapshot.error) {
560
+ console.error(`Error: ${snapshot.error}`);
561
+ if (snapshot.candidates?.length) {
562
+ console.error(`Similar nodes: ${snapshot.candidates.join(', ')}`);
563
+ }
564
+ break;
565
+ }
566
+ const findings = (0, analyzer_1.analyze)(snapshot);
567
+ if (cmd === 'findings') {
568
+ // Only show findings section
569
+ const important = findings.filter(f => f.level === 'warning' || f.level === 'error');
570
+ if (important.length === 0) {
571
+ console.log('No warnings or errors');
572
+ }
573
+ else {
574
+ for (const f of important) {
575
+ const icon = f.level === 'error' ? '\u26A0' : '\u26A0';
576
+ console.log(`- ${icon} ${f.message} [${f.confidence}]${f.location ? ` @ ${f.location}` : ''}`);
577
+ }
578
+ }
579
+ }
580
+ else if (useJson) {
581
+ console.log(JSON.stringify((0, renderer_1.renderJSON)(snapshot, findings, useBrief), null, 2));
582
+ }
583
+ else if (cmd === 'tree') {
584
+ // Only DOM tree
585
+ const lines = [];
586
+ lines.push(`## DOM tree (${snapshot.downDepth} levels deep)`);
587
+ lines.push('```');
588
+ // Inline renderNode
589
+ const renderNodeLocal = (node, indent) => {
590
+ if (!node)
591
+ return;
592
+ const cls = node.classes?.length ? `.${node.classes.join('.')}` : '';
593
+ const id = node.id ? `#${node.id}` : '';
594
+ const m = node.metrics;
595
+ const s = node.shape || {};
596
+ const shapeStr = s.role ? `[${s.role} h:${s.heightStrategy} w:${s.widthStrategy}]` : '';
597
+ const flag = [];
598
+ if (node.flags?.overflowsParent)
599
+ flag.push('\u26A0parent-overflow');
600
+ if (node.flags?.overflowsViewport)
601
+ flag.push('\u26A0viewport-overflow');
602
+ if (m?.clientHeight === 0 && m?.offsetHeight === 0)
603
+ flag.push('\u26A0collapsed');
604
+ lines.push(`${' '.repeat(indent)}<${node.tag}${id}${cls}> ${shapeStr} [${node.props?.position},${node.props?.display}] rect(${m?.rect?.width}\u00D7${m?.rect?.height})${flag.length ? ' ' + flag.join(' ') : ''}`);
605
+ for (const child of node.children || [])
606
+ renderNodeLocal(child, indent + 1);
607
+ };
608
+ renderNodeLocal(snapshot.tree, 0);
609
+ lines.push('```');
610
+ console.log(lines.join('\n'));
611
+ }
612
+ else {
613
+ console.log((0, renderer_1.renderMarkdown)(snapshot, findings, useBrief, useLayout));
614
+ }
615
+ break;
616
+ }
617
+ default: {
618
+ // Try as a direct selector (shorthand for 'report <selector>')
619
+ if (!cmd.startsWith('-')) {
620
+ const selector = cmd;
621
+ let useBrief = false;
622
+ let useLayout = false;
623
+ for (const arg of rest) {
624
+ if (arg === '--brief')
625
+ useBrief = true;
626
+ if (arg === '--layout')
627
+ useLayout = true;
628
+ }
629
+ let useDepth = depth;
630
+ if (useDepth === 0) {
631
+ try {
632
+ const measured = await launcher.measureDepth(selector);
633
+ useDepth = Math.min(measured, 20) || 6;
634
+ }
635
+ catch {
636
+ useDepth = 6;
637
+ }
638
+ }
639
+ const collectCfg = {
640
+ rootSelector: selector,
641
+ upTo: 'html',
642
+ downDepth: useDepth,
643
+ maxNodes: 60,
644
+ };
645
+ const snapshot = await launcher.collect(collectCfg);
646
+ if (snapshot.error) {
647
+ console.error(`Error: ${snapshot.error}`);
648
+ if (snapshot.candidates?.length) {
649
+ console.error(`Similar nodes: ${snapshot.candidates.join(', ')}`);
650
+ }
651
+ }
652
+ else {
653
+ const findings = (0, analyzer_1.analyze)(snapshot);
654
+ console.log((0, renderer_1.renderMarkdown)(snapshot, findings, useBrief, useLayout));
655
+ }
656
+ }
657
+ else {
658
+ console.error(`Unknown command: ${cmd}. Type 'help' for available commands.`);
659
+ }
660
+ }
661
+ }
662
+ }
663
+ catch (e) {
664
+ console.error(`Error: ${e.message || e}`);
665
+ }
666
+ rl.prompt();
667
+ });
668
+ rl.on('close', async () => {
669
+ await launcher.close().catch(() => { });
670
+ process.exit(0);
671
+ });
672
+ // Handle SIGINT gracefully
673
+ process.on('SIGINT', async () => {
674
+ console.error('\nClosing browser...');
675
+ await launcher.close().catch(() => { });
676
+ process.exit(0);
677
+ });
678
+ }
395
679
  //# sourceMappingURL=program.js.map
@@ -1,3 +1,3 @@
1
1
  import type { Snapshot, Finding } from './types';
2
- export declare function renderMarkdown(snapshot: Snapshot, findings: Finding[], brief?: boolean): string;
2
+ export declare function renderMarkdown(snapshot: Snapshot, findings: Finding[], brief?: boolean, layout?: boolean): string;
3
3
  export declare function renderJSON(snapshot: Snapshot, findings: Finding[], brief?: boolean): object;
@@ -136,6 +136,82 @@ function renderSketch(tree, findings) {
136
136
  }
137
137
  return renderSketchNode(sketchRoot, '', true);
138
138
  }
139
+ // ─── Layout diagram (ASCII box layout) ───
140
+ const BOX = { tl: '\u250C', tr: '\u2510', bl: '\u2514', br: '\u2518', h: '\u2500', v: '\u2502' };
141
+ function renderLayout(snapshot, findings) {
142
+ if (!snapshot.tree)
143
+ return ['(empty tree)'];
144
+ const vpW = snapshot.viewport.width;
145
+ const termWidth = Math.min(Math.max(process.stdout?.columns || 80, 60), 120);
146
+ const indentUnit = 2;
147
+ const maxDepth = 8;
148
+ const boxPad = 4; // '│ ' prefix per depth level
149
+ const issueLocations = new Set();
150
+ for (const f of findings) {
151
+ if (f.location && (f.level === 'warning' || f.level === 'error'))
152
+ issueLocations.add(f.location);
153
+ }
154
+ function boxCharW(nodeW, depth) {
155
+ const usedIndent = depth * indentUnit;
156
+ const available = termWidth - usedIndent - boxPad;
157
+ const ratio = nodeW / vpW;
158
+ return Math.max(Math.round(available * ratio), 14);
159
+ }
160
+ const lines = [];
161
+ function renderEl(node, depth, parentW) {
162
+ if (depth > maxDepth) {
163
+ lines.push(' '.repeat(depth) + '... (depth limit)');
164
+ return;
165
+ }
166
+ const m = node.metrics.rect;
167
+ // Skip zero-size elements (SVG symbols, hidden elements)
168
+ if (m.width < 1 && m.height < 1 && node.children.length > 0) {
169
+ for (const child of node.children)
170
+ renderEl(child, depth, parentW);
171
+ return;
172
+ }
173
+ if (m.width < 1 && m.height < 1)
174
+ return;
175
+ const label = nodeLabel(node);
176
+ const hasIssue = issueLocations.has(label);
177
+ const dimStr = `${Math.round(m.width)}\u00D7${Math.round(m.height)}`;
178
+ const charW = boxCharW(m.width, depth);
179
+ const innerW = charW - 2;
180
+ const indent = ' '.repeat(depth);
181
+ const issueMark = hasIssue ? ' \u26A0' : '';
182
+ if (innerW < 3) {
183
+ // Too narrow for a proper box
184
+ lines.push(`${indent}${BOX.v} ${label.slice(0, charW - 3)}... ${dimStr}`);
185
+ for (const child of node.children)
186
+ renderEl(child, depth + 1, m.width);
187
+ return;
188
+ }
189
+ // Top border: ┌─ label ────────┐ WxH
190
+ const labelPart = ` ${label} `;
191
+ const dimPart = ` ${dimStr} `;
192
+ const topFillLen = Math.max(0, innerW - labelPart.length);
193
+ lines.push(`${indent}${BOX.tl}${BOX.h}${labelPart}${BOX.h.repeat(topFillLen)}${BOX.tr}${dimPart}${issueMark}`);
194
+ // Content
195
+ if (node.children.length === 0) {
196
+ // Empty box
197
+ lines.push(`${indent}${BOX.v}${' '.repeat(innerW)}${BOX.v}`);
198
+ }
199
+ else {
200
+ for (const child of node.children)
201
+ renderEl(child, depth + 1, m.width);
202
+ }
203
+ // Bottom border: └───────────────┘
204
+ lines.push(`${indent}${BOX.bl}${BOX.h.repeat(innerW)}${BOX.br}`);
205
+ }
206
+ renderEl(snapshot.tree, 0, vpW);
207
+ const scaleStr = `${Math.round(vpW / termWidth)}px/char`;
208
+ const result = [];
209
+ result.push(`## Layout (${vpW}\u00D7${snapshot.viewport.height} viewport, scale: ${scaleStr})`);
210
+ result.push('```');
211
+ result.push(...lines);
212
+ result.push('```');
213
+ return result;
214
+ }
139
215
  // ─── Findings rendering ───
140
216
  const CONF_BADGE = {
141
217
  DEFINITE: 'DEFINITE',
@@ -193,7 +269,7 @@ function confidenceSummary(findings) {
193
269
  return parts.join(' | ');
194
270
  }
195
271
  // ─── Markdown ───
196
- function renderMarkdown(snapshot, findings, brief = false) {
272
+ function renderMarkdown(snapshot, findings, brief = false, layout = false) {
197
273
  const lines = [];
198
274
  const vp = snapshot.viewport ? `${snapshot.viewport.width}\u00D7${snapshot.viewport.height}` : 'unknown';
199
275
  lines.push('# cssprobe-cli report');
@@ -212,6 +288,10 @@ function renderMarkdown(snapshot, findings, brief = false) {
212
288
  lines.push('```');
213
289
  lines.push(...renderAncestors(snapshot.ancestors));
214
290
  lines.push('```');
291
+ if (layout) {
292
+ lines.push('');
293
+ lines.push(...renderLayout(snapshot, findings));
294
+ }
215
295
  if (brief) {
216
296
  lines.push('');
217
297
  lines.push('## Tree Sketch (issues only)');
@@ -219,7 +299,7 @@ function renderMarkdown(snapshot, findings, brief = false) {
219
299
  lines.push(...renderSketch(snapshot.tree, findings));
220
300
  lines.push('```');
221
301
  }
222
- else {
302
+ else if (!layout) {
223
303
  lines.push('');
224
304
  lines.push(`## DOM tree (${snapshot.downDepth} levels deep)`);
225
305
  lines.push('```');
package/dist/help.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
- "global": "Usage: cssprobe-cli <command> [args] [options]\n\n\nInspection:\n inspect <url> [selector] inspect the runtime css of a page (layout, scroll, colors, fonts, backgrounds)\n login <url> open browser for interactive login, then save state for later use\n state-import [file] import cookies from netscape format file into a playwright state file\n\nConfiguration:\n config-show show current configuration\n config-set <key> <value> set a configuration value\n config-list list all profiles\n config-use <name> switch active profile\n config-new <name> create a new profile\n config-path show configuration file path\n\nAgent Skills:\n skill-install install agent skill for cssprobe-cli (teaches agents how to use the cli)\n skill-uninstall remove agent skill for cssprobe-cli\n\nGlobal options:\n --help [command] print help\n --json output response as JSON\n --raw output only the result value\n --version print version\n -p, --profile <name> use named profile (or CSSPROBE_PROFILE env)",
2
+ "global": "Usage: cssprobe-cli <command> [args] [options]\n\n\nInspection:\n inspect <url> [selector] inspect the runtime css of a page (layout, scroll, colors, fonts, backgrounds)\n login <url> open browser for interactive login, then save state for later use\n state-import [file] import cookies from netscape format file into a playwright state file\n interactive <url> open browser and inspect interactively (repl mode)\n\nConfiguration:\n config-show show current configuration\n config-set <key> <value> set a configuration value\n config-list list all profiles\n config-use <name> switch active profile\n config-new <name> create a new profile\n config-path show configuration file path\n\nAgent Skills:\n skill-install install agent skill for cssprobe-cli (teaches agents how to use the cli)\n skill-uninstall remove agent skill for cssprobe-cli\n\nGlobal options:\n --help [command] print help\n --json output response as JSON\n --raw output only the result value\n --version print version\n -p, --profile <name> use named profile (or CSSPROBE_PROFILE env)",
3
3
  "commands": {
4
4
  "inspect": {
5
- "help": "cssprobe-cli inspect <url> [selector]\n\nInspect the runtime CSS of a page (layout, scroll, colors, fonts, backgrounds)\n\nArguments:\n <url> url, file:// path, or local html file path\n [selector] css selector for the root element (auto-detected if omitted)\nOptions:\n --json output structured json instead of markdown\n --headed show the browser window\n --browser browser engine (default chromium)\n --zoom run 1x/0.5x viewport diagnosis\n --depth dom tree depth (default: auto, max 20)\n --max-nodes node count cap (default 60)\n --up-to ancestor stop tag (default html)\n --state path to saved state file (cookies + localstorage)\n --brief compact output: tree sketch + warnings/errors only\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
5
+ "help": "cssprobe-cli inspect <url> [selector]\n\nInspect the runtime CSS of a page (layout, scroll, colors, fonts, backgrounds)\n\nArguments:\n <url> url, file:// path, or local html file path\n [selector] css selector for the root element (auto-detected if omitted)\nOptions:\n --json output structured json instead of markdown\n --headed show the browser window\n --browser browser engine (default chromium)\n --zoom run 1x/0.5x viewport diagnosis\n --depth dom tree depth (default: auto, max 20)\n --max-nodes node count cap (default 60)\n --up-to ancestor stop tag (default html)\n --state path to saved state file (cookies + localstorage)\n --brief compact output: tree sketch + warnings/errors only\n --layout ascii layout diagram showing element positions and sizes\n --wait wait for user interaction before collecting (implies --headed)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
6
6
  "flags": {
7
7
  "json": "boolean",
8
8
  "headed": "boolean",
@@ -12,7 +12,9 @@
12
12
  "max-nodes": "string",
13
13
  "up-to": "string",
14
14
  "state": "string",
15
- "brief": "boolean"
15
+ "brief": "boolean",
16
+ "layout": "boolean",
17
+ "wait": "boolean"
16
18
  },
17
19
  "args": [
18
20
  "url",
@@ -39,6 +41,17 @@
39
41
  "file"
40
42
  ]
41
43
  },
44
+ "interactive": {
45
+ "help": "cssprobe-cli interactive <url>\n\nOpen browser and inspect interactively (REPL mode)\n\nArguments:\n <url> url, file:// path, or local html file path\nOptions:\n --browser browser engine (default chromium)\n --state path to saved state file (cookies + localstorage)\n --depth dom tree depth (default: auto, max 20)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
46
+ "flags": {
47
+ "browser": "string",
48
+ "state": "string",
49
+ "depth": "string"
50
+ },
51
+ "args": [
52
+ "url"
53
+ ]
54
+ },
42
55
  "config-show": {
43
56
  "help": "cssprobe-cli config-show \n\nShow current configuration\n\nOptions:\n --profile profile name",
44
57
  "flags": {
@@ -104,6 +117,8 @@
104
117
  "headed",
105
118
  "zoom",
106
119
  "brief",
120
+ "layout",
121
+ "wait",
107
122
  "local"
108
123
  ]
109
124
  }
@@ -96,6 +96,39 @@ function buildSkillMd() {
96
96
  'cssprobe-cli state-import new-cookies.txt --merge existing.json --out merged.json',
97
97
  BT3,
98
98
  '',
99
+ '## Wait for User Interaction',
100
+ '',
101
+ 'For pages that require clicking a button or opening a dialog before inspection:',
102
+ '',
103
+ BT3 + 'bash',
104
+ 'cssprobe-cli inspect https://mysite.com/page ".dialog" --state ~/.cssprobe-cli/states/mysite.json --wait',
105
+ '# Browser opens -> perform actions (click, open dialog) -> press Enter -> inspect',
106
+ BT3,
107
+ '',
108
+ '## Interactive Mode (REPL)',
109
+ '',
110
+ 'For inspecting multiple areas of the same page:',
111
+ '',
112
+ BT3 + 'bash',
113
+ 'cssprobe-cli interactive https://mysite.com --state ~/.cssprobe-cli/states/mysite.json',
114
+ BT3,
115
+ '',
116
+ 'Runtime commands:',
117
+ '',
118
+ BT3,
119
+ 'inspect> tree .dialog-A # DOM tree',
120
+ 'inspect> layout .sidebar-B # ASCII layout diagram',
121
+ 'inspect> sketch .content # Tree sketch (issues only)',
122
+ 'inspect> findings .modal # Findings only',
123
+ 'inspect> json .dialog-A # JSON output',
124
+ 'inspect> report .dialog-A # Full report',
125
+ 'inspect> .dialog-A # Shorthand for report',
126
+ 'inspect> navigate https://other.com # Navigate to new URL',
127
+ 'inspect> depth 10 # Set depth',
128
+ 'inspect> help # Show help',
129
+ 'inspect> quit # Exit',
130
+ BT3,
131
+ '',
99
132
  '## Output Modes',
100
133
  '',
101
134
  'Always pass ' + BT + '--json' + BT + ' when consuming output programmatically:',
@@ -138,6 +171,9 @@ function buildSkillMd() {
138
171
  ' --max-nodes <n> Node count cap (default 60)',
139
172
  ' --up-to <tag> Ancestor stop tag (default html)',
140
173
  ' --state <file> Load saved state (cookies + localStorage)',
174
+ ' --brief Compact output: tree sketch + warnings/errors only',
175
+ ' --layout ASCII layout diagram showing element positions and sizes',
176
+ ' --wait Wait for user interaction before collecting (implies --headed)',
141
177
  BT3,
142
178
  '',
143
179
  '## state-import: Netscape Cookie Format',
@@ -125,9 +125,44 @@ cssprobe-cli inspect https://mysite.com/page ".target" --state ~/.cssprobe-cli/s
125
125
  cssprobe-cli state-import new-cookies.txt --merge existing-state.json --out updated.json
126
126
  ```
127
127
 
128
+ ### Option D: Wait for User Interaction
129
+
130
+ For pages that require clicking a button or opening a dialog before inspection:
131
+
132
+ ```bash
133
+ cssprobe-cli inspect https://mysite.com/page ".dialog" --state ~/.cssprobe-cli/states/mysite.json --wait
134
+ # Browser opens -> perform actions (click, open dialog) -> press Enter -> inspect
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Step 5: Interactive Mode (REPL)
140
+
141
+ For inspecting multiple areas of the same page without re-opening the browser:
142
+
143
+ ```bash
144
+ cssprobe-cli interactive https://mysite.com --state ~/.cssprobe-cli/states/mysite.json
145
+ ```
146
+
147
+ Runtime commands:
148
+
149
+ ```
150
+ inspect> tree .dialog-A # DOM tree
151
+ inspect> layout .sidebar-B # ASCII layout diagram
152
+ inspect> sketch .content # Tree sketch (issues only)
153
+ inspect> findings .modal # Findings only
154
+ inspect> json .dialog-A # JSON output
155
+ inspect> report .dialog-A # Full report
156
+ inspect> .dialog-A # Shorthand for report
157
+ inspect> navigate https://other.com # Navigate to new URL
158
+ inspect> depth 10 # Set depth
159
+ inspect> help # Show help
160
+ inspect> quit # Exit
161
+ ```
162
+
128
163
  ---
129
164
 
130
- ## Step 5: Understand the Output
165
+ ## Step 6: Understand the Output
131
166
 
132
167
  ### Markdown Report (default)
133
168
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cssprobe-cli",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Runtime CSS probe — inspect layout, colors, backgrounds, fonts and more in a live browser. AI agents and shell automation.",
5
5
  "keywords": [
6
6
  "css",