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
@@ -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':
@@ -70,8 +86,17 @@ async function program() {
70
86
  case 'eval':
71
87
  case 'resize':
72
88
  case 'playwright':
89
+ case 'click':
90
+ case 'hover':
91
+ case 'type':
92
+ case 'wait-for':
93
+ case 'sample':
94
+ case 'wait-stable':
73
95
  await handleSessionCommand(commandName, command, rawArgs, output);
74
96
  return;
97
+ case 'run':
98
+ await handleRun(command, rawArgs, output);
99
+ return;
75
100
  }
76
101
  output.error(`Unknown command: ${commandName}. Run 'cssprobe-cli --help' for usage.`);
77
102
  }
@@ -141,6 +166,8 @@ async function handleOpen(command, args, output) {
141
166
  headed: !!parsed.headed,
142
167
  viewport,
143
168
  state: parsed.state,
169
+ expectUrl: parsed['expect-url'],
170
+ failOnLogin: !!parsed['fail-on-login'],
144
171
  });
145
172
  if (output.json) {
146
173
  console.log(JSON.stringify(result, null, 2));
@@ -176,11 +203,100 @@ async function handleClose(command, args, output) {
176
203
  output.error(e instanceof Error ? e.message : String(e));
177
204
  }
178
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
+ }
179
286
  async function handleStatus(output) {
180
287
  try {
181
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
+ }
182
298
  if (output.json) {
183
- console.log(JSON.stringify(result, null, 2));
299
+ console.log(JSON.stringify({ ...result, page }, null, 2));
184
300
  return;
185
301
  }
186
302
  if (!result.config) {
@@ -191,15 +307,67 @@ async function handleStatus(output) {
191
307
  console.log(`Status: ${result.alive ? 'alive' : 'dead'}`);
192
308
  if (result.alive) {
193
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
+ }
194
315
  }
195
316
  }
196
317
  catch (e) {
197
318
  output.error(e instanceof Error ? e.message : String(e));
198
319
  }
199
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'}`);
342
+ }
343
+ console.log(`Status: ${report.ok ? 'ok' : 'attention'}`);
344
+ for (const warning of report.warnings)
345
+ console.log(`Warning: ${warning}`);
346
+ }
347
+ catch (e) {
348
+ output.error(e instanceof Error ? e.message : String(e));
349
+ }
350
+ }
351
+ async function handleRun(command, args, output) {
352
+ const fileArg = args._[1];
353
+ if (!fileArg)
354
+ output.error('Usage: cssprobe-cli run <file>');
355
+ const file = path_1.default.resolve(fileArg);
356
+ if (!fs_1.default.existsSync(file))
357
+ output.error(`Flow script not found: ${file}`);
358
+ args._ = ['run', file];
359
+ await handleSessionCommand('run', command, args, output);
360
+ }
200
361
  async function handleSessionCommand(commandName, command, args, output) {
201
362
  try {
202
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
+ }
203
371
  const parsed = (0, command_1.parseCommand)(command, cmdArgs);
204
372
  const result = await (0, session_ops_1.runSessionCommand)(commandName, command, parsed, !!args.json);
205
373
  // Screenshot: save the base64 data URL to a file and report the path
@@ -214,6 +382,8 @@ async function handleSessionCommand(commandName, command, args, output) {
214
382
  return;
215
383
  }
216
384
  console.log(result.text);
385
+ if (typeof result.exitCode === 'number' && result.exitCode !== 0)
386
+ process.exit(result.exitCode);
217
387
  }
