cssprobe-cli 0.3.1 → 0.4.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.
Files changed (40) hide show
  1. package/AGENTS.md +35 -7
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +54 -4
  4. package/dist/browser/frames.d.ts +15 -0
  5. package/dist/browser/frames.js +60 -0
  6. package/dist/browser/interactions.d.ts +1 -0
  7. package/dist/browser/interactions.js +9 -1
  8. package/dist/browser/launcher.d.ts +30 -2
  9. package/dist/browser/launcher.js +141 -34
  10. package/dist/browser/sampler.d.ts +43 -0
  11. package/dist/browser/sampler.js +111 -0
  12. package/dist/cli/commands.d.ts +3 -0
  13. package/dist/cli/commands.js +92 -5
  14. package/dist/cli/input.d.ts +6 -0
  15. package/dist/cli/input.js +24 -0
  16. package/dist/cli/program.js +152 -1
  17. package/dist/cli/session-ops.d.ts +8 -0
  18. package/dist/cli/session-ops.js +24 -1
  19. package/dist/cli/state-check.d.ts +17 -0
  20. package/dist/cli/state-check.js +43 -0
  21. package/dist/cli/url-guard.d.ts +2 -0
  22. package/dist/cli/url-guard.js +24 -0
  23. package/dist/daemon/daemon.js +67 -19
  24. package/dist/daemon/expect.d.ts +8 -0
  25. package/dist/daemon/expect.js +47 -0
  26. package/dist/daemon/flowRunner.d.ts +12 -1
  27. package/dist/daemon/flowRunner.js +22 -0
  28. package/dist/engine/evalSerializer.js +2 -2
  29. package/dist/help.json +94 -16
  30. package/dist/scenario/assertions.d.ts +13 -0
  31. package/dist/scenario/assertions.js +111 -0
  32. package/dist/scenario/reporters.d.ts +13 -0
  33. package/dist/scenario/reporters.js +79 -0
  34. package/dist/scenario/runner.d.ts +10 -0
  35. package/dist/scenario/runner.js +244 -0
  36. package/dist/scenario/schema.d.ts +1210 -0
  37. package/dist/scenario/schema.js +145 -0
  38. package/docs/guide/installation.md +1 -1
  39. package/llms.txt +1 -1
  40. package/package.json +1 -1
