cssprobe-cli 0.3.0 → 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 (44) hide show
  1. package/AGENTS.md +59 -6
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +111 -2
  4. package/dist/browser/frames.d.ts +15 -0
  5. package/dist/browser/frames.js +60 -0
  6. package/dist/browser/interactions.d.ts +49 -0
  7. package/dist/browser/interactions.js +96 -0
  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 +162 -4
  14. package/dist/cli/input.d.ts +6 -0
  15. package/dist/cli/input.js +24 -0
  16. package/dist/cli/program.js +171 -1
  17. package/dist/cli/session-ops.d.ts +10 -1
  18. package/dist/cli/session-ops.js +27 -56
  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 +162 -18
  24. package/dist/daemon/expect.d.ts +8 -0
  25. package/dist/daemon/expect.js +47 -0
  26. package/dist/daemon/flowRunner.d.ts +73 -0
  27. package/dist/daemon/flowRunner.js +177 -0
  28. package/dist/engine/evalSerializer.d.ts +1 -0
  29. package/dist/engine/evalSerializer.js +43 -0
  30. package/dist/help.json +156 -16
  31. package/dist/installer/skill-template.js +43 -0
  32. package/dist/scenario/assertions.d.ts +13 -0
  33. package/dist/scenario/assertions.js +111 -0
  34. package/dist/scenario/reporters.d.ts +13 -0
  35. package/dist/scenario/reporters.js +79 -0
  36. package/dist/scenario/runner.d.ts +10 -0
  37. package/dist/scenario/runner.js +244 -0
  38. package/dist/scenario/schema.d.ts +1210 -0
  39. package/dist/scenario/schema.js +145 -0
  40. package/dist/utils/screenshot.d.ts +1 -0
  41. package/dist/utils/screenshot.js +58 -0
  42. package/docs/guide/installation.md +1 -1
  43. package/llms.txt +1 -1
  44. package/package.json +1 -1
@@ -6,6 +6,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  };
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.BrowserLauncher = void 0;
9
+ exports.browserCacheRoots = browserCacheRoots;
10
+ exports.resolveExecutableIn = resolveExecutableIn;
11
+ exports.listBrowserInstalls = listBrowserInstalls;
12
+ exports.expectedBrowsers = expectedBrowsers;
13
+ exports.diagnoseBrowser = diagnoseBrowser;
14
+ exports.findBrowserExecutable = findBrowserExecutable;
9
15
  const fs_1 = __importDefault(require("fs"));
10
16
  const path_1 = __importDefault(require("path"));
11
17
  const os_1 = __importDefault(require("os"));
@@ -21,39 +27,133 @@ function loadBundle() {
21
27
  bundleContent = fs_1.default.readFileSync(COLLECTOR_BUNDLE_PATH, 'utf-8');
22
28
  return bundleContent;
23
29
  }