218
388
  catch (e) {
219
389
  output.error(e instanceof Error ? e.message : String(e));
@@ -1,6 +1,7 @@
1
1
  import { Session } from '../daemon/session';
2
2
  import type { SessionConfig } from '../daemon/session';
3
3
  import type { AnyCommandSchema } from './command';
4
+ export { saveScreenshotFile } from '../utils/screenshot';
4
5
  export interface OpenOptions {
5
6
  url?: string;
6
7
  browser?: string;
@@ -10,6 +11,8 @@ export interface OpenOptions {
10
11
  height: number;
11
12
  };
12
13
  state?: string;
14
+ expectUrl?: string;
15
+ failOnLogin?: boolean;
13
16
  }
14
17
  export interface OpenResult {
15
18
  sessionId: string;
@@ -20,6 +23,12 @@ export interface OpenResult {
20
23
  height: number;
21
24
  };
22
25
  }
26
+ export interface PageInfo {
27
+ url: string;
28
+ title: string;
29
+ frames: number;
30
+ }
31
+ export declare function getPageInfo(): Promise<PageInfo>;
23
32
  export declare function openSession(opts: OpenOptions): Promise<OpenResult>;
24
33
  export declare function waitForSession(timeoutMs: number): Promise<Session | undefined>;
25
34
  export interface CloseResult {
@@ -39,5 +48,5 @@ export declare function getSessionStatus(): Promise<StatusResult>;
39
48
  export declare function buildDaemonArgs(commandName: string, command: AnyCommandSchema, parsed: Record<string, unknown>): string[];
40
49
  export declare function runSessionCommand(commandName: string, command: AnyCommandSchema, parsed: Record<string, unknown>, json?: boolean): Promise<{
41
50
  text: string;
51
+ exitCode?: number;
42
52
  }>;
43
- export declare function saveScreenshotFile(dataUrl: string, out?: string): string;
@@ -1,49 +1,24 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.saveScreenshotFile = void 0;
4
+ exports.getPageInfo = getPageInfo;
36
5
  exports.openSession = openSession;
37
6
  exports.waitForSession = waitForSession;
38
7
  exports.closeSession = closeSession;
39
8
  exports.getSessionStatus = getSessionStatus;
40
9
  exports.buildDaemonArgs = buildDaemonArgs;
41
10
  exports.runSessionCommand = runSessionCommand;
42
- exports.saveScreenshotFile = saveScreenshotFile;
43
- const fs = __importStar(require("fs"));
44
- const os = __importStar(require("os"));
45
- const path = __importStar(require("path"));
46
11
  const session_1 = require("../daemon/session");
12
+ const url_guard_1 = require("./url-guard");
13
+ var screenshot_1 = require("../utils/screenshot");
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
+ }
47
22
  async function openSession(opts) {
48
23
  const url = opts.url || 'about:blank';
49
24
  const browser = opts.browser || 'chromium';
@@ -58,10 +33,24 @@ async function openSession(opts) {
58
33
  if (!session)
59
34
  throw new Error('Daemon failed to become ready within 5s. Check the daemon log for errors.');
60
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
+ }
61
50
  return {
62
51
  sessionId: 'default',
63
52
  pid,
64
- url,
53
+ url: info.url,
65
54
  viewport: opts.viewport || { width: 1280, height: 720 },
66
55
  };
67
56
  }
@@ -142,22 +131,4 @@ async function runSessionCommand(commandName, command, parsed, json = false) {
142
131
  runArgs.json = true;
143
132
  return await session.run(runArgs);
144
133
  }
145
- function saveScreenshotFile(dataUrl, out) {
146
- const base64 = dataUrl.replace(/^data:image\/png;base64,/, '');
147
- const buffer = Buffer.from(base64, 'base64');
148
- const resolvedOut = out
149
- ? path.resolve(out)
150
- : (() => {
151
- const dir = path.join(os.homedir(), '.cssprobe-cli', 'screenshots');
152
- if (!fs.existsSync(dir))
153
- fs.mkdirSync(dir, { recursive: true });
154
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
155
- return path.join(dir, `screenshot-${ts}.png`);
156
- })();
157
- const dir = path.dirname(resolvedOut);
158
- if (!fs.existsSync(dir))
159
- fs.mkdirSync(dir, { recursive: true });
160
- fs.writeFileSync(resolvedOut, buffer);
161
- return resolvedOut;
162
- }
163
134
  //# sourceMappingURL=session-ops.js.map
@@ -0,0 +1,17 @@
1
+ export interface StorageCookie {
2
+ name: string;
3
+ domain: string;
4
+ expires?: number;
5
+ }
6
+ export interface StateReport {
7
+ cookies: number;
8
+ domains: string[];
9
+ expired: string[];
10
+ sessionCookies: number;
11
+ targetMatches: boolean | null;
12
+ warnings: string[];
13
+ ok: boolean;
14
+ }
15
+ export declare function analyzeState(state: {
16
+ cookies?: StorageCookie[];
17
+ }, targetUrl?: string, nowMs?: number): StateReport;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeState = analyzeState;
4
+ function normalizeDomain(domain) {
5
+ return domain.replace(/^\./, '').toLowerCase();
6
+ }
7
+ function analyzeState(state, targetUrl, nowMs = Date.now()) {
8
+ const cookies = state.cookies ?? [];
9
+ const expired = [];
10
+ let sessionCookies = 0;
11
+ for (const cookie of cookies) {
12
+ if (cookie.expires === undefined || cookie.expires <= 0) {
13
+ sessionCookies++;
14
+ }
15
+ else if (cookie.expires * 1000 <= nowMs) {
16
+ expired.push(cookie.name);
17
+ }
18
+ }
19
+ const domains = Array.from(new Set(cookies.map(cookie => normalizeDomain(cookie.domain)))).sort();
20
+ let targetMatches = null;
21
+ if (targetUrl) {
22
+ try {
23
+ const host = new URL(targetUrl).hostname.toLowerCase();
24
+ targetMatches = cookies.some(cookie => {
25
+ const domain = normalizeDomain(cookie.domain);
26
+ return host === domain || host.endsWith(`.${domain}`);
27
+ });
28
+ }
29
+ catch {
30
+ throw new Error(`Invalid --url value: ${targetUrl}`);
31
+ }
32
+ }
33
+ const warnings = [];
34
+ if (!cookies.length)
35
+ warnings.push('no cookies in state file');
36
+ if (expired.length)
37
+ warnings.push(`expired cookies: ${expired.join(', ')}`);
38
+ if (targetMatches === false)
39
+ warnings.push(`no cookies match ${targetUrl}`);
40
+ const ok = cookies.length > 0 && expired.length === 0 && targetMatches !== false;
41
+ return { cookies: cookies.length, domains, expired, sessionCookies, targetMatches, warnings, ok };
42
+ }
43
+ //# sourceMappingURL=state-check.js.map
@@ -0,0 +1,2 @@
1
+ export declare function looksLikeLoginUrl(raw: string): boolean;
2
+ export declare function matchesUrlPattern(pattern: string, raw: string): boolean;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.looksLikeLoginUrl = looksLikeLoginUrl;
4
+ exports.matchesUrlPattern = matchesUrlPattern;
5
+ function looksLikeLoginUrl(raw) {
6
+ try {
7
+ const url = new URL(raw);
8
+ return /\/(?:s\/)?login(?:\/|$)/.test(url.pathname);
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ function matchesUrlPattern(pattern, raw) {
15
+ let regex;
16
+ try {
17
+ regex = new RegExp(pattern);
18
+ }
19
+ catch {
20
+ throw new Error(`Invalid --expect-url pattern: ${pattern}`);
21
+ }
22
+ return regex.test(raw);
23
+ }
24
+ //# sourceMappingURL=url-guard.js.map