@@ -0,0 +1,43 @@
1
+ import type { Frame } from 'playwright-core';
2
+ export interface SamplePoint {
3
+ t: number;
4
+ value: unknown;
5
+ visibility: string;
6
+ error?: string;
7
+ }
8
+ export interface SampleSummary {
9
+ samples: number;
10
+ durationMs: number;
11
+ first: SamplePoint | null;
12
+ last: SamplePoint | null;
13
+ min: number | null;
14
+ max: number | null;
15
+ numeric: Array<[number, number]>;
16
+ changes: number;
17
+ hiddenMs: number;
18
+ tail: {
19
+ fromT: number;
20
+ min: number | null;
21
+ max: number | null;
22
+ windowMs: number;
23
+ };
24
+ }
25
+ export declare function extractNumber(value: unknown, key?: string): number | null;
26
+ export declare function summarizeSamples(points: SamplePoint[], key?: string, tailWindowMs?: number, tailFromMs?: number): SampleSummary;
27
+ export declare function isWithinTolerance(previous: unknown, next: unknown, tolerance: number): boolean;
28
+ export declare function sampleFrame(frame: Frame, expression: string, opts: {
29
+ intervalMs: number;
30
+ durationMs: number;
31
+ ignoreErrors?: boolean;
32
+ }): Promise<SamplePoint[]>;
33
+ export declare function waitForStableValue(frame: Frame, expression: string, opts: {
34
+ toleranceMs?: number;
35
+ windowMs?: number;
36
+ timeoutMs?: number;
37
+ intervalMs?: number;
38
+ tolerance?: number;
39
+ }): Promise<{
40
+ elapsedMs: number;
41
+ value: unknown;
42
+ samples: number;
43
+ }>;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractNumber = extractNumber;
4
+ exports.summarizeSamples = summarizeSamples;
5
+ exports.isWithinTolerance = isWithinTolerance;
6
+ exports.sampleFrame = sampleFrame;
7
+ exports.waitForStableValue = waitForStableValue;
8
+ function extractNumber(value, key) {
9
+ if (key) {
10
+ let current = value;
11
+ for (const part of key.split('.')) {
12
+ if (current === null || current === undefined)
13
+ return null;
14
+ current = current[part];
15
+ }
16
+ return typeof current === 'number' && Number.isFinite(current) ? current : null;
17
+ }
18
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
19
+ }
20
+ function summarizeSamples(points, key, tailWindowMs = 5000, tailFromMs) {
21
+ const numeric = [];
22
+ for (const point of points) {
23
+ const n = extractNumber(point.value, key);
24
+ if (n !== null)
25
+ numeric.push([point.t, n]);
26
+ }
27
+ const tailFrom = Math.max(0, tailFromMs ?? (points.length ? points[points.length - 1].t - tailWindowMs : 0));
28
+ const tailNumeric = numeric.filter(([t]) => t >= tailFrom).map(([, value]) => value);
29
+ let changes = 0;
30
+ let hiddenMs = 0;
31
+ for (let i = 0; i < points.length; i++) {
32
+ if (i > 0 && JSON.stringify(points[i].value) !== JSON.stringify(points[i - 1].value))
33
+ changes++;
34
+ if (i < points.length - 1 && points[i].visibility !== 'visible') {
35
+ hiddenMs += points[i + 1].t - points[i].t;
36
+ }
37
+ }
38
+ return {
39
+ samples: points.length,
40
+ durationMs: points.length ? points[points.length - 1].t : 0,
41
+ first: points[0] ?? null,
42
+ last: points[points.length - 1] ?? null,
43
+ min: numeric.length ? Math.min(...numeric.map(item => item[1])) : null,
44
+ max: numeric.length ? Math.max(...numeric.map(item => item[1])) : null,
45
+ numeric,
46
+ changes,
47
+ hiddenMs,
48
+ tail: {
49
+ fromT: tailFrom,
50
+ min: tailNumeric.length ? Math.min(...tailNumeric) : null,
51
+ max: tailNumeric.length ? Math.max(...tailNumeric) : null,
52
+ windowMs: tailWindowMs,
53
+ },
54
+ };
55
+ }
56
+ function isWithinTolerance(previous, next, tolerance) {
57
+ if (typeof previous === 'number' && typeof next === 'number') {
58
+ if (Number.isNaN(previous) || Number.isNaN(next))
59
+ return false;
60
+ return Math.abs(next - previous) <= tolerance;
61
+ }
62
+ return JSON.stringify(previous) === JSON.stringify(next);
63
+ }
64
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
65
+ async function readValue(frame, expression) {
66
+ return frame.evaluate(`(async () => ({ value: await (${expression}), visibility: document.visibilityState }))()`);
67
+ }
68
+ async function sampleFrame(frame, expression, opts) {
69
+ const intervalMs = Math.max(1, Math.floor(opts.intervalMs));
70
+ const started = Date.now();
71
+ const points = [];
72
+ while (Date.now() - started < opts.durationMs) {
73
+ const t = Date.now() - started;
74
+ try {
75
+ const { value, visibility } = await readValue(frame, expression);
76
+ points.push({ t, value, visibility });
77
+ }
78
+ catch (error) {
79
+ if (!opts.ignoreErrors)
80
+ throw error;
81
+ points.push({ t, value: null, visibility: 'unknown', error: error.message });
82
+ }
83
+ await sleep(intervalMs);
84
+ }
85
+ return points;
86
+ }
87
+ async function waitForStableValue(frame, expression, opts) {
88
+ const intervalMs = Math.max(1, opts.intervalMs ?? 200);
89
+ const windowMs = Math.max(1, opts.windowMs ?? 1500);
90
+ const timeoutMs = Math.max(1, opts.timeoutMs ?? 30000);
91
+ const tolerance = opts.tolerance ?? 0;
92
+ const started = Date.now();
93
+ let previous;
94
+ let stableSince = started;
95
+ let samples = 0;
96
+ while (Date.now() - started < timeoutMs) {
97
+ const { value } = await readValue(frame, expression);
98
+ samples++;
99
+ const now = Date.now();
100
+ if (samples === 1 || !isWithinTolerance(previous, value, tolerance)) {
101
+ previous = value;
102
+ stableSince = now;
103
+ }
104
+ else if (now - stableSince >= windowMs) {
105
+ return { elapsedMs: now - started, value, samples };
106
+ }
107
+ await sleep(intervalMs);
108
+ }
109
+ throw new Error(`wait-stable timed out after ${timeoutMs}ms; last value: ${JSON.stringify(previous)}`);
110
+ }
111
+ //# sourceMappingURL=sampler.js.map
@@ -14,14 +14,17 @@ export declare const inspectCommand: import("./command").CommandSchema<z.ZodObje
14
14
  brief: z.ZodOptional<z.ZodBoolean>;
