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.
- package/AGENTS.md +29 -4
- package/CHANGELOG.md +11 -0
- package/README.md +84 -1
- package/dist/browser/interactions.d.ts +48 -0
- package/dist/browser/interactions.js +88 -0
- package/dist/cli/commands.js +97 -0
- package/dist/cli/program.js +78 -152
- package/dist/cli/session-ops.d.ts +44 -0
- package/dist/cli/session-ops.js +111 -0
- package/dist/daemon/daemon.js +102 -6
- package/dist/daemon/flowRunner.d.ts +62 -0
- package/dist/daemon/flowRunner.js +155 -0
- package/dist/engine/evalSerializer.d.ts +1 -0
- package/dist/engine/evalSerializer.js +43 -0
- package/dist/help.json +84 -1
- package/dist/installer/index.d.ts +2 -0
- package/dist/installer/index.js +6 -1
- package/dist/installer/mcp.d.ts +14 -0
- package/dist/installer/mcp.js +274 -0
- package/dist/installer/skill-template.js +43 -0
- package/dist/installer/toml.d.ts +20 -0
- package/dist/installer/toml.js +98 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.js +174 -0
- package/dist/mcp/server-instructions.d.ts +1 -0
- package/dist/mcp/server-instructions.js +27 -0
- package/dist/mcp/tools.d.ts +261 -0
- package/dist/mcp/tools.js +238 -0
- package/dist/utils/screenshot.d.ts +1 -0
- package/dist/utils/screenshot.js +58 -0
- package/llms.txt +2 -1
- package/package.json +2 -1
package/dist/cli/program.js
CHANGED
|
@@ -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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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:
|
|
160
|
-
console.log(`PID: ${pid}`);
|
|
161
|
-
console.log(`Viewport: ${viewport
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
248
|
-
console.log(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
package/dist/daemon/daemon.js
CHANGED
|
@@ -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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 {};
|