cssprobe-cli 0.2.26 → 0.3.1

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.
@@ -13,6 +13,8 @@ const commands_1 = require("./commands");
13
13
  const output_1 = require("./output");
14
14
  const config_1 = require("../config/config");
15
15
  const session_1 = require("../daemon/session");
16
+ const session_ops_1 = require("./session-ops");
17
+ const mcp_1 = require("../mcp");
16
18
  const globalOptions = ['json', 'raw', 'help', 'h', 'version', 'v', 'p', 'profile'];
17
19
  const booleanGlobalOptions = ['help', 'json', 'raw', 'version', 'v', 'h'];
18
20
  async function program() {
@@ -37,8 +39,13 @@ async function program() {
37
39
  return;
38
40
  if (handleSkillCommands(commandName, command, rawArgs, output))
39
41
  return;
42
+ if (handleMcpInstallCommands(commandName, command, rawArgs, output))
43
+ return;
40
44
  // Session-based commands
41
45
  switch (commandName) {
46
+ case 'mcp':
47
+ await (0, mcp_1.runMcpServer)();
48
+ return;
42
49
  case 'open':
43
50
  await handleOpen(command, rawArgs, output);
44
51
  return;
@@ -63,8 +70,15 @@ async function program() {
63
70
  case 'eval':
64
71
  case 'resize':
65
72
  case 'playwright':
73
+ case 'click':
74
+ case 'hover':
75
+ case 'type':
76
+ case 'wait-for':
66
77
  await handleSessionCommand(commandName, command, rawArgs, output);
67
78
  return;
79
+ case 'run':
80
+ await handleRun(command, rawArgs, output);
81
+ return;
68
82
  }
69
83
  output.error(`Unknown command: ${commandName}. Run 'cssprobe-cli --help' for usage.`);
70
84
  }
@@ -111,12 +125,8 @@ async function handleOpen(command, args, output) {
111
125
  try {
112
126
  const cmdArgs = splitArgs(args);
113
127
  const parsed = (0, command_1.parseCommand)(command, cmdArgs);
114
- const url = parsed.url || 'about:blank';
115
- const browser = parsed.browser || 'chromium';
116
- const headed = !!parsed.headed;
117
- const viewportStr = parsed.viewport;
118
- const state = parsed.state;
119
128
  // Parse viewport
129
+ const viewportStr = parsed.viewport;
120
130
  let viewport;
121
131
  if (viewportStr) {
122
132
  const parts = viewportStr.split('x');
@@ -132,90 +142,41 @@ async function handleOpen(command, args, output) {
132
142
  }
133
143
  viewport = { width, height };
134
144
  }
135
- // Start daemon
136
- const { pid } = await session_1.Session.startDaemon({
137
- browser,
138
- headed,
145
+ const result = await (0, session_ops_1.openSession)({
146
+ url: parsed.url,
147
+ browser: parsed.browser,
148
+ headed: !!parsed.headed,
139
149
  viewport,
140
- state,
141
- _: ['open', url],
142
- }, 'open');
143
- // Wait for the daemon to be ready (poll session file instead of fixed delay)
144
- const session = await waitForSession(5000);
145
- if (!session)
146
- throw new Error('Daemon failed to become ready within 5s. Check the daemon log for errors.');
147
- await session.run({ _: ['goto', url] });
148
- const result = {
149
- sessionId: 'default',
150
- pid,
151
- url,
152
- viewport: viewport || { width: 1280, height: 720 },
153
- };
150
+ state: parsed.state,
151
+ });
154
152
  if (output.json) {
155
153
  console.log(JSON.stringify(result, null, 2));
156
154
  }
157
155
  else {
158
- console.log(`Browser opened at ${url}`);
159
- console.log(`Session ID: default`);
160
- console.log(`PID: ${pid}`);
161
- console.log(`Viewport: ${viewport?.width || 1280}x${viewport?.height || 720}`);
156
+ console.log(`Browser opened at ${result.url}`);
157
+ console.log(`Session ID: ${result.sessionId}`);
158
+ console.log(`PID: ${result.pid}`);
159
+ console.log(`Viewport: ${result.viewport.width}x${result.viewport.height}`);
162
160
  }
163
161
  }
164
162
  catch (e) {
165
163
  output.error(e instanceof Error ? e.message : String(e));
166
164
  }
167
165
  }
168
- async function waitForSession(timeoutMs) {
169
- const deadline = Date.now() + timeoutMs;
170
- while (Date.now() < deadline) {
171
- const session = await (0, session_1.loadSession)();
172
- if (session && await session.canConnect())
173
- return session;
174
- await new Promise(resolve => setTimeout(resolve, 100));
175
- }
176
- return undefined;
177
- }
178
166
  async function handleClose(command, args, output) {
179
167
  try {
180
168
  const cmdArgs = splitArgs(args);
181
169
  const parsed = (0, command_1.parseCommand)(command, cmdArgs);
182
- if (parsed.all) {
183
- const sessions = await (0, session_1.listAllSessions)();
184
- if (sessions.length === 0) {
185
- output.error('No active sessions found.');
186
- return;
187
- }
188
- const results = [];
189
- for (const session of sessions) {
190
- try {
191
- await session.stop();
192
- results.push({ sessionId: session.name, closed: true });
193
- }
194
- catch (e) {
195
- results.push({ sessionId: session.name, closed: false, error: e.message });
196
- }
197
- }
198
- if (output.json) {
199
- console.log(JSON.stringify({ closed: results }, null, 2));
200
- }
201
- else {
202
- const closedCount = results.filter(r => r.closed).length;
203
- console.log(`Closed ${closedCount} session(s).`);
204
- }
170
+ const result = await (0, session_ops_1.closeSession)(!!parsed.all);
171
+ if (output.json) {
172
+ console.log(JSON.stringify(parsed.all ? { closed: result.sessions } : { closed: true, sessionId: 'default' }, null, 2));
173
+ }
174
+ else if (parsed.all) {
175
+ const closedCount = result.sessions.filter(r => r.closed).length;
176
+ console.log(`Closed ${closedCount} session(s).`);
205
177
  }
206
178
  else {
207
- const session = await (0, session_1.loadSession)();
208
- if (!session) {
209
- output.error('No active session. Run: cssprobe-cli open <url>');
210
- return;
211
- }
212
- await session.stop();
213
- if (output.json) {
214
- console.log(JSON.stringify({ closed: true, sessionId: 'default' }, null, 2));
215
- }
216
- else {
217
- console.log('Browser closed.');
218
- }
179
+ console.log('Browser closed.');
219
180
  }
220
181
  }
221
182
  catch (e) {
@@ -224,83 +185,43 @@ async function handleClose(command, args, output) {
224
185
  }
225
186
  async function handleStatus(output) {
226
187
  try {
227
- const session = await (0, session_1.loadSession)();
228
- if (!session) {
229
- const result = { sessionId: 'default', alive: false };
230
- if (output.json) {
231
- console.log(JSON.stringify(result, null, 2));
232
- }
233
- else {
234
- console.log('No active session.');
235
- }
236
- return;
237
- }
238
- const canConnect = await session.canConnect();
239
- const result = {
240
- sessionId: 'default',
241
- alive: canConnect,
242
- config: session.config,
243
- };
188
+ const result = await (0, session_ops_1.getSessionStatus)();
244
189
  if (output.json) {
245
190
  console.log(JSON.stringify(result, null, 2));
191
+ return;
246
192
  }
247
- else {
248
- console.log(`Session: default`);
249
- console.log(`Status: ${canConnect ? 'alive' : 'dead'}`);
250
- if (canConnect) {
251
- console.log(`Browser: ${session.config.browser.browserName}`);
252
- }
193
+ if (!result.config) {
194
+ console.log('No active session.');
195
+ return;
196
+ }
197
+ console.log(`Session: default`);
198
+ console.log(`Status: ${result.alive ? 'alive' : 'dead'}`);
199
+ if (result.alive) {
200
+ console.log(`Browser: ${result.config.browser.browserName}`);
253
201
  }
254
202
  }
255
203
  catch (e) {
256
204
  output.error(e instanceof Error ? e.message : String(e));
257
205
  }
258
206
  }
207
+ async function handleRun(command, args, output) {
208
+ const fileArg = args._[1];
209
+ if (!fileArg)
210
+ output.error('Usage: cssprobe-cli run <file>');
211
+ const file = path_1.default.resolve(fileArg);
212
+ if (!fs_1.default.existsSync(file))
213
+ output.error(`Flow script not found: ${file}`);
214
+ args._ = ['run', file];
215
+ await handleSessionCommand('run', command, args, output);
216
+ }
259
217
  async function handleSessionCommand(commandName, command, args, output) {
260
218
  try {
261
- const session = await (0, session_1.loadSession)();
262
- if (!session) {
263
- output.error('No active session. Run: cssprobe-cli open <url>');
264
- return;
265
- }
266
219
  const cmdArgs = splitArgs(args);
267
220
  const parsed = (0, command_1.parseCommand)(command, cmdArgs);
268
- // Build command args for daemon
269
- const daemonArgs = [commandName];
270
- // Add positional args
271
- if (command.args) {
272
- const argsSchema = command.args;
273
- const argNames = Object.keys(argsSchema.shape);
274
- for (const name of argNames) {
275
- if (parsed[name] !== undefined) {
276
- daemonArgs.push(String(parsed[name]));
277
- }
278
- }
279
- }
280
- // Add options
281
- if (command.options) {
282
- const optionsSchema = command.options;
283
- const optionNames = Object.keys(optionsSchema.shape);
284
- for (const name of optionNames) {
285
- if (parsed[name] !== undefined) {
286
- if (typeof parsed[name] === 'boolean') {
287
- if (parsed[name])
288
- daemonArgs.push(`--${name}`);
289
- }
290
- else {
291
- daemonArgs.push(`--${name}=${parsed[name]}`);
292
- }
293
- }
294
- }
295
- }
296
- // Run command in session
297
- const runArgs = { ...cmdArgs, _: daemonArgs };
298
- if (args.json)
299
- runArgs.json = true;
300
- const result = await session.run(runArgs);
221
+ const result = await (0, session_ops_1.runSessionCommand)(commandName, command, parsed, !!args.json);
301
222
  // Screenshot: save the base64 data URL to a file and report the path
302
223
  if (commandName === 'screenshot' && typeof result.text === 'string' && result.text.startsWith('data:image/png;base64,')) {
303
- const saved = saveScreenshotFile(result.text, parsed.out);
224
+ const saved = (0, session_ops_1.saveScreenshotFile)(result.text, parsed.out);
304
225
  if (output.json) {
305
226
  console.log(JSON.stringify({ saved }, null, 2));
306
227
  }
@@ -310,29 +231,13 @@ async function handleSessionCommand(commandName, command, args, output) {
310
231
  return;
311
232
  }
312
233
  console.log(result.text);
234
+ if (typeof result.exitCode === 'number' && result.exitCode !== 0)
235
+ process.exit(result.exitCode);
313
236
  }
314
237
  catch (e) {
315
238
  output.error(e instanceof Error ? e.message : String(e));
316
239
  }
317
240
  }
318
- function saveScreenshotFile(dataUrl, out) {
319
- const base64 = dataUrl.replace(/^data:image\/png;base64,/, '');
320
- const buffer = Buffer.from(base64, 'base64');
321
- const resolvedOut = out
322
- ? path_1.default.resolve(out)
323
- : (() => {
324
- const dir = path_1.default.join(os_1.default.homedir(), '.cssprobe-cli', 'screenshots');
325
- if (!fs_1.default.existsSync(dir))
326
- fs_1.default.mkdirSync(dir, { recursive: true });
327
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
328
- return path_1.default.join(dir, `screenshot-${ts}.png`);
329
- })();
330
- const dir = path_1.default.dirname(resolvedOut);
331
- if (!fs_1.default.existsSync(dir))
332
- fs_1.default.mkdirSync(dir, { recursive: true });
333
- fs_1.default.writeFileSync(resolvedOut, buffer);
334
- return resolvedOut;
335
- }
336
241
  // ── config commands ──
337
242
  function handleConfigCommands(commandName, command, args, output) {
338
243
  try {
@@ -426,6 +331,27 @@ function handleSkillCommands(commandName, command, args, output) {
426
331
  }
427
332
  return false;
428
333
  }
334
+ // ── mcp-install commands ──
335
+ function handleMcpInstallCommands(commandName, command, args, output) {
336
+ if (commandName !== 'mcp-install' && commandName !== 'mcp-uninstall')
337
+ return false;
338
+ const targetFlag = args.target || 'auto';
339
+ const loc = args.local ? 'local' : 'global';
340
+ try {
341
+ const result = commandName === 'mcp-install'
342
+ ? (0, installer_1.installMcpConfig)(loc, targetFlag)
343
+ : (0, installer_1.uninstallMcpConfig)(loc, targetFlag);
344
+ const summary = result.files.map(f => `${f.agent}: ${f.action} ${f.path}`);
345
+ for (const note of result.notes)
346
+ summary.push(`note: ${note}`);
347
+ console.log(output.format({ files: summary }));
348
+ return true;
349
+ }
350
+ catch (e) {
351
+ output.error(e instanceof Error ? e.message : String(e));
352
+ }
353
+ return false;
354
+ }
429
355
  function parseNetscapeCookies(content) {
430
356
  const cookies = [];
431
357
  const lines = content.split('\n');
@@ -0,0 +1,44 @@
1
+ import { Session } from '../daemon/session';
2
+ import type { SessionConfig } from '../daemon/session';
3
+ import type { AnyCommandSchema } from './command';
4
+ export { saveScreenshotFile } from '../utils/screenshot';
5
+ export interface OpenOptions {
6
+ url?: string;
7
+ browser?: string;
8
+ headed?: boolean;
9
+ viewport?: {
10
+ width: number;
11
+ height: number;
12
+ };
13
+ state?: string;
14
+ }
15
+ export interface OpenResult {
16
+ sessionId: string;
17
+ pid?: number;
18
+ url: string;
19
+ viewport: {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ }
24
+ export declare function openSession(opts: OpenOptions): Promise<OpenResult>;
25
+ export declare function waitForSession(timeoutMs: number): Promise<Session | undefined>;
26
+ export interface CloseResult {
27
+ sessions: Array<{
28
+ sessionId: string;
29
+ closed: boolean;
30
+ error?: string;
31
+ }>;
32
+ }
33
+ export declare function closeSession(all?: boolean): Promise<CloseResult>;
34
+ export interface StatusResult {
35
+ sessionId: string;
36
+ alive: boolean;
37
+ config?: SessionConfig;
38
+ }
39
+ export declare function getSessionStatus(): Promise<StatusResult>;
40
+ export declare function buildDaemonArgs(commandName: string, command: AnyCommandSchema, parsed: Record<string, unknown>): string[];
41
+ export declare function runSessionCommand(commandName: string, command: AnyCommandSchema, parsed: Record<string, unknown>, json?: boolean): Promise<{
42
+ text: string;
43
+ exitCode?: number;
44
+ }>;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.saveScreenshotFile = void 0;
4
+ exports.openSession = openSession;
5
+ exports.waitForSession = waitForSession;
6
+ exports.closeSession = closeSession;
7
+ exports.getSessionStatus = getSessionStatus;
8
+ exports.buildDaemonArgs = buildDaemonArgs;
9
+ exports.runSessionCommand = runSessionCommand;
10
+ const session_1 = require("../daemon/session");
11
+ var screenshot_1 = require("../utils/screenshot");
12
+ Object.defineProperty(exports, "saveScreenshotFile", { enumerable: true, get: function () { return screenshot_1.saveScreenshotFile; } });
13
+ async function openSession(opts) {
14
+ const url = opts.url || 'about:blank';
15
+ const browser = opts.browser || 'chromium';
16
+ const { pid } = await session_1.Session.startDaemon({
17
+ browser,
18
+ headed: !!opts.headed,
19
+ viewport: opts.viewport,
20
+ state: opts.state,
21
+ _: ['open', url],
22
+ }, 'open');
23
+ const session = await waitForSession(5000);
24
+ if (!session)
25
+ throw new Error('Daemon failed to become ready within 5s. Check the daemon log for errors.');
26
+ await session.run({ _: ['goto', url] });
27
+ return {
28
+ sessionId: 'default',
29
+ pid,
30
+ url,
31
+ viewport: opts.viewport || { width: 1280, height: 720 },
32
+ };
33
+ }
34
+ async function waitForSession(timeoutMs) {
35
+ const deadline = Date.now() + timeoutMs;
36
+ while (Date.now() < deadline) {
37
+ const session = await (0, session_1.loadSession)();
38
+ if (session && await session.canConnect())
39
+ return session;
40
+ await new Promise(resolve => setTimeout(resolve, 100));
41
+ }
42
+ return undefined;
43
+ }
44
+ async function closeSession(all = false) {
45
+ if (all) {
46
+ const sessions = await (0, session_1.listAllSessions)();
47
+ if (sessions.length === 0)
48
+ throw new Error('No active sessions found.');
49
+ const results = [];
50
+ for (const session of sessions) {
51
+ try {
52
+ await session.stop();
53
+ results.push({ sessionId: session.name, closed: true });
54
+ }
55
+ catch (e) {
56
+ results.push({ sessionId: session.name, closed: false, error: e.message });
57
+ }
58
+ }
59
+ return { sessions: results };
60
+ }
61
+ const session = await (0, session_1.loadSession)();
62
+ if (!session)
63
+ throw new Error('No active session. Run: cssprobe-cli open <url>');
64
+ await session.stop();
65
+ return { sessions: [{ sessionId: 'default', closed: true }] };
66
+ }
67
+ async function getSessionStatus() {
68
+ const session = await (0, session_1.loadSession)();
69
+ if (!session)
70
+ return { sessionId: 'default', alive: false };
71
+ const canConnect = await session.canConnect();
72
+ return {
73
+ sessionId: 'default',
74
+ alive: canConnect,
75
+ config: session.config,
76
+ };
77
+ }
78
+ function buildDaemonArgs(commandName, command, parsed) {
79
+ const daemonArgs = [commandName];
80
+ if (command.args) {
81
+ for (const name of Object.keys(command.args.shape)) {
82
+ if (parsed[name] !== undefined)
83
+ daemonArgs.push(String(parsed[name]));
84
+ }
85
+ }
86
+ if (command.options) {
87
+ for (const name of Object.keys(command.options.shape)) {
88
+ if (parsed[name] === undefined)
89
+ continue;
90
+ if (typeof parsed[name] === 'boolean') {
91
+ if (parsed[name])
92
+ daemonArgs.push(`--${name}`);
93
+ }
94
+ else {
95
+ daemonArgs.push(`--${name}=${parsed[name]}`);
96
+ }
97
+ }
98
+ }
99
+ return daemonArgs;
100
+ }
101
+ async function runSessionCommand(commandName, command, parsed, json = false) {
102
+ const session = await (0, session_1.loadSession)();
103
+ if (!session)
104
+ throw new Error('No active session. Run: cssprobe-cli open <url>');
105
+ const daemonArgs = buildDaemonArgs(commandName, command, parsed);
106
+ const runArgs = { ...parsed, _: daemonArgs };
107
+ if (json)
108
+ runArgs.json = true;
109
+ return await session.run(runArgs);
110
+ }
111
+ //# sourceMappingURL=session-ops.js.map
@@ -17,6 +17,9 @@ const launcher_1 = require("../browser/launcher");
17
17
  const analyzer_1 = require("../engine/analyzer");
18
18
  const renderer_1 = require("../engine/renderer");
19
19
  const dom_tree_1 = require("../engine/renderers/dom-tree");
20
+ const interactions_1 = require("../browser/interactions");
21
+ const evalSerializer_1 = require("../engine/evalSerializer");
22
+ const flowRunner_1 = require("./flowRunner");
20
23
  const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
21
24
  async function startDaemon(sessionName, clientInfo, options) {
22
25
  const sessionConfig = createSessionConfig(clientInfo, sessionName, options);
@@ -173,12 +176,18 @@ async function executeCommand(launcher, args) {
173
176
  const page = launcher.getPage();
174
177
  if (!page)
175
178
  throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
176
- const result = await page.evaluate(expression);
177
- if (typeof result === 'string')
178
- return { text: result };
179
- if (result === undefined)
180
- return { text: 'undefined' };
181
- return { text: JSON.stringify(result, null, 2) };
179
+ const handle = await page.evaluateHandle(expression);
180
+ try {
181
+ const result = await handle.evaluate(evalSerializer_1.serializeEvalResult);
182
+ if (typeof result === 'string')
183
+ return { text: result };
184
+ if (result === undefined)
185
+ return { text: 'undefined' };
186
+ return { text: JSON.stringify(result, null, 2) };
187
+ }
188
+ finally {
189
+ await handle.dispose();
190
+ }
182
191
  }
183
192
  case 'resize': {
184
193
  const width = parseInt(params[0], 10);
@@ -206,6 +215,79 @@ async function executeCommand(launcher, args) {
206
215
  return { text: JSON.stringify({ ok: true, value: null, note: 'call returned no value' }) };
207
216
  return { text: JSON.stringify(result, null, 2) };
208
217
  }
218
+ case 'click':
219
+ case 'hover': {
220
+ const selector = params[0];
221
+ if (!selector)
222
+ throw new Error(`Selector is required for ${command} command`);
223
+ const page = requirePage(launcher);
224
+ const opts = { nth: args.nth, force: args.force, timeout: args.timeout };
225
+ const result = command === 'click'
226
+ ? await (0, interactions_1.clickElement)(page, selector, opts)
227
+ : await (0, interactions_1.hoverElement)(page, selector, opts);
228
+ if (args.json) {
229
+ return { text: JSON.stringify({ ok: true, selector, matches: result.matches, nth: result.nth }, null, 2) };
230
+ }
231
+ return { text: formatTargetAction(command === 'click' ? 'Clicked' : 'Hovered', selector, result) };
232
+ }
233
+ case 'type': {
234
+ const selector = params[0];
235
+ const text = params[1];
236
+ if (!selector || text === undefined)
237
+ throw new Error('Usage: type <selector> <text>');
238
+ await (0, interactions_1.typeInto)(requirePage(launcher), selector, text, {
239
+ press: args.press,
240
+ sequential: args.sequential,
241
+ clear: args.clear,
242
+ delay: args.delay,
243
+ nth: args.nth,
244
+ timeout: args.timeout,
245
+ });
246
+ if (args.json) {
247
+ const payload = { ok: true, selector, length: text.length };
248
+ if (args.press)
249
+ payload.pressed = args.press;
250
+ return { text: JSON.stringify(payload, null, 2) };
251
+ }
252
+ const pressedNote = args.press ? ` then pressed ${args.press}` : '';
253
+ return { text: `Typed ${text.length} chars into ${selector}${pressedNote}` };
254
+ }
255
+ case 'wait-for': {
256
+ // selector is read from the parsed object: option flags are appended to
257
+ // the argv array after positional args, so params[0] is not reliable here.
258
+ const selector = args.selector;
259
+ const timeout = args.timeout ?? 30000;
260
+ const target = (0, interactions_1.resolveWaitTarget)(selector, {
261
+ js: args.js,
262
+ networkIdle: args['network-idle'],
263
+ state: args.state,
264
+ });
265
+ let elapsedMs;
266
+ try {
267
+ elapsedMs = await (0, interactions_1.waitForCondition)(requirePage(launcher), target, timeout);
268
+ }
269
+ catch (e) {
270
+ throw new Error(`wait-for failed for ${(0, interactions_1.describeWaitTarget)(target)} within ${timeout}ms: ${e.message}`);
271
+ }
272
+ if (args.json) {
273
+ const payload = {
274
+ ok: true,
275
+ waitedFor: target.kind === 'networkIdle' ? 'networkidle' : target.kind,
276
+ elapsedMs,
277
+ };
278
+ if (target.kind === 'selector')
279
+ payload.state = target.state;
280
+ return { text: JSON.stringify(payload, null, 2) };
281
+ }
282
+ const label = target.kind === 'selector' ? `${selector} (${target.state})` : (0, interactions_1.describeWaitTarget)(target);
283
+ return { text: `Wait satisfied: ${label} (${elapsedMs}ms)` };
284
+ }
285
+ case 'run': {
286
+ const absPath = params[0];
287
+ if (!absPath)
288
+ throw new Error('Flow script path is required');
289
+ return await (0, flowRunner_1.runFlow)(launcher, absPath, { timeout: args.timeout, json: !!args.json });
290
+ }
209
291
  case 'state-save': {
210
292
  const context = launcher.getContext();
211
293
  if (!context)
@@ -217,6 +299,20 @@ async function executeCommand(launcher, args) {
217
299
  throw new Error(`Unknown command: ${command}`);
218
300
  }
219
301
  }
302
+ function requirePage(launcher) {
303
+ const page = launcher.getPage();
304
+ if (!page)
305
+ throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
306
+ return page;
307
+ }
308
+ function formatTargetAction(verb, selector, result) {
309
+ const descriptor = result.descriptor
310
+ ? ` → ${result.descriptor.tag}${result.descriptor.classes.map(c => `.${c}`).join('')}${result.descriptor.text ? ` "${result.descriptor.text}"` : ''}`
311
+ : '';
312
+ const matchNote = result.matches === 1 ? '1 match' : `${result.matches} matches`;
313
+ const nthNote = result.matches > 1 ? `, used #${result.nth}` : '';
314
+ return `${verb} ${selector}${descriptor} (${matchNote}${nthNote})`;
315
+ }
220
316
  async function socketExists(socketPath) {
221
317
  try {
222
318
  const stat = await fs_1.default.promises.stat(socketPath);
@@ -0,0 +1,62 @@
1
+ import type { Page } from 'playwright-core';
2
+ import type { BrowserLauncher } from '../browser/launcher';
3
+ import { type TargetResult, type TypeOptions, type WaitState } from '../browser/interactions';
4
+ import type { Finding, Snapshot } from '../engine/types';
5
+ export interface FlowEnvelope {
6
+ ok: boolean;
7
+ logs: string[];
8
+ result?: unknown;
9
+ error?: string;
10
+ elapsedMs: number;
11
+ }
12
+ export interface WaitForOptions {
13
+ state?: WaitState;
14
+ timeout?: number;
15
+ }
16
+ export interface WaitOutcome {
17
+ waitedFor: 'selector' | 'js' | 'networkidle';
18
+ state?: WaitState;
19
+ elapsedMs: number;
20
+ }
21
+ export interface FlowApi {
22
+ page: Page;
23
+ click(selector: string, opts?: {
24
+ nth?: number;
25
+ force?: boolean;
26
+ timeout?: number;
27
+ }): Promise<TargetResult>;
28
+ hover(selector: string, opts?: {
29
+ nth?: number;
30
+ force?: boolean;
31
+ timeout?: number;
32
+ }): Promise<TargetResult>;
33
+ type(selector: string, text: string, opts?: TypeOptions): Promise<void>;
34
+ waitFor(target: string | {
35
+ js: string;
36
+ timeout?: number;
37
+ } | {
38
+ networkIdle: true;
39
+ timeout?: number;
40
+ }, opts?: WaitForOptions): Promise<WaitOutcome>;
41
+ inspect(selector: string, opts?: {
42
+ format?: 'json' | 'markdown';
43
+ }): Promise<{
44
+ snapshot: Snapshot;
45
+ findings: Finding[];
46
+ } | string>;
47
+ screenshot(opts?: {
48
+ path?: string;
49
+ fullPage?: boolean;
50
+ }): Promise<string>;
51
+ log(...args: unknown[]): void;
52
+ }
53
+ type FlowFn = (api: FlowApi) => Promise<unknown> | unknown;
54
+ export declare function loadFlowScript(absPath: string): Promise<FlowFn>;
55
+ export declare function runFlow(launcher: BrowserLauncher, absPath: string, opts?: {
56
+ timeout?: number;
57
+ json?: boolean;
58
+ }): Promise<{
59
+ text: string;
60
+ exitCode: number;
61
+ }>;
62
+ export {};