cssprobe-cli 0.1.0 → 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 +43 -6
- package/dist/browser/launcher.d.ts +6 -0
- package/dist/browser/launcher.js +46 -0
- package/dist/cli/commands.d.ts +9 -0
- package/dist/cli/commands.js +18 -2
- package/dist/cli/program.js +303 -5
- package/dist/engine/renderer.d.ts +2 -2
- package/dist/engine/renderer.js +222 -15
- package/dist/help.json +20 -3
- package/dist/installer/skill-template.js +45 -9
- package/docs/guide/installation.md +43 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,10 +24,10 @@ cssprobe-cli skill-install
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
# With explicit selector
|
|
27
|
-
cssprobe-cli inspect https://
|
|
27
|
+
cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout ".container"
|
|
28
28
|
|
|
29
29
|
# Auto-detect root element
|
|
30
|
-
cssprobe-cli inspect https://
|
|
30
|
+
cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout
|
|
31
31
|
|
|
32
32
|
# Local HTML file
|
|
33
33
|
cssprobe-cli inspect ./test.html ".container"
|
|
@@ -44,13 +44,38 @@ 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
|
|
47
50
|
```
|
|
48
51
|
|
|
49
|
-
### 3.
|
|
52
|
+
### 3. Interactive Mode
|
|
50
53
|
|
|
51
54
|
```bash
|
|
52
|
-
|
|
53
|
-
cssprobe-cli
|
|
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
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 4. JSON Output
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --json
|
|
78
|
+
cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --json | jq '.findings[] | {id, confidence, message}'
|
|
54
79
|
```
|
|
55
80
|
|
|
56
81
|
---
|
|
@@ -61,6 +86,8 @@ cssprobe-cli --json inspect https://example.com ".content" | jq '.findings[] | {
|
|
|
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,10 +97,17 @@ cssprobe-cli --json inspect https://example.com ".content" | jq '.findings[] | {
|
|
|
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
|
```
|
|
76
104
|
|
|
105
|
+
### Inspection
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body
|
|
109
|
+
```
|
|
110
|
+
|
|
77
111
|
### Configuration
|
|
78
112
|
|
|
79
113
|
```bash
|
|
@@ -101,10 +135,13 @@ Options:
|
|
|
101
135
|
--headed Show browser window
|
|
102
136
|
--browser <engine> chromium|firefox|webkit (default chromium)
|
|
103
137
|
--zoom Run 1x/0.5x viewport diagnosis
|
|
104
|
-
--depth <n> DOM tree depth (default
|
|
138
|
+
--depth <n> DOM tree depth (default: auto)
|
|
105
139
|
--max-nodes <n> Node count cap (default 60)
|
|
106
140
|
--up-to <tag> Ancestor stop tag (default html)
|
|
107
141
|
--state <file> Load saved state (cookies + localStorage)
|
|
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)
|
|
108
145
|
```
|
|
109
146
|
|
|
110
147
|
---
|
|
@@ -13,6 +13,12 @@ export declare class BrowserLauncher {
|
|
|
13
13
|
constructor(options?: LauncherOptions);
|
|
14
14
|
open(url: string): Promise<void>;
|
|
15
15
|
collect(cfg: CollectConfig): Promise<Snapshot>;
|
|
16
|
+
/** Measure the DOM tree depth from a root selector to the deepest leaf. */
|
|
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>;
|
|
16
22
|
/** Scan the page for likely root elements (modals, dialogs, scroll containers). */
|
|
17
23
|
autoDetectRoot(): Promise<string[]>;
|
|
18
24
|
/**
|
package/dist/browser/launcher.js
CHANGED
|
@@ -106,6 +106,52 @@ class BrowserLauncher {
|
|
|
106
106
|
}, cfg);
|
|
107
107
|
return snapshot;
|
|
108
108
|
}
|
|
109
|
+
/** Measure the DOM tree depth from a root selector to the deepest leaf. */
|
|
110
|
+
async measureDepth(selector) {
|
|
111
|
+
if (!this.page)
|
|
112
|
+
throw new Error('Browser not opened. Call open() first.');
|
|
113
|
+
return this.page.evaluate((sel) => {
|
|
114
|
+
const root = document.querySelector(sel);
|
|
115
|
+
if (!root)
|
|
116
|
+
return 0;
|
|
117
|
+
let maxDepth = 0;
|
|
118
|
+
const walk = (el, depth) => {
|
|
119
|
+
if (depth > maxDepth)
|
|
120
|
+
maxDepth = depth;
|
|
121
|
+
for (const child of Array.from(el.children))
|
|
122
|
+
walk(child, depth + 1);
|
|
123
|
+
};
|
|
124
|
+
walk(root, 0);
|
|
125
|
+
return maxDepth;
|
|
126
|
+
}, selector);
|
|
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
|
+
}
|
|
109
155
|
/** Scan the page for likely root elements (modals, dialogs, scroll containers). */
|
|
110
156
|
async autoDetectRoot() {
|
|
111
157
|
if (!this.page)
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export declare const inspectCommand: import("./command").CommandSchema<z.ZodObje
|
|
|
21
21
|
'max-nodes': z.ZodOptional<z.ZodEffects<z.ZodNumber, number, unknown>>;
|
|
22
22
|
'up-to': z.ZodOptional<z.ZodString>;
|
|
23
23
|
state: z.ZodOptional<z.ZodString>;
|
|
24
|
+
brief: z.ZodOptional<z.ZodBoolean>;
|
|
25
|
+
layout: z.ZodOptional<z.ZodBoolean>;
|
|
26
|
+
wait: z.ZodOptional<z.ZodBoolean>;
|
|
24
27
|
}, "strip", z.ZodTypeAny, {
|
|
25
28
|
json?: boolean | undefined;
|
|
26
29
|
headed?: boolean | undefined;
|
|
@@ -30,6 +33,9 @@ export declare const inspectCommand: import("./command").CommandSchema<z.ZodObje
|
|
|
30
33
|
'max-nodes'?: number | undefined;
|
|
31
34
|
'up-to'?: string | undefined;
|
|
32
35
|
state?: string | undefined;
|
|
36
|
+
brief?: boolean | undefined;
|
|
37
|
+
layout?: boolean | undefined;
|
|
38
|
+
wait?: boolean | undefined;
|
|
33
39
|
}, {
|
|
34
40
|
json?: boolean | undefined;
|
|
35
41
|
headed?: boolean | undefined;
|
|
@@ -39,4 +45,7 @@ export declare const inspectCommand: import("./command").CommandSchema<z.ZodObje
|
|
|
39
45
|
'max-nodes'?: unknown;
|
|
40
46
|
'up-to'?: string | undefined;
|
|
41
47
|
state?: string | undefined;
|
|
48
|
+
brief?: boolean | undefined;
|
|
49
|
+
layout?: boolean | undefined;
|
|
50
|
+
wait?: boolean | undefined;
|
|
42
51
|
}>>;
|
package/dist/cli/commands.js
CHANGED
|
@@ -58,10 +58,13 @@ const inspect = (0, command_1.declareCommand)({
|
|
|
58
58
|
headed: z.boolean().optional().describe('show the browser window'),
|
|
59
59
|
browser: z.enum(['chromium', 'firefox', 'webkit']).optional().describe('browser engine (default chromium)'),
|
|
60
60
|
zoom: z.boolean().optional().describe('run 1x/0.5x viewport diagnosis'),
|
|
61
|
-
depth: numberArg.optional().describe('DOM tree depth (default
|
|
61
|
+
depth: numberArg.optional().describe('DOM tree depth (default: auto, max 20)'),
|
|
62
62
|
'max-nodes': numberArg.optional().describe('node count cap (default 60)'),
|
|
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
|
+
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)'),
|
|
65
68
|
}),
|
|
66
69
|
});
|
|
67
70
|
const login = (0, command_1.declareCommand)({
|
|
@@ -156,8 +159,21 @@ const stateImport = (0, command_1.declareCommand)({
|
|
|
156
159
|
merge: z.string().optional().describe('existing state file to merge into'),
|
|
157
160
|
}),
|
|
158
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
|
+
});
|
|
159
175
|
const commandsArray = [
|
|
160
|
-
inspect, login, stateImport,
|
|
176
|
+
inspect, login, stateImport, interactive,
|
|
161
177
|
configShow, configSet, configList, configUse, configNew, configPath,
|
|
162
178
|
skillInstall, skillUninstall,
|
|
163
179
|
];
|
package/dist/cli/program.js
CHANGED
|
@@ -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) {
|
|
@@ -100,12 +104,26 @@ async function handleInspect(command, cmdEntry, args, output) {
|
|
|
100
104
|
const url = parsed.url;
|
|
101
105
|
const selector = parsed.selector;
|
|
102
106
|
const zoom = !!parsed.zoom;
|
|
103
|
-
const
|
|
107
|
+
const depthRaw = parsed.depth;
|
|
108
|
+
let depth;
|
|
109
|
+
if (depthRaw === 'auto' || depthRaw === undefined) {
|
|
110
|
+
depth = 0; // placeholder, resolved after browser opens
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
depth = Number(depthRaw);
|
|
114
|
+
}
|
|
104
115
|
const maxNodes = parsed['max-nodes'] || 60;
|
|
105
116
|
const upTo = parsed['up-to'] || 'html';
|
|
106
117
|
const headed = !!parsed.headed || config.headed;
|
|
107
118
|
const browser = parsed.browser || config.browser;
|
|
108
119
|
const state = parsed.state || undefined;
|
|
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
|
+
}
|
|
109
127
|
if (!url)
|
|
110
128
|
output.error('URL is required. Usage: cssprobe-cli inspect <url> [selector]');
|
|
111
129
|
// Auto-detect root selector if not provided
|
|
@@ -119,22 +137,32 @@ async function handleInspect(command, cmdEntry, args, output) {
|
|
|
119
137
|
rootSelector = detected;
|
|
120
138
|
console.error(`Auto-detected root: ${rootSelector}`);
|
|
121
139
|
}
|
|
140
|
+
const launcher = new launcher_1.BrowserLauncher({ browser, headed, state });
|
|
141
|
+
await launcher.open(url);
|
|
142
|
+
// Wait for user interaction if --wait
|
|
143
|
+
if (wait) {
|
|
144
|
+
await launcher.waitForUser();
|
|
145
|
+
}
|
|
146
|
+
// Resolve auto depth
|
|
147
|
+
if (depth === 0) {
|
|
148
|
+
const measured = await launcher.measureDepth(rootSelector);
|
|
149
|
+
depth = Math.min(measured, 20) || 6;
|
|
150
|
+
console.error(`Auto-detected depth: ${measured} (using ${depth})`);
|
|
151
|
+
}
|
|
122
152
|
const collectCfg = {
|
|
123
153
|
rootSelector,
|
|
124
154
|
upTo,
|
|
125
155
|
downDepth: depth,
|
|
126
156
|
maxNodes,
|
|
127
157
|
};
|
|
128
|
-
const launcher = new launcher_1.BrowserLauncher({ browser, headed, state });
|
|
129
|
-
await launcher.open(url);
|
|
130
158
|
const snapshot = await launcher.collect(collectCfg);
|
|
131
159
|
await launcher.close();
|
|
132
160
|
const findings = (0, analyzer_1.analyze)(snapshot);
|
|
133
161
|
if (output.json) {
|
|
134
|
-
console.log(output.format((0, renderer_1.renderJSON)(snapshot, findings)));
|
|
162
|
+
console.log(output.format((0, renderer_1.renderJSON)(snapshot, findings, brief)));
|
|
135
163
|
}
|
|
136
164
|
else {
|
|
137
|
-
console.log((0, renderer_1.renderMarkdown)(snapshot, findings));
|
|
165
|
+
console.log((0, renderer_1.renderMarkdown)(snapshot, findings, brief, layout));
|
|
138
166
|
}
|
|
139
167
|
}
|
|
140
168
|
catch (e) {
|
|
@@ -378,4 +406,274 @@ async function handleStateImport(command, cmdEntry, args, output) {
|
|
|
378
406
|
output.error(e instanceof Error ? e.message : String(e));
|
|
379
407
|
}
|
|
380
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
|
+
}
|
|
381
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[]): string;
|
|
3
|
-
export declare function renderJSON(snapshot: Snapshot, findings: Finding[]): object;
|
|
2
|
+
export declare function renderMarkdown(snapshot: Snapshot, findings: Finding[], brief?: boolean, layout?: boolean): string;
|
|
3
|
+
export declare function renderJSON(snapshot: Snapshot, findings: Finding[], brief?: boolean): object;
|
package/dist/engine/renderer.js
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.renderMarkdown = renderMarkdown;
|
|
6
6
|
exports.renderJSON = renderJSON;
|
|
7
|
+
// ─── Node label (matches analyzer.ts nodeLabel) ───
|
|
8
|
+
function nodeLabel(node) {
|
|
9
|
+
const cls = node.classes.length ? `.${node.classes.join('.')}` : '';
|
|
10
|
+
const id = node.id ? `#${node.id}` : '';
|
|
11
|
+
return `${node.tag}${id}${cls}`;
|
|
12
|
+
}
|
|
7
13
|
// ─── Node rendering (DOM tree) ───
|
|
8
14
|
function renderNode(node, indent) {
|
|
9
15
|
const lines = [];
|
|
@@ -57,6 +63,155 @@ function renderAncestors(chain) {
|
|
|
57
63
|
(a.inlineStyle ? ` style="${a.inlineStyle}"` : '');
|
|
58
64
|
});
|
|
59
65
|
}
|
|
66
|
+
function buildSketchTree(node, issueLabels) {
|
|
67
|
+
if (!node)
|
|
68
|
+
return null;
|
|
69
|
+
const label = nodeLabel(node);
|
|
70
|
+
const s = node.shape || {};
|
|
71
|
+
const shapeStr = s.role ? `[${s.role} h:${s.heightStrategy} w:${s.widthStrategy}]` : '';
|
|
72
|
+
const flags = [];
|
|
73
|
+
if (node.flags.overflowsParent)
|
|
74
|
+
flags.push('\u26A0parent-overflow');
|
|
75
|
+
if (node.flags.overflowsViewport)
|
|
76
|
+
flags.push('\u26A0viewport-overflow');
|
|
77
|
+
if (node.metrics.clientHeight === 0 && node.metrics.offsetHeight === 0)
|
|
78
|
+
flags.push('\u26A0collapsed');
|
|
79
|
+
if (node.flags.scrollable)
|
|
80
|
+
flags.push('\u2714scrollable');
|
|
81
|
+
if (node.containingBlockModifiers.length)
|
|
82
|
+
flags.push(`CB:${node.containingBlockModifiers.join(' ')}`);
|
|
83
|
+
const hasIssue = issueLabels.has(label) || flags.length > 0;
|
|
84
|
+
const children = [];
|
|
85
|
+
let omitted = 0;
|
|
86
|
+
for (const child of node.children) {
|
|
87
|
+
const sketchChild = buildSketchTree(child, issueLabels);
|
|
88
|
+
if (!sketchChild)
|
|
89
|
+
continue;
|
|
90
|
+
if (sketchChild.hasIssue || hasDescendantIssue(sketchChild)) {
|
|
91
|
+
children.push(sketchChild);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
omitted++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { label, shape: shapeStr, flags, children, hasIssue, omitted };
|
|
98
|
+
}
|
|
99
|
+
function hasDescendantIssue(node) {
|
|
100
|
+
if (node.hasIssue)
|
|
101
|
+
return true;
|
|
102
|
+
return node.children.some(c => hasDescendantIssue(c));
|
|
103
|
+
}
|
|
104
|
+
function renderSketchNode(node, prefix, isLast) {
|
|
105
|
+
const lines = [];
|
|
106
|
+
const connector = isLast ? '\u2514\u2500 ' : '\u251C\u2500 ';
|
|
107
|
+
const flagStr = node.flags.length ? ` ${node.flags.join(' ')}` : '';
|
|
108
|
+
const issueMark = node.hasIssue && node.flags.length === 0 ? ' \u26A0' : '';
|
|
109
|
+
lines.push(`${prefix}${connector}${node.label} ${node.shape}${flagStr}${issueMark}`);
|
|
110
|
+
const childPrefix = prefix + (isLast ? ' ' : '\u2502 ');
|
|
111
|
+
const totalChildren = node.children.length + (node.omitted > 0 ? 1 : 0);
|
|
112
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
113
|
+
lines.push(...renderSketchNode(node.children[i], childPrefix, i === totalChildren - 1));
|
|
114
|
+
}
|
|
115
|
+
if (node.omitted > 0) {
|
|
116
|
+
const omConnector = '\u2514\u2500 ';
|
|
117
|
+
lines.push(`${childPrefix}${omConnector}... (${node.omitted} subtrees omitted)`);
|
|
118
|
+
}
|
|
119
|
+
return lines;
|
|
120
|
+
}
|
|
121
|
+
function renderSketch(tree, findings) {
|
|
122
|
+
if (!tree)
|
|
123
|
+
return ['(empty tree)'];
|
|
124
|
+
const issueLabels = new Set();
|
|
125
|
+
for (const f of findings) {
|
|
126
|
+
if (f.level === 'warning' || f.level === 'error') {
|
|
127
|
+
if (f.location)
|
|
128
|
+
issueLabels.add(f.location);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const sketchRoot = buildSketchTree(tree, issueLabels);
|
|
132
|
+
if (!sketchRoot)
|
|
133
|
+
return ['(empty tree)'];
|
|
134
|
+
if (!hasDescendantIssue(sketchRoot) && !sketchRoot.hasIssue) {
|
|
135
|
+
return [`${sketchRoot.label} ${sketchRoot.shape} (no issues found in tree)`];
|
|
136
|
+
}
|
|
137
|
+
return renderSketchNode(sketchRoot, '', true);
|
|
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
|
+
}
|
|
60
215
|
// ─── Findings rendering ───
|
|
61
216
|
const CONF_BADGE = {
|
|
62
217
|
DEFINITE: 'DEFINITE',
|
|
@@ -85,6 +240,22 @@ function renderFindings(findings) {
|
|
|
85
240
|
}
|
|
86
241
|
return lines;
|
|
87
242
|
}
|
|
243
|
+
function renderBriefFindings(findings) {
|
|
244
|
+
const lines = [];
|
|
245
|
+
lines.push('## Findings');
|
|
246
|
+
const important = findings.filter(f => f.level === 'warning' || f.level === 'error');
|
|
247
|
+
if (important.length === 0) {
|
|
248
|
+
lines.push('- No warnings or errors');
|
|
249
|
+
return lines;
|
|
250
|
+
}
|
|
251
|
+
for (const f of important) {
|
|
252
|
+
const badge = `[${CONF_BADGE[f.confidence]}]`;
|
|
253
|
+
const loc = f.location ? ` @ ${f.location}` : '';
|
|
254
|
+
const icon = f.level === 'error' ? '\u26A0' : '\u26A0';
|
|
255
|
+
lines.push(`- ${icon} ${f.message} ${badge}${loc}`);
|
|
256
|
+
}
|
|
257
|
+
return lines;
|
|
258
|
+
}
|
|
88
259
|
function confidenceSummary(findings) {
|
|
89
260
|
const counts = findings.reduce((acc, f) => {
|
|
90
261
|
acc[f.confidence] = (acc[f.confidence] || 0) + 1;
|
|
@@ -98,7 +269,7 @@ function confidenceSummary(findings) {
|
|
|
98
269
|
return parts.join(' | ');
|
|
99
270
|
}
|
|
100
271
|
// ─── Markdown ───
|
|
101
|
-
function renderMarkdown(snapshot, findings) {
|
|
272
|
+
function renderMarkdown(snapshot, findings, brief = false, layout = false) {
|
|
102
273
|
const lines = [];
|
|
103
274
|
const vp = snapshot.viewport ? `${snapshot.viewport.width}\u00D7${snapshot.viewport.height}` : 'unknown';
|
|
104
275
|
lines.push('# cssprobe-cli report');
|
|
@@ -117,13 +288,26 @@ function renderMarkdown(snapshot, findings) {
|
|
|
117
288
|
lines.push('```');
|
|
118
289
|
lines.push(...renderAncestors(snapshot.ancestors));
|
|
119
290
|
lines.push('```');
|
|
291
|
+
if (layout) {
|
|
292
|
+
lines.push('');
|
|
293
|
+
lines.push(...renderLayout(snapshot, findings));
|
|
294
|
+
}
|
|
295
|
+
if (brief) {
|
|
296
|
+
lines.push('');
|
|
297
|
+
lines.push('## Tree Sketch (issues only)');
|
|
298
|
+
lines.push('```');
|
|
299
|
+
lines.push(...renderSketch(snapshot.tree, findings));
|
|
300
|
+
lines.push('```');
|
|
301
|
+
}
|
|
302
|
+
else if (!layout) {
|
|
303
|
+
lines.push('');
|
|
304
|
+
lines.push(`## DOM tree (${snapshot.downDepth} levels deep)`);
|
|
305
|
+
lines.push('```');
|
|
306
|
+
lines.push(...renderNode(snapshot.tree, 0));
|
|
307
|
+
lines.push('```');
|
|
308
|
+
}
|
|
120
309
|
lines.push('');
|
|
121
|
-
lines.push(
|
|
122
|
-
lines.push('```');
|
|
123
|
-
lines.push(...renderNode(snapshot.tree, 0));
|
|
124
|
-
lines.push('```');
|
|
125
|
-
lines.push('');
|
|
126
|
-
lines.push(...renderFindings(findings));
|
|
310
|
+
lines.push(...(brief ? renderBriefFindings(findings) : renderFindings(findings)));
|
|
127
311
|
if (snapshot.crossOriginBlocked && snapshot.crossOriginBlocked > 0) {
|
|
128
312
|
lines.push('');
|
|
129
313
|
lines.push('## Cross-origin limitation');
|
|
@@ -135,19 +319,42 @@ function renderMarkdown(snapshot, findings) {
|
|
|
135
319
|
return lines.join('\n');
|
|
136
320
|
}
|
|
137
321
|
// ─── JSON ───
|
|
138
|
-
function renderJSON(snapshot, findings) {
|
|
322
|
+
function renderJSON(snapshot, findings, brief = false) {
|
|
139
323
|
const counts = findings.reduce((acc, f) => {
|
|
140
324
|
acc[f.confidence] = (acc[f.confidence] || 0) + 1;
|
|
141
325
|
return acc;
|
|
142
326
|
}, {});
|
|
327
|
+
const meta = {
|
|
328
|
+
rootSelector: snapshot.rootSelector,
|
|
329
|
+
viewport: snapshot.viewport,
|
|
330
|
+
nodeCount: snapshot.nodeCount,
|
|
331
|
+
crossOriginBlocked: snapshot.crossOriginBlocked,
|
|
332
|
+
blockedSheetUrls: snapshot.blockedSheetUrls,
|
|
333
|
+
};
|
|
334
|
+
if (brief) {
|
|
335
|
+
const important = findings.filter(f => f.level === 'warning' || f.level === 'error');
|
|
336
|
+
const briefFindings = important.map(f => ({
|
|
337
|
+
id: f.id,
|
|
338
|
+
level: f.level,
|
|
339
|
+
message: f.message,
|
|
340
|
+
confidence: f.confidence,
|
|
341
|
+
location: f.location,
|
|
342
|
+
evidence: f.evidence.slice(0, 1),
|
|
343
|
+
}));
|
|
344
|
+
return {
|
|
345
|
+
meta,
|
|
346
|
+
snapshot: { rootSelector: snapshot.rootSelector, nodeCount: snapshot.nodeCount, downDepth: snapshot.downDepth },
|
|
347
|
+
findings: briefFindings,
|
|
348
|
+
summary: {
|
|
349
|
+
total: findings.length,
|
|
350
|
+
warnings: important.filter(f => f.level === 'warning').length,
|
|
351
|
+
errors: important.filter(f => f.level === 'error').length,
|
|
352
|
+
confidence: { DEFINITE: counts.DEFINITE || 0, INDEFINITE: counts.INDEFINITE || 0, UNVERIFIABLE: counts.UNVERIFIABLE || 0 },
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
143
356
|
return {
|
|
144
|
-
meta
|
|
145
|
-
rootSelector: snapshot.rootSelector,
|
|
146
|
-
viewport: snapshot.viewport,
|
|
147
|
-
nodeCount: snapshot.nodeCount,
|
|
148
|
-
crossOriginBlocked: snapshot.crossOriginBlocked,
|
|
149
|
-
blockedSheetUrls: snapshot.blockedSheetUrls,
|
|
150
|
-
},
|
|
357
|
+
meta,
|
|
151
358
|
snapshot: snapshot.error ? { error: snapshot.error, candidates: snapshot.candidates } : snapshot,
|
|
152
359
|
findings,
|
|
153
360
|
summary: {
|
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
|
|
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",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"depth": "string",
|
|
12
12
|
"max-nodes": "string",
|
|
13
13
|
"up-to": "string",
|
|
14
|
-
"state": "string"
|
|
14
|
+
"state": "string",
|
|
15
|
+
"brief": "boolean",
|
|
16
|
+
"layout": "boolean",
|
|
17
|
+
"wait": "boolean"
|
|
15
18
|
},
|
|
16
19
|
"args": [
|
|
17
20
|
"url",
|
|
@@ -38,6 +41,17 @@
|
|
|
38
41
|
"file"
|
|
39
42
|
]
|
|
40
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
|
+
},
|
|
41
55
|
"config-show": {
|
|
42
56
|
"help": "cssprobe-cli config-show \n\nShow current configuration\n\nOptions:\n --profile profile name",
|
|
43
57
|
"flags": {
|
|
@@ -102,6 +116,9 @@
|
|
|
102
116
|
"json",
|
|
103
117
|
"headed",
|
|
104
118
|
"zoom",
|
|
119
|
+
"brief",
|
|
120
|
+
"layout",
|
|
121
|
+
"wait",
|
|
105
122
|
"local"
|
|
106
123
|
]
|
|
107
124
|
}
|
|
@@ -40,7 +40,7 @@ function buildSkillMd() {
|
|
|
40
40
|
'npx playwright install chromium',
|
|
41
41
|
BT3,
|
|
42
42
|
'',
|
|
43
|
-
'Verify: ' + BT + 'cssprobe-cli
|
|
43
|
+
'Verify: ' + BT + 'cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --json | head -5' + BT,
|
|
44
44
|
'',
|
|
45
45
|
'## Command Discovery',
|
|
46
46
|
'',
|
|
@@ -56,10 +56,10 @@ function buildSkillMd() {
|
|
|
56
56
|
'',
|
|
57
57
|
BT3 + 'bash',
|
|
58
58
|
'# With explicit selector',
|
|
59
|
-
'cssprobe-cli
|
|
59
|
+
'cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body',
|
|
60
60
|
'',
|
|
61
61
|
'# Auto-detect root element (picks modal/dialog/scroll-container)',
|
|
62
|
-
'cssprobe-cli inspect https://
|
|
62
|
+
'cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout',
|
|
63
63
|
'',
|
|
64
64
|
'# Local HTML file',
|
|
65
65
|
'cssprobe-cli inspect ./test.html ".container"',
|
|
@@ -96,13 +96,46 @@ 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:',
|
|
102
135
|
'',
|
|
103
136
|
BT3 + 'bash',
|
|
104
|
-
'cssprobe-cli inspect https://
|
|
105
|
-
'cssprobe-cli
|
|
137
|
+
'cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --json',
|
|
138
|
+
'cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --json | jq ' + BT + "'.findings[] | {id, confidence, message}'" + BT,
|
|
106
139
|
BT3,
|
|
107
140
|
'',
|
|
108
141
|
'Without ' + BT + '--json' + BT + ', output is a Markdown report with ancestor chain,',
|
|
@@ -134,10 +167,13 @@ function buildSkillMd() {
|
|
|
134
167
|
' --headed Show browser window',
|
|
135
168
|
' --browser <engine> chromium|firefox|webkit (default chromium)',
|
|
136
169
|
' --zoom Run 1x/0.5x viewport diagnosis',
|
|
137
|
-
' --depth <n> DOM tree depth (default
|
|
170
|
+
' --depth <n> DOM tree depth (default: auto)',
|
|
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',
|
|
@@ -176,10 +212,10 @@ function buildSkillMd() {
|
|
|
176
212
|
'',
|
|
177
213
|
BT3 + 'bash',
|
|
178
214
|
'# Auto-detect and analyze',
|
|
179
|
-
'cssprobe-cli inspect https://
|
|
215
|
+
'cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout',
|
|
180
216
|
'',
|
|
181
217
|
'# Look for findings with level "warning" or "error"',
|
|
182
|
-
'cssprobe-cli
|
|
218
|
+
'cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --json | jq ' + BT + "'.findings[] | select(.level != \"info\")'" + BT,
|
|
183
219
|
BT3,
|
|
184
220
|
'',
|
|
185
221
|
'### 3. Inspect with authentication',
|
|
@@ -196,7 +232,7 @@ function buildSkillMd() {
|
|
|
196
232
|
'',
|
|
197
233
|
BT3 + 'bash',
|
|
198
234
|
'# Use --zoom to compare 1x vs 0.5x viewport',
|
|
199
|
-
'cssprobe-cli inspect https://
|
|
235
|
+
'cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --zoom',
|
|
200
236
|
BT3,
|
|
201
237
|
'',
|
|
202
238
|
'## Anti-patterns',
|
|
@@ -27,7 +27,7 @@ npm install -g cssprobe-cli
|
|
|
27
27
|
Or run without installing (slower but zero setup):
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
npx cssprobe-cli inspect https://
|
|
30
|
+
npx cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
Verify:
|
|
@@ -55,7 +55,7 @@ npx playwright install chromium
|
|
|
55
55
|
Verify:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
cssprobe-cli inspect https://
|
|
58
|
+
cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --json | head -5
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
JSON output = it works. `Browser not installed` = run the install command above.
|
|
@@ -68,10 +68,10 @@ JSON output = it works. `Browser not installed` = run the install command above.
|
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
# With explicit selector
|
|
71
|
-
cssprobe-cli inspect https://
|
|
71
|
+
cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body
|
|
72
72
|
|
|
73
73
|
# Auto-detect root element
|
|
74
|
-
cssprobe-cli inspect https://
|
|
74
|
+
cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout
|
|
75
75
|
|
|
76
76
|
# Local HTML file
|
|
77
77
|
cssprobe-cli inspect ./test.html ".container"
|
|
@@ -80,8 +80,8 @@ cssprobe-cli inspect ./test.html ".container"
|
|
|
80
80
|
### JSON Output (for programmatic consumption)
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
cssprobe-cli inspect https://
|
|
84
|
-
cssprobe-cli
|
|
83
|
+
cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --json
|
|
84
|
+
cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body --json | jq '.findings[] | {id, confidence, message}'
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
---
|
|
@@ -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
|
|
165
|
+
## Step 6: Understand the Output
|
|
131
166
|
|
|
132
167
|
### Markdown Report (default)
|
|
133
168
|
|
|
@@ -218,7 +253,7 @@ Priority (highest to lowest):
|
|
|
218
253
|
For agents that trust copy-paste:
|
|
219
254
|
|
|
220
255
|
```bash
|
|
221
|
-
npm install -g cssprobe-cli && npx playwright install chromium && cssprobe-cli inspect https://
|
|
256
|
+
npm install -g cssprobe-cli && npx playwright install chromium && cssprobe-cli inspect https://getbootstrap.com/docs/5.3/examples/checkout body
|
|
222
257
|
```
|
|
223
258
|
|
|
224
259
|
---
|
package/package.json
CHANGED