15
15
  layout: z.ZodOptional<z.ZodBoolean>;
16
16
  depth: z.ZodOptional<z.ZodEffects<z.ZodNumber, number, unknown>>;
17
+ frame: z.ZodOptional<z.ZodString>;
17
18
  }, "strip", z.ZodTypeAny, {
18
19
  json?: boolean | undefined;
19
20
  brief?: boolean | undefined;
20
21
  layout?: boolean | undefined;
21
22
  depth?: number | undefined;
23
+ frame?: string | undefined;
22
24
  }, {
23
25
  json?: boolean | undefined;
24
26
  brief?: boolean | undefined;
25
27
  layout?: boolean | undefined;
26
28
  depth?: unknown;
29
+ frame?: string | undefined;
27
30
  }>>;
@@ -57,6 +57,8 @@ const open = (0, command_1.declareCommand)({
57
57
  headed: z.boolean().optional().describe('show the browser window'),
58
58
  viewport: z.string().optional().describe('viewport size as WxH (e.g. 1280x720)'),
59
59
  state: z.string().optional().describe('path to saved state file (cookies + localStorage)'),
60
+ 'expect-url': z.string().optional().describe('regex the final page URL must match'),
61
+ 'fail-on-login': z.boolean().optional().describe('fail when the page redirects to a login URL'),
60
62
  }),
61
63
  });
