@ytspar/sweetlink 1.17.0 → 1.18.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.
@@ -112,11 +112,24 @@ function getRelativePath(absolutePath) {
112
112
  * Report screenshot success to console
113
113
  */
114
114
  function reportScreenshotSuccess(outputPath, width, height, method, selector) {
115
- console.log(`[Sweetlink] Screenshot saved to: ${getRelativePath(outputPath)}`);
116
- console.log(`[Sweetlink] Dimensions: ${width}x${height}`);
117
- if (selector)
118
- console.log(`[Sweetlink] Selector: ${selector}`);
119
- console.log(`[Sweetlink] Method: ${method}`);
115
+ // One compact line by default; the multi-line version is preserved
116
+ // behind SWEETLINK_VERBOSE=1 for log-scrapers and the curious.
117
+ let sizeKb = '';
118
+ try {
119
+ sizeKb = ` · ${Math.round(fs.statSync(outputPath).size / 1024)}KB`;
120
+ }
121
+ catch { /* file may have been moved or removed */ }
122
+ const selPart = selector ? ` · ${selector}` : '';
123
+ if (process.env.SWEETLINK_VERBOSE === '1') {
124
+ console.log(`[Sweetlink] ✓ Screenshot saved to: ${getRelativePath(outputPath)}`);
125
+ console.log(`[Sweetlink] Dimensions: ${width}x${height}`);
126
+ if (selector)
127
+ console.log(`[Sweetlink] Selector: ${selector}`);
128
+ console.log(`[Sweetlink] Method: ${method}`);
129
+ }
130
+ else {
131
+ console.log(`[Sweetlink] ✓ Screenshot saved: ${getRelativePath(outputPath)} · ${width}x${height}${sizeKb}${selPart} · ${method}`);
132
+ }
120
133
  }
121
134
  let resolvedWsUrl = null;
122
135
  const DEFAULT_WS_URL = process.env.SWEETLINK_WS_URL || 'ws://localhost:9223';
@@ -459,6 +472,7 @@ async function screenshot(options) {
459
472
  fullPage: options.fullPage,
460
473
  viewport: options.viewport,
461
474
  padding: options.padding,
475
+ theme: options.theme,
462
476
  });
463
477
  const data = resp.data;
464
478
  const outputPath = options.output || getDefaultScreenshotPath();
@@ -1852,11 +1866,25 @@ const COMMAND_HELP = {
1852
1866
  stop Stop recording and generate session manifest
1853
1867
  status Show recording status (default)
1854
1868
 
1869
+ Options for start:
1870
+ --label <text> Human-friendly label embedded in the manifest + SUMMARY title
1871
+ --viewport <preset|WxH> Recording viewport (default: 1512x982)
1872
+
1855
1873
  Examples:
1856
- pnpm sweetlink record start
1874
+ pnpm sweetlink record start --label "login flow"
1857
1875
  pnpm sweetlink snapshot -i
1858
1876
  pnpm sweetlink click @e3
1859
1877
  pnpm sweetlink record stop`,
1878
+ sessions: ` sessions [list|open]
1879
+ List or open all recorded sessions in this project.
1880
+
1881
+ Subcommands:
1882
+ list Print every session with label, duration, action count, error count (default)
1883
+ open Open .sweetlink/index.html in the default browser
1884
+
1885
+ Examples:
1886
+ pnpm sweetlink sessions list
1887
+ pnpm sweetlink sessions open`,
1860
1888
  report: ` report [options]
1861
1889
  Print or share the latest session report.
1862
1890
 
@@ -1935,13 +1963,29 @@ Sweetlink CLI - Autonomous Development Bridge
1935
1963
 
1936
1964
  Usage:
1937
1965
  pnpm sweetlink <command> [options]
1938
-
1939
- Commands:
1940
- `);
1941
- for (const help of Object.values(COMMAND_HELP)) {
1942
- console.log(help);
1943
- console.log('');
1966
+ pnpm sweetlink <command> --help Detailed help for a single command
1967
+ pnpm sweetlink --help --all Show full help for every command
1968
+
1969
+ Commands:`);
1970
+ // Extract the first descriptive sentence from each command's help block
1971
+ // so the top-level help is scannable in <40 lines.
1972
+ for (const [name, help] of Object.entries(COMMAND_HELP)) {
1973
+ // Each block looks like:
1974
+ // " command [args]\n First-line description.\n..."
1975
+ // We pick the first non-empty line after the signature.
1976
+ const lines = help.split('\n').map((l) => l.trim()).filter(Boolean);
1977
+ const desc = lines[1] ?? '';
1978
+ const summary = desc.length > 70 ? desc.slice(0, 67) + '…' : desc;
1979
+ console.log(` ${name.padEnd(14)} ${summary}`);
1980
+ }
1981
+ if (process.argv.includes('--all')) {
1982
+ console.log('\n— Full per-command details —\n');
1983
+ for (const help of Object.values(COMMAND_HELP)) {
1984
+ console.log(help);
1985
+ console.log('');
1986
+ }
1944
1987
  }