24
- /** Try to find an existing browser executable in the Playwright cache. */
25
- function findBrowserExecutable(engine) {
26
- const cacheDir = path_1.default.join(os_1.default.homedir(), 'Library', 'Caches', 'ms-playwright');
27
- if (!fs_1.default.existsSync(cacheDir))
28
- return null;
29
- const platform = process.platform;
30
- if (engine === 'chromium') {
31
- // Look for chromium-* directories (sorted newest first)
32
- const dirs = fs_1.default.readdirSync(cacheDir)
33
- .filter(d => d.startsWith('chromium-'))
34
- .sort((a, b) => {
35
- const na = parseInt(a.split('-')[1]) || 0;
36
- const nb = parseInt(b.split('-')[1]) || 0;
37
- return nb - na;
38
- });
39
- for (const dir of dirs) {
40
- if (platform === 'darwin') {
41
- const app = path_1.default.join(cacheDir, dir, 'chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing');
42
- if (fs_1.default.existsSync(app))
43
- return app;
44
- const appIntel = path_1.default.join(cacheDir, dir, 'chrome-mac', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing');
45
- if (fs_1.default.existsSync(appIntel))
46
- return appIntel;
47
- }
48
- else if (platform === 'linux') {
49
- const bin = path_1.default.join(cacheDir, dir, 'chrome-linux', 'chrome');
50
- if (fs_1.default.existsSync(bin))
51
- return bin;
52
- }
53
- }
30
+ function browserCacheRoots() {
31
+ const fromEnv = process.env.PLAYWRIGHT_BROWSERS_PATH;
32
+ if (fromEnv)
33
+ return [fromEnv];
34
+ if (process.platform === 'darwin') {
35
+ return [path_1.default.join(os_1.default.homedir(), 'Library', 'Caches', 'ms-playwright')];
36
+ }
37
+ if (process.platform === 'win32') {
38
+ const localAppData = process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Local');
39
+ return [path_1.default.join(localAppData, 'ms-playwright')];
40
+ }
41
+ return [path_1.default.join(os_1.default.homedir(), '.cache', 'ms-playwright')];
42
+ }
43
+ function resolveExecutableIn(dir, platform) {
44
+ const candidates = [];
45
+ if (platform === 'darwin') {
46
+ candidates.push(path_1.default.join(dir, 'chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'));
47
+ candidates.push(path_1.default.join(dir, 'chrome-mac', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'));
48
+ }
49
+ else if (platform === 'linux') {
50
+ candidates.push(path_1.default.join(dir, 'chrome-linux', 'chrome'));
51
+ candidates.push(path_1.default.join(dir, 'chrome-linux64', 'chrome'));
52
+ }
53
+ else if (platform === 'win32') {
54
+ candidates.push(path_1.default.join(dir, 'chrome-win', 'chrome.exe'));
55
+ candidates.push(path_1.default.join(dir, 'chrome-win64', 'chrome.exe'));
56
+ }
57
+ for (const candidate of candidates) {
58
+ if (fs_1.default.existsSync(candidate))
59
+ return candidate;
54
60
  }
55
61
  return null;
56
62
  }
63
+ function listBrowserInstalls(cacheRoots = browserCacheRoots()) {
64
+ const installs = [];
65
+ for (const root of cacheRoots) {
66
+ if (!fs_1.default.existsSync(root))
67
+ continue;
68
+ for (const entry of fs_1.default.readdirSync(root)) {
69
+ const match = /^(chromium|chromium_headless_shell)-(\d+)$/.exec(entry);
70
+ if (!match)
71
+ continue;
72
+ const dir = path_1.default.join(root, entry);
73
+ installs.push({
74
+ kind: match[1] === 'chromium' ? 'chromium' : 'chromium-headless-shell',
75
+ revision: parseInt(match[2], 10),
76
+ dir,
77
+ executable: resolveExecutableIn(dir, process.platform),
78
+ });
79
+ }
80
+ }
81
+ return installs.sort((a, b) => b.revision - a.revision);
82
+ }
83
+ function expectedBrowsers() {
84
+ try {
85
+ const pkgPath = require.resolve('playwright-core/package.json');
86
+ const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
87
+ const browsersPath = path_1.default.join(path_1.default.dirname(pkgPath), 'browsers.json');
88
+ const browsers = JSON.parse(fs_1.default.readFileSync(browsersPath, 'utf-8'));
89
+ const chromium = browsers.browsers?.find(browser => browser.name === 'chromium');
90
+ const headlessShell = browsers.browsers?.find(browser => browser.name === 'chromium-headless-shell');
91
+ return {
92
+ chromium: chromium ? parseInt(chromium.revision, 10) : undefined,
93
+ headlessShell: headlessShell ? parseInt(headlessShell.revision, 10) : undefined,
94
+ playwrightVersion: pkg.version,
95
+ };
96
+ }
97
+ catch {
98
+ return {};
99
+ }
100
+ }
101
+ function installCommand(expected) {
102
+ return expected.playwrightVersion
103
+ ? `npx playwright@${expected.playwrightVersion} install chromium`
104
+ : 'npx playwright install chromium';
105
+ }
106
+ function diagnoseBrowser(engine = 'chromium', cacheRoots = browserCacheRoots()) {
107
+ const expected = expectedBrowsers();
108
+ const installs = listBrowserInstalls(cacheRoots);
109
+ const chromiumInstalls = installs.filter(install => install.kind === 'chromium' && install.executable);
110
+ const exact = expected.chromium
111
+ ? chromiumInstalls.some(install => install.revision === expected.chromium)
112
+ : chromiumInstalls.length > 0;
113
+ if (engine !== 'chromium') {
114
+ return {
115
+ platform: process.platform,
116
+ cacheRoots,
117
+ expected,
118
+ installs,
119
+ status: 'missing',
120
+ hint: `Automatic discovery only covers chromium; run "npx playwright install ${engine}" if launch fails.`,
121
+ };
122
+ }
123
+ if (exact) {
124
+ return { platform: process.platform, cacheRoots, expected, installs, status: 'exact', hint: null };
125
+ }
126
+ if (chromiumInstalls.length > 0) {
127
+ const found = chromiumInstalls.map(install => install.revision).join(', ');
128
+ return {
129
+ platform: process.platform,
130
+ cacheRoots,
131
+ expected,
132
+ installs,
133
+ status: 'fallback',
134
+ hint: `Found chromium revision(s) ${found}, expected ${expected.chromium ?? 'unknown'}. Launch may work through the fallback. To install the expected build: ${installCommand(expected)}`,
135
+ };
136
+ }
137
+ return {
138
+ platform: process.platform,
139
+ cacheRoots,
140
+ expected,
141
+ installs,
142
+ status: 'missing',
143
+ hint: `No chromium install found. Install it with: ${installCommand(expected)}`,
144
+ };
145
+ }
146
+ /** Try to find an existing browser executable in the Playwright cache. */
147
+ function findBrowserExecutable(engine, cacheRoots = browserCacheRoots()) {
148
+ if (engine !== 'chromium')
149
+ return null;
150
+ const installs = listBrowserInstalls(cacheRoots).filter(install => install.kind === 'chromium' && install.executable);
151
+ if (!installs.length)
152
+ return null;
153
+ const expected = expectedBrowsers().chromium;
154
+ const exact = installs.find(install => install.revision === expected);
155
+ return (exact ?? installs[0]).executable;
156
+ }
57
157
  class BrowserLauncher {
58
158
  options;
59
159
  browser = null;
@@ -74,7 +174,13 @@ class BrowserLauncher {
74
174
  if (execPath) {
75
175
  launchOptions.executablePath = execPath;
76
176
  }
77
- this.browser = await factory.launch(launchOptions);
177
+ try {
178
+ this.browser = await factory.launch(launchOptions);
179
+ }
180
+ catch (error) {
181
+ const diagnosis = diagnoseBrowser(engine);
182
+ throw new Error(`${error.message}\nBrowser diagnosis: ${diagnosis.status}. Run "cssprobe-cli doctor" for details.`);
183
+ }
78
184
  // Load saved state if provided
79
185
  const vp = this.options.viewport || { width: 1280, height: 720 };
80
186
  const contextOptions = { viewport: vp };
@@ -98,12 +204,13 @@ class BrowserLauncher {
98
204
  await this.page.goto(`file://${resolved}`, { waitUntil: 'load', timeout: 30000 });
99
205
  }
100
206
  }
101
- async collect(cfg) {
207
+ async collect(cfg, target) {
102
208
  if (!this.page)
103
209
  throw new Error('Browser not opened. Call open() first.');
210
+ const host = target ?? this.page;
104
211
  const bundle = loadBundle();
105
- await this.page.addScriptTag({ content: bundle });
106
- const snapshot = await this.page.evaluate((config) => {
212
+ await host.addScriptTag({ content: bundle });
213
+ const snapshot = await host.evaluate((config) => {
107
214
  return window.__cssprobe_cli.collect(config);
108
215
  }, cfg);
109
216
  return snapshot;
@@ -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,107 @@ 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})"'),
205
+ }),
206
+ });
207
+ // ── interaction ──
208
+ const click = (0, command_1.declareCommand)({
209
+ name: 'click',
210
+ category: 'core',
211
+ description: 'Click an element with real mouse events (works with custom React components)',
212
+ args: z.object({
213
+ selector: z.string().describe('CSS selector'),
214
+ }),
215
+ options: z.object({
216
+ nth: numberArg.optional().describe('1-based match index (default: first match)'),
217
+ force: z.boolean().optional().describe('skip actionability checks'),
218
+ timeout: numberArg.optional().describe('action timeout in ms (default 30000)'),
219
+ }),
220
+ });
221
+ const hover = (0, command_1.declareCommand)({
222
+ name: 'hover',
223
+ category: 'core',
224
+ description: 'Hover an element with real mouse events',
225
+ args: z.object({
226
+ selector: z.string().describe('CSS selector'),
227
+ }),
228
+ options: z.object({
229
+ nth: numberArg.optional().describe('1-based match index (default: first match)'),
230
+ force: z.boolean().optional().describe('skip actionability checks'),
231
+ timeout: numberArg.optional().describe('action timeout in ms (default 30000)'),
232
+ }),
233
+ });
234
+ const typeCmd = (0, command_1.declareCommand)({
235
+ name: 'type',
236
+ category: 'core',
237
+ description: 'Type text into an element (fill by default; --sequential for real key events)',
238
+ args: z.object({
239
+ selector: z.string().describe('CSS selector'),
240
+ text: z.string().describe('text to type'),
241
+ }),
242
+ options: z.object({
243
+ press: z.string().optional().describe('key to press after typing (e.g. Enter, Tab, Control+A)'),
244
+ sequential: z.boolean().optional().describe('type with real key events (for custom editors)'),
245
+ clear: z.boolean().optional().describe('clear existing content before sequential typing'),
246
+ delay: numberArg.optional().describe('delay between keystrokes in ms (--sequential, default 0)'),
247
+ nth: numberArg.optional().describe('1-based match index (default: first match)'),
248
+ timeout: numberArg.optional().describe('action timeout in ms (default 30000)'),
249
+ }),
250
+ });
251
+ const waitForCmd = (0, command_1.declareCommand)({
252
+ name: 'wait-for',
253
+ category: 'core',
254
+ description: 'Wait for a condition: selector state, JS predicate, or network idle',
255
+ args: z.object({
256
+ selector: z.string().optional().describe('CSS selector to wait for'),
257
+ }),
258
+ options: z.object({
259
+ state: z.enum(['visible', 'hidden', 'attached', 'detached']).optional().describe('selector state to wait for (default visible)'),
260
+ js: z.string().optional().describe('JS predicate, e.g. "() => document.querySelector(\'.x\')?.offsetHeight > 100"'),
261
+ 'network-idle': z.boolean().optional().describe('wait until the page is network-idle'),
262
+ timeout: numberArg.optional().describe('timeout in ms (default 30000)'),
263
+ }),
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
+ });
296
+ const runCmd = (0, command_1.declareCommand)({
297
+ name: 'run',
298
+ category: 'core',
299
+ description: 'Run a multi-step flow script (default-exports an async function receiving the API object)',
300
+ args: z.object({
301
+ file: z.string().describe('flow script path (.js / .mjs / .cjs)'),
302
+ }),
303
+ options: z.object({
304
+ timeout: numberArg.optional().describe('whole-script timeout in ms (default 300000, 0 = unlimited)'),
158
305
  }),
159
306
  });
160
307
  // ── state management ──
@@ -179,6 +326,15 @@ const stateSave = (0, command_1.declareCommand)({
179
326
  name: z.string().optional().describe('state file name (default: session)'),
180
327
  }),
181
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
+ });
182
338
  // ── config ──
183
339
  const configShow = (0, command_1.declareCommand)({
184
340
  name: 'config-show',
@@ -274,13 +430,15 @@ const mcpUninstall = (0, command_1.declareCommand)({
274
430
  // ── export ──
275
431
  const commandsArray = [
276
432
  // session management
277
- open, close, status,
433
+ open, close, status, framesCmd, doctor, scenarioCmd,
278
434
  // cssprobe
279
435
  inspect, tree, layout, findings, injectCss,
280
436
  // browser
281
437
  resize, evalCmd, playwrightCmd, screenshot,
438
+ // interaction
439
+ click, hover, typeCmd, waitForCmd, sampleCmd, waitStableCmd, runCmd,
282
440
  // state
283
- stateImport, stateSave,
441
+ stateImport, stateSave, stateCheck,
284
442
  // config
285
443
  configShow, configSet, configList, configUse, configNew, configPath,
286
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