62
64
  const close = (0, command_1.declareCommand)({
@@ -72,6 +74,37 @@ const status = (0, command_1.declareCommand)({
72
74
  category: 'core',
73
75
  description: 'Show current session status',
74
76
  });
77
+ const framesCmd = (0, command_1.declareCommand)({
78
+ name: 'frames',
79
+ category: 'core',
80
+ description: 'List frames in the current page',
81
+ });
82
+ const scenarioCmd = (0, command_1.declareCommand)({
83
+ name: 'scenario',
84
+ category: 'core',
85
+ description: 'Run a JSON scenario suite (regression cases, artifacts, JUnit)',
86
+ args: z.object({
87
+ action: z.literal('run').describe('only "run" is supported'),
88
+ file: z.string().describe('scenario suite JSON path'),
89
+ }),
90
+ options: z.object({
91
+ state: z.string().optional().describe('fallback state file when the suite/case does not set one'),
92
+ artifacts: z.string().optional().describe('artifact directory (default: ~/.cssprobe-cli/scenario-runs/<suite>-<timestamp>)'),
93
+ junit: z.string().optional().describe('write a JUnit XML report to this path'),
94
+ bail: z.boolean().optional().describe('stop after the first failing case'),
95
+ headed: z.boolean().optional().describe('show the browser window'),
96
+ browser: z.enum(['chromium', 'firefox', 'webkit']).optional().describe('browser engine (default chromium)'),
97
+ viewport: z.string().optional().describe('viewport size as WxH (default: suite defaults or 1280x720)'),
98
+ }),
99
+ });
100
+ const doctor = (0, command_1.declareCommand)({
101
+ name: 'doctor',
102
+ category: 'core',
103
+ description: 'Diagnose browser installation and Playwright revision mismatch',
104
+ options: z.object({
105
+ engine: z.string().optional().describe('browser engine: chromium, firefox, webkit (default: chromium)'),
106
+ }),
107
+ });
75
108
  // ── cssprobe commands ──
76
109
  const inspect = (0, command_1.declareCommand)({
77
110
  name: 'inspect',
@@ -85,6 +118,7 @@ const inspect = (0, command_1.declareCommand)({
85
118
  brief: z.boolean().optional().describe('compact output: tree sketch + warnings/errors only'),
86
119
  layout: z.boolean().optional().describe('ASCII layout diagram showing element positions and sizes'),
87
120
  depth: numberArg.optional().describe('DOM tree depth (default: auto, max 20)'),
121
+ frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
88
122
  }),
89
123
  });
90
124
  const tree = (0, command_1.declareCommand)({
@@ -96,6 +130,7 @@ const tree = (0, command_1.declareCommand)({
96
130
  }),
97
131
  options: z.object({
98
132
  depth: numberArg.optional().describe('tree depth (default: auto, max 20)'),
133
+ frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
99
134
  }),
100
135
  });
101
136
  const layout = (0, command_1.declareCommand)({
@@ -105,6 +140,9 @@ const layout = (0, command_1.declareCommand)({
105
140
  args: z.object({
106
141
  selector: z.string().describe('CSS selector for the root element'),
107
142
  }),
143
+ options: z.object({
144
+ frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
145
+ }),
108
146
  });
109
147
  const findings = (0, command_1.declareCommand)({
110
148
  name: 'findings',
@@ -113,6 +151,9 @@ const findings = (0, command_1.declareCommand)({
113
151
  args: z.object({
114
152
  selector: z.string().describe('CSS selector for the root element'),
115
153
  }),
154
+ options: z.object({
155
+ frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
156
+ }),
116
157
  });
117
158
  const injectCss = (0, command_1.declareCommand)({
118
159
  name: 'inject-css',
@@ -121,6 +162,9 @@ const injectCss = (0, command_1.declareCommand)({
121
162
  args: z.object({
122
163
  css: z.string().describe('CSS code to inject'),
123
164
  }),
165
+ options: z.object({
166
+ frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
167
+ }),
124
168
  });
125
169
  // ── browser commands ──
126
170
  const screenshot = (0, command_1.declareCommand)({
@@ -146,7 +190,10 @@ const evalCmd = (0, command_1.declareCommand)({
146
190
  category: 'core',
147
191
  description: 'Evaluate JavaScript expression on the page (browser context)',
148
192
  args: z.object({
149
- expression: z.string().describe('JavaScript expression to evaluate'),
193
+ expression: z.string().describe('JavaScript expression, or @file / - (stdin) for long scripts'),
194
+ }),
195
+ options: z.object({
196
+ frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
150
197
  }),
151
198
  });
152
199
  const playwrightCmd = (0, command_1.declareCommand)({
@@ -154,7 +201,7 @@ const playwrightCmd = (0, command_1.declareCommand)({
154
201
  category: 'core',
155
202
  description: 'Execute Playwright API call (Node.js context)',
156
203
  args: z.object({
157
- call: z.string().describe('Playwright API call, e.g. "page.setViewportSize({width: 1280, height: 720})"'),
204
+ call: z.string().describe('Playwright API call, or @file / - (stdin); e.g. "page.setViewportSize({width: 1280, height: 720})"'),
158
205
  }),
159
206
  });
160
207
  // ── interaction ──
@@ -215,6 +262,37 @@ const waitForCmd = (0, command_1.declareCommand)({
215
262
  timeout: numberArg.optional().describe('timeout in ms (default 30000)'),
216
263
  }),
217
264
  });
265
+ const sampleCmd = (0, command_1.declareCommand)({
266
+ name: 'sample',
267
+ category: 'core',
268
+ description: 'Sample a JS expression over time and return a timeline with summary stats',
269
+ args: z.object({
270
+ expression: z.string().describe('JS expression sampled on an interval'),
271
+ }),
272
+ options: z.object({
273
+ interval: numberArg.optional().describe('sampling interval in ms (default 250)'),
274
+ duration: numberArg.optional().describe('total sampling duration in ms (default 5000)'),
275
+ key: z.string().optional().describe('dot path for numeric extraction, e.g. "top" or "box.y"'),
276
+ 'tail-from': numberArg.optional().describe('tail starts at this elapsed ms (default: last 5000ms)'),
277
+ 'ignore-errors': z.boolean().optional().describe('record evaluation errors as sample points instead of aborting'),
278
+ frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
279
+ }),
280
+ });
281
+ const waitStableCmd = (0, command_1.declareCommand)({
282
+ name: 'wait-stable',
283
+ category: 'core',
284
+ description: 'Wait until a JS expression stops changing (tolerance + stable window)',
285
+ args: z.object({
286
+ expression: z.string().describe('JS expression polled until stable'),
287
+ }),
288
+ options: z.object({
289
+ tolerance: numberArg.optional().describe('absolute tolerance for numeric values (default 0)'),
290
+ window: numberArg.optional().describe('stable window in ms (default 1500)'),
291
+ timeout: numberArg.optional().describe('timeout in ms (default 30000)'),
292
+ interval: numberArg.optional().describe('polling interval in ms (default 200)'),
293
+ frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
294
+ }),
295
+ });
218
296
  const runCmd = (0, command_1.declareCommand)({
219
297
  name: 'run',
220
298
  category: 'core',
@@ -248,6 +326,15 @@ const stateSave = (0, command_1.declareCommand)({
248
326
  name: z.string().optional().describe('state file name (default: session)'),
249
327
  }),
250
328
  });
329
+ const stateCheck = (0, command_1.declareCommand)({
330
+ name: 'state-check',
331
+ category: 'core',
332
+ description: 'Validate a saved state file: cookies, expiry, and target domain coverage',
333
+ options: z.object({
334
+ state: z.string().optional().describe('state file path (default: ~/.cssprobe-cli/states/session.json)'),
335
+ url: z.string().optional().describe('target URL whose hostname must be covered by the cookies'),
336
+ }),
337
+ });
251
338
  // ── config ──
252
339
  const configShow = (0, command_1.declareCommand)({
253
340
  name: 'config-show',
@@ -343,15 +430,15 @@ const mcpUninstall = (0, command_1.declareCommand)({
343
430
  // ── export ──
344
431
  const commandsArray = [
345
432
  // session management
346
- open, close, status,
433
+ open, close, status, framesCmd, doctor, scenarioCmd,
347
434
  // cssprobe
348
435
  inspect, tree, layout, findings, injectCss,
349
436
  // browser
350
437
  resize, evalCmd, playwrightCmd, screenshot,
351
438
  // interaction
352
- click, hover, typeCmd, waitForCmd, runCmd,
439
+ click, hover, typeCmd, waitForCmd, sampleCmd, waitStableCmd, runCmd,
353
440
  // state
354
- stateImport, stateSave,
441
+ stateImport, stateSave, stateCheck,
355
442
  // config
356
443
  configShow, configSet, configList, configUse, configNew, configPath,
357
444
  // skill
@@ -0,0 +1,6 @@
1
+ export interface InputSource {
2
+ readFile?: (file: string) => string;
3
+ readStdin?: () => string;
4
+ exists?: (file: string) => boolean;
5
+ }
6
+ export declare function resolveInputArg(value: string, source?: InputSource): string;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveInputArg = resolveInputArg;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function resolveInputArg(value, source = {}) {
10
+ const exists = source.exists ?? fs_1.default.existsSync;
11
+ const readFile = source.readFile ?? ((file) => fs_1.default.readFileSync(file, 'utf-8'));
12
+ if (value === '-') {
13
+ const readStdin = source.readStdin ?? (() => fs_1.default.readFileSync(0, 'utf-8'));
14
+ return readStdin();
15
+ }
16
+ if (value.startsWith('@')) {
17
+ const file = path_1.default.resolve(value.slice(1));
18
+ if (!exists(file))
19
+ throw new Error(`File not found: ${file}`);
20
+ return readFile(file);
21
+ }
22
+ return value;
23
+ }
24
+ //# sourceMappingURL=input.js.map
@@ -9,11 +9,17 @@ const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
10
10
  const minimist_1 = require("./minimist");
11
11
  const command_1 = require("./command");
12
+ const input_1 = require("./input");
12
13
  const commands_1 = require("./commands");
13
14
  const output_1 = require("./output");
14
15
  const config_1 = require("../config/config");
15
16
  const session_1 = require("../daemon/session");
16
17
  const session_ops_1 = require("./session-ops");
18
+ const state_check_1 = require("./state-check");
19
+ const launcher_1 = require("../browser/launcher");
20
+ const schema_1 = require("../scenario/schema");
21
+ const runner_1 = require("../scenario/runner");
22
+ const reporters_1 = require("../scenario/reporters");
17
23
  const mcp_1 = require("../mcp");
18
24
  const globalOptions = ['json', 'raw', 'help', 'h', 'version', 'v', 'p', 'profile'];
19
25
  const booleanGlobalOptions = ['help', 'json', 'raw', 'version', 'v', 'h'];
@@ -52,15 +58,25 @@ async function program() {
52
58
  case 'close':
53
59
  await handleClose(command, rawArgs, output);
54
60
  return;
61
+ case 'doctor':
62
+ handleDoctor(command, rawArgs, output);
63
+ return;
64
+ case 'scenario':
65
+ await handleScenario(command, rawArgs, output);
66
+ return;
55
67
  case 'status':
56
68
  await handleStatus(output);
57
69
  return;
58
70
  case 'state-import':
59
71
  await handleStateImport(command, rawArgs, output);
60
72
  return;
73
+ case 'state-check':
74
+ handleStateCheck(command, rawArgs, output);
75
+ return;
61
76
  case 'state-save':
62
77
  await handleStateSave(command, rawArgs, output);
63
78
  return;
79
+ case 'frames':
64
80
  case 'inspect':
65
81
  case 'tree':
66
82
  case 'layout':
@@ -74,6 +90,8 @@ async function program() {
74
90
  case 'hover':
75
91
  case 'type':
76
92
  case 'wait-for':
93
+ case 'sample':
94
+ case 'wait-stable':
77
95
  await handleSessionCommand(commandName, command, rawArgs, output);
78
96
  return;
79
97
  case 'run':
@@ -148,6 +166,8 @@ async function handleOpen(command, args, output) {
148
166
  headed: !!parsed.headed,
149
167
  viewport,
150
168
  state: parsed.state,
169
+ expectUrl: parsed['expect-url'],
170
+ failOnLogin: !!parsed['fail-on-login'],
151
171
  });
152
172
  if (output.json) {
153
173
  console.log(JSON.stringify(result, null, 2));
@@ -183,11 +203,100 @@ async function handleClose(command, args, output) {
183
203
  output.error(e instanceof Error ? e.message : String(e));
184
204
  }
185
205
  }
206
+ async function handleScenario(command, args, output) {
207
+ try {
208
+ const cmdArgs = splitArgs(args);
209
+ const parsed = (0, command_1.parseCommand)(command, cmdArgs);
210
+ const file = path_1.default.resolve(parsed.file);
211
+ if (!fs_1.default.existsSync(file))
212
+ output.error(`Scenario suite not found: ${file}`);
213
+ const raw = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
214
+ const validation = schema_1.suiteSchema.safeParse(raw);
215
+ if (!validation.success) {
216
+ const issues = validation.error.issues
217
+ .map(issue => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
218
+ .join('\n');
219
+ output.error(`Invalid scenario suite:\n${issues}`);
220
+ }
221
+ let viewport;
222
+ const viewportStr = parsed.viewport;
223
+ if (viewportStr) {
224
+ const parts = viewportStr.split('x');
225
+ const width = parseInt(parts[0], 10);
226
+ const height = parseInt(parts[1], 10);
227
+ if (parts.length !== 2 || isNaN(width) || isNaN(height)) {
228
+ output.error('Invalid viewport format. Use WxH (e.g. 1280x720)');
229
+ }
230
+ viewport = { width, height };
231
+ }
232
+ const result = await (0, runner_1.runScenario)(validation.data, file, {
233
+ artifactsDir: parsed.artifacts,
234
+ junitPath: parsed.junit,
235
+ bail: !!parsed.bail,
236
+ headed: !!parsed.headed,
237
+ browser: parsed.browser,
238
+ viewport,
239
+ state: parsed.state,
240
+ });
241
+ if (output.json) {
242
+ console.log(JSON.stringify(result, null, 2));
243
+ }
244
+ else {
245
+ console.log((0, reporters_1.formatConsole)(result.suite, result.results));
246
+ console.log(`Artifacts: ${result.artifactsDir}`);
247
+ if (parsed.junit)
248
+ console.log(`JUnit: ${path_1.default.resolve(parsed.junit)}`);
249
+ }
250
+ if (!result.ok)
251
+ process.exit(1);
252
+ }
253
+ catch (e) {
254
+ output.error(e instanceof Error ? e.message : String(e));
255
+ }
256
+ }
257
+ function handleDoctor(command, args, output) {
258
+ try {
259
+ const cmdArgs = splitArgs(args);
260
+ const parsed = (0, command_1.parseCommand)(command, cmdArgs);
261
+ const diagnosis = (0, launcher_1.diagnoseBrowser)(parsed.engine);
262
+ if (output.json) {
263
+ console.log(JSON.stringify(diagnosis, null, 2));
264
+ return;
265
+ }
266
+ console.log(`Platform: ${diagnosis.platform}`);
267
+ console.log(`Cache roots: ${diagnosis.cacheRoots.join(', ')}`);
268
+ console.log(`Expected: chromium ${diagnosis.expected.chromium ?? 'unknown'}, headless-shell ${diagnosis.expected.headlessShell ?? 'unknown'} (playwright-core ${diagnosis.expected.playwrightVersion ?? 'unknown'})`);
269
+ if (diagnosis.installs.length) {
270
+ console.log('Installed:');
271
+ for (const install of diagnosis.installs) {
272
+ console.log(` ${install.kind}-${install.revision}${install.executable ? '' : ' (no executable)'} ${install.dir}`);
273
+ }
274
+ }
275
+ else {
276
+ console.log('Installed: none');
277
+ }
278
+ console.log(`Status: ${diagnosis.status}`);
279
+ if (diagnosis.hint)
280
+ console.log(`Hint: ${diagnosis.hint}`);
281
+ }
282
+ catch (e) {
283
+ output.error(e instanceof Error ? e.message : String(e));
284
+ }
285
+ }
186
286
  async function handleStatus(output) {
187
287
  try {
188
288
  const result = await (0, session_ops_1.getSessionStatus)();
289
+ let page = null;
290
+ if (result.alive) {
291
+ try {
292
+ page = await (0, session_ops_1.getPageInfo)();
293
+ }
294
+ catch {
295
+ page = null;
296
+ }
297
+ }
189
298
  if (output.json) {
190
- console.log(JSON.stringify(result, null, 2));
299
+ console.log(JSON.stringify({ ...result, page }, null, 2));
191
300
  return;
192
301
  }
193
302
  if (!result.config) {
@@ -198,7 +307,42 @@ async function handleStatus(output) {
198
307
  console.log(`Status: ${result.alive ? 'alive' : 'dead'}`);
199
308
  if (result.alive) {
200
309
  console.log(`Browser: ${result.config.browser.browserName}`);
310
+ if (page) {
311
+ console.log(`URL: ${page.url}`);
312
+ console.log(`Title: ${page.title}`);
313
+ console.log(`Frames: ${page.frames}`);
314
+ }
315
+ }
316
+ }
317
+ catch (e) {
318
+ output.error(e instanceof Error ? e.message : String(e));
319
+ }
320
+ }
321
+ function handleStateCheck(command, args, output) {
322
+ try {
323
+ const cmdArgs = splitArgs(args);
324
+ const parsed = (0, command_1.parseCommand)(command, cmdArgs);
325
+ const file = path_1.default.resolve(parsed.state ??
326
+ path_1.default.join(os_1.default.homedir(), '.cssprobe-cli', 'states', 'session.json'));
327
+ if (!fs_1.default.existsSync(file))
328
+ output.error(`State file not found: ${file}`);
329
+ const raw = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
330
+ if (!raw || !Array.isArray(raw.cookies))
331
+ throw new Error(`Invalid state file (missing cookies): ${file}`);
332
+ const report = (0, state_check_1.analyzeState)(raw, parsed.url);
333
+ if (output.json) {
334
+ console.log(JSON.stringify({ file, ...report }, null, 2));
335
+ return;
336
+ }
337
+ console.log(`State: ${file}`);
338
+ console.log(`Cookies: ${report.cookies} (${report.sessionCookies} session)`);
339
+ console.log(`Domains: ${report.domains.join(', ') || '(none)'}`);
340
+ if (report.targetMatches !== null) {
341
+ console.log(`Target match: ${report.targetMatches ? 'yes' : 'no'}`);
201
342
  }
343
+ console.log(`Status: ${report.ok ? 'ok' : 'attention'}`);
344
+ for (const warning of report.warnings)
345
+ console.log(`Warning: ${warning}`);
202
346
  }
203
347
  catch (e) {
204
348
  output.error(e instanceof Error ? e.message : String(e));
@@ -217,6 +361,13 @@ async function handleRun(command, args, output) {
217
361
  async function handleSessionCommand(commandName, command, args, output) {
218
362
  try {
219
363
  const cmdArgs = splitArgs(args);
364
+ if (commandName === 'eval' || commandName === 'playwright') {
365
+ const input = cmdArgs._[1];
366
+ if (typeof input === 'string') {
367
+ cmdArgs._ = cmdArgs._.slice();
368
+ cmdArgs._[1] = (0, input_1.resolveInputArg)(input);
369
+ }
370
+ }
220
371
  const parsed = (0, command_1.parseCommand)(command, cmdArgs);
221
372
  const result = await (0, session_ops_1.runSessionCommand)(commandName, command, parsed, !!args.json);
222
373
  // Screenshot: save the base64 data URL to a file and report the path
@@ -11,6 +11,8 @@ export interface OpenOptions {
11
11
  height: number;
12
12
  };
13
13
  state?: string;
14
+ expectUrl?: string;
15
+ failOnLogin?: boolean;
14
16
  }
15
17
  export interface OpenResult {
16
18
  sessionId: string;
@@ -21,6 +23,12 @@ export interface OpenResult {
21
23
  height: number;
22
24
  };
23
25
  }
26
+ export interface PageInfo {
27
+ url: string;
28
+ title: string;
29
+ frames: number;
30
+ }
31
+ export declare function getPageInfo(): Promise<PageInfo>;
24
32
  export declare function openSession(opts: OpenOptions): Promise<OpenResult>;
25
33
  export declare function waitForSession(timeoutMs: number): Promise<Session | undefined>;
26
34
  export interface CloseResult {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.saveScreenshotFile = void 0;
4
+ exports.getPageInfo = getPageInfo;
4
5
  exports.openSession = openSession;
5
6
  exports.waitForSession = waitForSession;
6
7
  exports.closeSession = closeSession;
@@ -8,8 +9,16 @@ exports.getSessionStatus = getSessionStatus;
8
9
  exports.buildDaemonArgs = buildDaemonArgs;
9
10
  exports.runSessionCommand = runSessionCommand;
10
11
  const session_1 = require("../daemon/session");
12
+ const url_guard_1 = require("./url-guard");
11
13
  var screenshot_1 = require("../utils/screenshot");
12
14
  Object.defineProperty(exports, "saveScreenshotFile", { enumerable: true, get: function () { return screenshot_1.saveScreenshotFile; } });
15
+ async function getPageInfo() {
16
+ const session = await (0, session_1.loadSession)();
17
+ if (!session)
18
+ throw new Error('No active session. Run: cssprobe-cli open <url>');
19
+ const result = await session.run({ _: ['page-info'] });
20
+ return JSON.parse(result.text);
21
+ }
13
22
  async function openSession(opts) {
14
23
  const url = opts.url || 'about:blank';
15
24
  const browser = opts.browser || 'chromium';
@@ -24,10 +33,24 @@ async function openSession(opts) {
24
33
  if (!session)
25
34
  throw new Error('Daemon failed to become ready within 5s. Check the daemon log for errors.');
26
35
  await session.run({ _: ['goto', url] });
36
+ const info = await getPageInfo();
37
+ const guardError = (() => {
38
+ if (opts.failOnLogin && (0, url_guard_1.looksLikeLoginUrl)(info.url)) {
39
+ return `Login required: page redirected to ${info.url}. Refresh the state file or log in manually.`;
40
+ }
41
+ if (opts.expectUrl && !(0, url_guard_1.matchesUrlPattern)(opts.expectUrl, info.url)) {
42
+ return `Expected URL to match ${opts.expectUrl}, got ${info.url}`;
43
+ }
44
+ return null;
45
+ })();
46
+ if (guardError) {
47
+ await closeSession();
48
+ throw new Error(guardError);
49
+ }
27
50
  return {
28
51
  sessionId: 'default',
29
52
  pid,
30
- url,
53
+ url: info.url,
31
54
  viewport: opts.viewport || { width: 1280, height: 720 },
32
55
  };
33
56
  }