1988
+ console.log('');
1945
1989
  console.log(GLOBAL_HELP);
1946
1990
  }
1947
1991
  function showCommandHelp(command) {
@@ -2003,6 +2047,7 @@ if (hasFlag('--output-schema')) {
2003
2047
  'fill',
2004
2048
  'console',
2005
2049
  'record',
2050
+ 'sessions',
2006
2051
  'proof',
2007
2052
  'report',
2008
2053
  'demo',
@@ -2131,6 +2176,7 @@ async function handleStatusCommand() {
2131
2176
  height: getArg('--height') ? parseInt(getArg('--height'), 10) : undefined,
2132
2177
  hover: hasFlag('--hover'),
2133
2178
  padding: getArg('--padding') ? parseInt(getArg('--padding'), 10) : undefined,
2179
+ theme: getArg('--theme'),
2134
2180
  url: getArg('--url'),
2135
2181
  wait: !hasFlag('--no-wait'), // Wait by default, --no-wait to skip
2136
2182
  waitTimeout: getArg('--wait-timeout')
@@ -2393,9 +2439,22 @@ async function handleStatusCommand() {
2393
2439
  const subcommand = args[1];
2394
2440
  const state = await ensureDaemon(projRoot, targetUrl);
2395
2441
  if (subcommand === 'start') {
2396
- const resp = await daemonRequest(state, 'record-start');
2442
+ const params = {};
2443
+ const label = getArg('--label');
2444
+ const viewport = getArg('--viewport');
2445
+ const storageState = getArg('--storage-state');
2446
+ if (label)
2447
+ params.label = label;
2448
+ if (viewport)
2449
+ params.viewport = viewport;
2450
+ if (storageState)
2451
+ params.storageState = storageState;
2452
+ if (hasFlag('--trace'))
2453
+ params.trace = true;
2454
+ const resp = await daemonRequest(state, 'record-start', params);
2397
2455
  const data = resp.data;
2398
- console.log(`[Sweetlink] Recording started: ${data.sessionId}`);
2456
+ console.log(`[Sweetlink] Recording started: ${data.sessionId}` +
2457
+ (data.label ? ` (${data.label})` : ''));
2399
2458
  result = data;
2400
2459
  }
2401
2460
  else if (subcommand === 'stop') {
@@ -2404,12 +2463,17 @@ async function handleStatusCommand() {
2404
2463
  const m = data.manifest;
2405
2464
  console.log(`[Sweetlink] Recording stopped: ${m.sessionId}`);
2406
2465
  console.log(` Duration: ${m.duration.toFixed(1)}s | Actions: ${m.commands.length}${m.video ? ' | Video: ' + m.video : ''}`);
2407
- // Auto-open the viewer
2466
+ // Auto-open the viewer (cross-platform; --no-open to suppress)
2408
2467
  if (data.viewerPath && !hasFlag('--no-open')) {
2409
2468
  console.log(` Viewer: ${data.viewerPath}`);
2410
2469
  const { execFile } = await import('child_process');
2411
- const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
2412
- execFile(openCmd, [data.viewerPath], (err) => {
2470
+ // `start` on Windows is a cmd builtin, not an exe must invoke via cmd.
2471
+ const cmd = process.platform === 'darwin' ? 'open'
2472
+ : process.platform === 'win32' ? 'cmd'
2473
+ : 'xdg-open';
2474
+ const args = process.platform === 'win32' ? ['/c', 'start', '', data.viewerPath]
2475
+ : [data.viewerPath];
2476
+ execFile(cmd, args, (err) => {
2413
2477
  if (err)
2414
2478
  console.error(' Could not open viewer:', err.message);
2415
2479
  });
@@ -2420,6 +2484,100 @@ async function handleStatusCommand() {
2420
2484
  }
2421
2485
  result = data;
2422
2486
  }
2487
+ else if (subcommand === 'exec') {
2488
+ // record exec "click @e2; fill @e3 hello world; click @e5"
2489
+ // Runs a semicolon-separated DSL inside a fresh recording, then
2490
+ // auto-stops. Each step is one of:
2491
+ // click <selector|@ref>
2492
+ // fill <@ref> <value> (rest of line after ref = value)
2493
+ // press <key>
2494
+ // sleep <ms>
2495
+ // Strip known --flag value pairs from positional args before
2496
+ // joining what remains as the script body.
2497
+ const flagsWithValues = new Set(['--url', '--label', '--viewport', '--storage-state']);
2498
+ const positional = [];
2499
+ for (let i = 2; i < args.length; i++) {
2500
+ const a = args[i];
2501
+ if (a.startsWith('--')) {
2502
+ if (flagsWithValues.has(a))
2503
+ i++; // skip its value
2504
+ continue;
2505
+ }
2506
+ positional.push(a);
2507
+ }
2508
+ const script = positional.join(' ').trim();
2509
+ if (!script) {
2510
+ console.error('[Sweetlink] Error: record exec requires a script. Example: `record exec "click @e2; fill @e3 hello"`');
2511
+ process.exit(1);
2512
+ }
2513
+ const label = getArg('--label');
2514
+ const startResp = await daemonRequest(state, 'record-start', label ? { label } : {});
2515
+ const startData = startResp.data;
2516
+ console.log(`[Sweetlink] Recording: ${startData.sessionId}${label ? ` (${label})` : ''}`);
2517
+ const steps = script.split(';').map((s) => s.trim()).filter(Boolean);
2518
+ // Snapshot once up-front so refs resolve.
2519
+ await daemonRequest(state, 'snapshot', { interactive: true });
2520
+ for (const step of steps) {
2521
+ const [verb, ...rest] = step.split(/\s+/);
2522
+ try {
2523
+ if (verb === 'click') {
2524
+ const target = rest[0];
2525
+ if (!target)
2526
+ throw new Error('click needs a target');
2527
+ if (/^@e\d+$/.test(target)) {
2528
+ await daemonRequest(state, 'click-ref', { ref: target });
2529
+ }
2530
+ else {
2531
+ await daemonRequest(state, 'click-css', { selector: target });
2532
+ }
2533
+ console.log(` · click ${target}`);
2534
+ }
2535
+ else if (verb === 'fill') {
2536
+ const ref = rest[0];
2537
+ const value = rest.slice(1).join(' ');
2538
+ if (!ref || !/^@e\d+$/.test(ref))
2539
+ throw new Error('fill needs a @ref and a value');
2540
+ await daemonRequest(state, 'fill-ref', { ref, value });
2541
+ console.log(` · fill ${ref} = "${value}"`);
2542
+ }
2543
+ else if (verb === 'press') {
2544
+ const key = rest[0];
2545
+ if (!key)
2546
+ throw new Error('press needs a key');
2547
+ await daemonRequest(state, 'press-key', { key });
2548
+ console.log(` · press ${key}`);
2549
+ }
2550
+ else if (verb === 'sleep') {
2551
+ const ms = parseInt(rest[0] ?? '0', 10);
2552
+ await new Promise((r) => setTimeout(r, ms));
2553
+ console.log(` · sleep ${ms}ms`);
2554
+ }
2555
+ else {
2556
+ throw new Error(`Unknown verb '${verb}'. Allowed: click, fill, press, sleep.`);
2557
+ }
2558
+ }
2559
+ catch (err) {
2560
+ console.error(` ✗ step "${step}" failed: ${err instanceof Error ? err.message : err}`);
2561
+ // Continue to record-stop so the partial recording is preserved.
2562
+ }
2563
+ }
2564
+ const stopResp = await daemonRequest(state, 'record-stop');
2565
+ const stopData = stopResp.data;
2566
+ console.log(`[Sweetlink] Done: ${stopData.manifest.commands.length} actions in ` +
2567
+ `${stopData.manifest.duration.toFixed(1)}s${stopData.viewerPath ? ` · ${stopData.viewerPath}` : ''}`);
2568
+ result = stopData;
2569
+ }
2570
+ else if (subcommand === 'pause') {
2571
+ const resp = await daemonRequest(state, 'record-pause');
2572
+ console.log('[Sweetlink] Recording paused. Use `record resume` to continue.');
2573
+ result = resp.data;
2574
+ }
2575
+ else if (subcommand === 'resume') {
2576
+ const resp = await daemonRequest(state, 'record-resume');
2577
+ const d = resp.data;
2578
+ console.log(`[Sweetlink] Recording resumed. Paused for ${(d.pausedDurationMs / 1000).toFixed(1)}s.`);
2579
+ result = resp.data;
2580
+ }
2423
2581
  else {
2424
2582
  const resp = await daemonRequest(state, 'record-status');
2425
2583
  const data = resp.data;
@@ -2553,6 +2711,99 @@ async function handleStatusCommand() {
2553
2711
  }
2554
2712
  break;
2555
2713
  }
2714
+ case 'sessions': {
2715
+ const sub = args[1];
2716
+ const projRoot = findProjectRoot();
2717
+ const targetUrl = getArg('--url') ?? 'http://localhost:3000';
2718
+ const state = await ensureDaemon(projRoot, targetUrl);
2719
+ const resp = await daemonRequest(state, 'sessions-list');
2720
+ const data = resp.data;
2721
+ if (sub === 'list' || !sub) {
2722
+ if (data.sessions.length === 0) {
2723
+ console.log('[Sweetlink] No sessions found.');
2724
+ }
2725
+ else {
2726
+ console.log(`[Sweetlink] ${data.sessions.length} session(s):\n`);
2727
+ for (const s of data.sessions) {
2728
+ const errTotal = s.errors ? s.errors.console + s.errors.network + s.errors.server : 0;
2729
+ const errBadge = errTotal > 0 ? ` · ${errTotal} err` : '';
2730
+ const labelTxt = s.label ? ` [${s.label}]` : '';
2731
+ const dur = s.duration ? `${s.duration.toFixed(1)}s` : '—';
2732
+ console.log(` ${s.sessionId}${labelTxt} · ${dur} · ${s.actionCount} actions${errBadge}`);
2733
+ }
2734
+ if (data.indexPath)
2735
+ console.log(`\n Index: ${data.indexPath}`);
2736
+ }
2737
+ result = { sessions: data.sessions };
2738
+ }
2739
+ else if (sub === 'diff') {
2740
+ // sessions diff <a> <b> — compare two recordings
2741
+ const [aId, bId] = [args[2], args[3]];
2742
+ if (!aId || !bId) {
2743
+ console.error('[Sweetlink] Usage: sessions diff <session-A> <session-B>');
2744
+ process.exit(1);
2745
+ }
2746
+ const findSession = (id) => data.sessions.find((s) => s.sessionId === id || s.sessionId.endsWith(id));
2747
+ const a = findSession(aId);
2748
+ const b = findSession(bId);
2749
+ if (!a || !b) {
2750
+ console.error(`[Sweetlink] Could not find session: ${!a ? aId : bId}`);
2751
+ process.exit(1);
2752
+ }
2753
+ const aManifest = JSON.parse(fs.readFileSync(a.manifestPath, 'utf-8'));
2754
+ const bManifest = JSON.parse(fs.readFileSync(b.manifestPath, 'utf-8'));
2755
+ const aActions = aManifest.commands.map((c) => `${c.action} ${c.args.join(' ')}`);
2756
+ const bActions = bManifest.commands.map((c) => `${c.action} ${c.args.join(' ')}`);
2757
+ console.log(`\n${a.sessionId}${a.label ? ` "${a.label}"` : ''} vs ${b.sessionId}${b.label ? ` "${b.label}"` : ''}\n`);
2758
+ console.log(`Duration: ${a.duration?.toFixed(1)}s vs ${b.duration?.toFixed(1)}s`);
2759
+ console.log(`Actions: ${a.actionCount} vs ${b.actionCount}`);
2760
+ const aErr = a.errors ? a.errors.console + a.errors.network + a.errors.server : 0;
2761
+ const bErr = b.errors ? b.errors.console + b.errors.network + b.errors.server : 0;
2762
+ console.log(`Errors: ${aErr} vs ${bErr}`);
2763
+ // Action diff (myers-style "added/removed" by line)
2764
+ const inA = new Set(aActions);
2765
+ const inB = new Set(bActions);
2766
+ const added = bActions.filter((x) => !inA.has(x));
2767
+ const removed = aActions.filter((x) => !inB.has(x));
2768
+ if (removed.length) {
2769
+ console.log(`\nOnly in ${a.sessionId}:`);
2770
+ removed.forEach((s) => console.log(` - ${s}`));
2771
+ }
2772
+ if (added.length) {
2773
+ console.log(`\nOnly in ${b.sessionId}:`);
2774
+ added.forEach((s) => console.log(` + ${s}`));
2775
+ }
2776
+ if (!added.length && !removed.length) {
2777
+ console.log('\nAction sequences are identical.');
2778
+ }
2779
+ result = {
2780
+ a: { id: a.sessionId, label: a.label, duration: a.duration, actions: a.actionCount, errors: aErr },
2781
+ b: { id: b.sessionId, label: b.label, duration: b.duration, actions: b.actionCount, errors: bErr },
2782
+ added,
2783
+ removed,
2784
+ };
2785
+ }
2786
+ else if (sub === 'open') {
2787
+ // Open the index.html in the browser
2788
+ if (data.indexPath) {
2789
+ const { execFile } = await import('child_process');
2790
+ const cmd = process.platform === 'darwin' ? 'open'
2791
+ : process.platform === 'win32' ? 'cmd'
2792
+ : 'xdg-open';
2793
+ const cmdArgs = process.platform === 'win32'
2794
+ ? ['/c', 'start', '', data.indexPath]
2795
+ : [data.indexPath];
2796
+ execFile(cmd, cmdArgs, () => { });
2797
+ console.log(`[Sweetlink] Opened ${data.indexPath}`);
2798
+ }
2799
+ result = { indexPath: data.indexPath };
2800
+ }
2801
+ else {
2802
+ console.error(`[Sweetlink] Unknown sessions subcommand: ${sub}. Try: list, open`);
2803
+ process.exit(1);
2804
+ }
2805
+ break;
2806
+ }
2556
2807
  case 'demo': {
2557
2808
  const sub = args[1];
2558
2809
  const projRoot = findProjectRoot();