@ytspar/sweetlink 1.14.0 → 1.15.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.
- package/README.md +207 -32
- package/claude-skills/screenshot/SKILL.md +21 -1
- package/dist/cli/sweetlink.js +295 -3
- package/dist/cli/sweetlink.js.map +1 -1
- package/dist/daemon/browser.d.ts +7 -0
- package/dist/daemon/browser.d.ts.map +1 -1
- package/dist/daemon/browser.js +21 -2
- package/dist/daemon/browser.js.map +1 -1
- package/dist/daemon/client.d.ts +5 -3
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +25 -19
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/demo.d.ts +68 -0
- package/dist/daemon/demo.d.ts.map +1 -0
- package/dist/daemon/demo.js +247 -0
- package/dist/daemon/demo.js.map +1 -0
- package/dist/daemon/diff.js +5 -5
- package/dist/daemon/diff.js.map +1 -1
- package/dist/daemon/errorPatterns.d.ts +17 -0
- package/dist/daemon/errorPatterns.d.ts.map +1 -0
- package/dist/daemon/errorPatterns.js +81 -0
- package/dist/daemon/errorPatterns.js.map +1 -0
- package/dist/daemon/evidence.d.ts.map +1 -1
- package/dist/daemon/evidence.js +8 -7
- package/dist/daemon/evidence.js.map +1 -1
- package/dist/daemon/index.js +24 -12
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/recording.d.ts +22 -12
- package/dist/daemon/recording.d.ts.map +1 -1
- package/dist/daemon/recording.js +114 -31
- package/dist/daemon/recording.js.map +1 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +94 -17
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session.d.ts +6 -0
- package/dist/daemon/session.d.ts.map +1 -1
- package/dist/daemon/stateFile.d.ts +17 -10
- package/dist/daemon/stateFile.d.ts.map +1 -1
- package/dist/daemon/stateFile.js +34 -18
- package/dist/daemon/stateFile.js.map +1 -1
- package/dist/daemon/summary.d.ts +28 -0
- package/dist/daemon/summary.d.ts.map +1 -0
- package/dist/daemon/summary.js +152 -0
- package/dist/daemon/summary.js.map +1 -0
- package/dist/daemon/types.d.ts +1 -1
- package/dist/daemon/types.d.ts.map +1 -1
- package/dist/daemon/types.js.map +1 -1
- package/dist/daemon/viewer.d.ts +9 -18
- package/dist/daemon/viewer.d.ts.map +1 -1
- package/dist/daemon/viewer.js +441 -100
- package/dist/daemon/viewer.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +173 -0
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +19 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vite.d.ts +12 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +13 -0
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -181,7 +181,7 @@ pnpm sweetlink query --selector "h1"
|
|
|
181
181
|
### Screenshots
|
|
182
182
|
|
|
183
183
|
```bash
|
|
184
|
-
# Full page screenshot
|
|
184
|
+
# Full page screenshot (html2canvas, fast)
|
|
185
185
|
pnpm sweetlink screenshot
|
|
186
186
|
|
|
187
187
|
# Element screenshot
|
|
@@ -190,8 +190,23 @@ pnpm sweetlink screenshot --selector ".company-card"
|
|
|
190
190
|
# Full page with custom output
|
|
191
191
|
pnpm sweetlink screenshot --full-page --output page.png
|
|
192
192
|
|
|
193
|
-
#
|
|
194
|
-
pnpm sweetlink screenshot --
|
|
193
|
+
# Navigate to URL before capturing
|
|
194
|
+
pnpm sweetlink screenshot --url http://localhost:3000/about
|
|
195
|
+
|
|
196
|
+
# Custom viewport dimensions
|
|
197
|
+
pnpm sweetlink screenshot --force-cdp --width 375 --height 667
|
|
198
|
+
pnpm sweetlink screenshot --force-cdp --viewport mobile
|
|
199
|
+
|
|
200
|
+
# Pixel-perfect via daemon (see Persistent Daemon section)
|
|
201
|
+
pnpm sweetlink screenshot --hifi
|
|
202
|
+
pnpm sweetlink screenshot --responsive
|
|
203
|
+
|
|
204
|
+
# Skip server readiness check
|
|
205
|
+
pnpm sweetlink screenshot --no-wait
|
|
206
|
+
|
|
207
|
+
# Force specific method
|
|
208
|
+
pnpm sweetlink screenshot --force-cdp # Playwright/CDP
|
|
209
|
+
pnpm sweetlink screenshot --force-ws # WebSocket/html2canvas
|
|
195
210
|
```
|
|
196
211
|
|
|
197
212
|
### DOM Queries
|
|
@@ -306,30 +321,125 @@ pnpm sweetlink click --selector ".tab" --index 2
|
|
|
306
321
|
|
|
307
322
|
**Debugging:** Use `DEBUG=1 pnpm sweetlink click ...` to see generated JavaScript.
|
|
308
323
|
|
|
309
|
-
### Network Requests
|
|
324
|
+
### Network Requests
|
|
310
325
|
|
|
311
326
|
```bash
|
|
312
|
-
# Get all network requests
|
|
327
|
+
# Get all network requests (via WebSocket bridge)
|
|
313
328
|
pnpm sweetlink network
|
|
314
329
|
|
|
315
330
|
# Filter by URL
|
|
316
331
|
pnpm sweetlink network --filter "/api/"
|
|
332
|
+
|
|
333
|
+
# Failed requests only (via daemon ring buffer)
|
|
334
|
+
pnpm sweetlink network --failed
|
|
335
|
+
pnpm sweetlink network --failed --last 10
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Schema, Outline, Accessibility
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
# Page schema (JSON-LD, meta tags, Open Graph)
|
|
342
|
+
pnpm sweetlink schema
|
|
343
|
+
|
|
344
|
+
# Document outline (heading structure)
|
|
345
|
+
pnpm sweetlink outline
|
|
346
|
+
|
|
347
|
+
# Accessibility audit (axe-core)
|
|
348
|
+
pnpm sweetlink a11y
|
|
349
|
+
|
|
350
|
+
# Web Vitals (FCP, LCP, CLS, INP)
|
|
351
|
+
pnpm sweetlink vitals
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Server Management
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
# Wait for dev server to be ready (useful in scripts)
|
|
358
|
+
pnpm sweetlink wait --url http://localhost:3000
|
|
359
|
+
pnpm sweetlink wait --url http://localhost:3000 --wait-timeout 60000
|
|
360
|
+
|
|
361
|
+
# Check server health
|
|
362
|
+
pnpm sweetlink status
|
|
363
|
+
|
|
364
|
+
# Clean up Sweetlink processes
|
|
365
|
+
pnpm sweetlink cleanup
|
|
366
|
+
pnpm sweetlink cleanup --force
|
|
367
|
+
|
|
368
|
+
# Target specific app in monorepo (by branch or app name)
|
|
369
|
+
pnpm sweetlink screenshot --app my-app
|
|
317
370
|
```
|
|
318
371
|
|
|
319
372
|
### Persistent Daemon (v2)
|
|
320
373
|
|
|
321
374
|
Sweetlink v2 adds a persistent Playwright daemon for high-fidelity operations. The daemon auto-starts on first use and auto-stops after 30min idle.
|
|
322
375
|
|
|
376
|
+
#### Auto-Start with Vite
|
|
377
|
+
|
|
378
|
+
Add `daemon: true` to your Vite plugin config — the daemon starts/stops with your dev server:
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// vite.config.ts
|
|
382
|
+
import { sweetlink } from '@ytspar/sweetlink/vite';
|
|
383
|
+
|
|
384
|
+
export default defineConfig({
|
|
385
|
+
plugins: [sweetlink({ daemon: true })]
|
|
386
|
+
// Or with visible browser: sweetlink({ daemon: true, headed: true })
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
#### Auto-Start with Dev Script
|
|
391
|
+
|
|
392
|
+
Add a `dev` script that starts both your app and the daemon:
|
|
393
|
+
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"scripts": {
|
|
397
|
+
"dev": "vite",
|
|
398
|
+
"dev:daemon": "concurrently 'vite' 'sweetlink daemon start --url http://localhost:5173'"
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
#### Multiple Apps (Monorepo)
|
|
404
|
+
|
|
405
|
+
Each daemon is scoped by app port. Running two apps on different ports creates separate daemons:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
# Terminal 1: App A on port 3000
|
|
409
|
+
sweetlink daemon start --url http://localhost:3000
|
|
410
|
+
# State: .sweetlink/daemon-3000.json
|
|
411
|
+
|
|
412
|
+
# Terminal 2: App B on port 5173
|
|
413
|
+
sweetlink daemon start --url http://localhost:5173
|
|
414
|
+
# State: .sweetlink/daemon-5173.json
|
|
415
|
+
|
|
416
|
+
# Commands target specific apps via --url
|
|
417
|
+
sweetlink screenshot --hifi --url http://localhost:3000
|
|
418
|
+
sweetlink screenshot --hifi --url http://localhost:5173
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### Manual Lifecycle
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
# Start manually (auto-starts on first --hifi command if not running)
|
|
425
|
+
pnpm sweetlink daemon start --url http://localhost:5173
|
|
426
|
+
pnpm sweetlink daemon start --url http://localhost:5173 --headed # visible browser
|
|
427
|
+
|
|
428
|
+
# Check status
|
|
429
|
+
pnpm sweetlink daemon status
|
|
430
|
+
|
|
431
|
+
# Stop (or wait for 30min idle auto-stop)
|
|
432
|
+
pnpm sweetlink daemon stop
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
#### CLI Quick Reference
|
|
436
|
+
|
|
323
437
|
```bash
|
|
324
438
|
# Pixel-perfect screenshot via persistent daemon (~150ms after startup)
|
|
325
439
|
pnpm sweetlink screenshot --hifi
|
|
326
440
|
|
|
327
441
|
# Responsive screenshots at 3 breakpoints (375/768/1280)
|
|
328
442
|
pnpm sweetlink screenshot --responsive
|
|
329
|
-
|
|
330
|
-
# Manage daemon lifecycle
|
|
331
|
-
pnpm sweetlink daemon status
|
|
332
|
-
pnpm sweetlink daemon stop
|
|
333
443
|
```
|
|
334
444
|
|
|
335
445
|
### Accessibility Snapshots & Refs
|
|
@@ -375,6 +485,38 @@ pnpm sweetlink record stop
|
|
|
375
485
|
# Generates .sweetlink/<session-id>/viewer.html
|
|
376
486
|
```
|
|
377
487
|
|
|
488
|
+
### Demo Documents
|
|
489
|
+
|
|
490
|
+
Build a Markdown tutorial/proof document step-by-step (inspired by [Showboat](https://simonwillison.net/2026/Feb/10/showboat-and-rodney/)):
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
# Start a new demo
|
|
494
|
+
pnpm sweetlink demo init "How to use the search feature"
|
|
495
|
+
|
|
496
|
+
# Add narrative prose
|
|
497
|
+
pnpm sweetlink demo note "First, run the tests to see them pass."
|
|
498
|
+
|
|
499
|
+
# Run a command and capture output inline
|
|
500
|
+
pnpm sweetlink demo exec "pnpm test -- --grep search"
|
|
501
|
+
|
|
502
|
+
# Take a screenshot and embed it
|
|
503
|
+
pnpm sweetlink demo screenshot --url http://localhost:5173 --caption "Search results"
|
|
504
|
+
|
|
505
|
+
# Capture accessibility tree
|
|
506
|
+
pnpm sweetlink demo snapshot --url http://localhost:5173
|
|
507
|
+
|
|
508
|
+
# Remove last section if it's wrong
|
|
509
|
+
pnpm sweetlink demo pop
|
|
510
|
+
|
|
511
|
+
# Re-run all commands to verify outputs haven't changed
|
|
512
|
+
pnpm sweetlink demo verify
|
|
513
|
+
|
|
514
|
+
# Check status
|
|
515
|
+
pnpm sweetlink demo status
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Result: a `DEMO.md` with embedded command outputs and screenshots that serves as both documentation and a regression test.
|
|
519
|
+
|
|
378
520
|
### PR Evidence
|
|
379
521
|
|
|
380
522
|
```bash
|
|
@@ -564,19 +706,53 @@ This 15x token savings enables 10+ autonomous iterations within Claude's budget.
|
|
|
564
706
|
|
|
565
707
|
## Comparison with Alternatives
|
|
566
708
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
|
570
|
-
|
|
571
|
-
|
|
|
572
|
-
|
|
|
573
|
-
|
|
|
574
|
-
|
|
|
575
|
-
|
|
|
576
|
-
|
|
|
577
|
-
|
|
|
578
|
-
|
|
|
579
|
-
|
|
|
709
|
+
### Screenshot & Interaction
|
|
710
|
+
|
|
711
|
+
| Feature | Sweetlink | Playwright CLI | Chrome DevTools MCP | agent-browser |
|
|
712
|
+
|---------|-----------|----------------|--------------------|--------------|
|
|
713
|
+
| Setup | Vite/Next plugin | npm install | Chrome flag | npm install |
|
|
714
|
+
| Token cost/screenshot | ~1,000 | ~5,000 | ~5,000 | ~3,000 |
|
|
715
|
+
| Fast screenshots (html2canvas) | ✅ | ❌ | ❌ | ❌ |
|
|
716
|
+
| Pixel-perfect screenshots | ✅ (`--hifi`) | ✅ | ✅ | ✅ |
|
|
717
|
+
| Responsive (multi-viewport) | ✅ (`--responsive`) | Manual | ❌ | ❌ |
|
|
718
|
+
| Device emulation (iPhone, Pixel) | ✅ (named presets) | ✅ | ❌ | ✅ |
|
|
719
|
+
| @ref element interaction | ✅ (accessibility tree) | ❌ (CSS selectors) | ❌ | ✅ (Stagehand AI) |
|
|
720
|
+
| DOM queries | ✅ | ✅ | ✅ | ❌ |
|
|
721
|
+
| JS execution | ✅ | ✅ | ✅ | ❌ |
|
|
722
|
+
| Persistent browser sessions | ✅ (daemon) | ❌ (per-run) | ✅ | ✅ |
|
|
723
|
+
|
|
724
|
+
### Evidence & Reporting
|
|
725
|
+
|
|
726
|
+
| Feature | Sweetlink | ProofShot | Proof | Playwright |
|
|
727
|
+
|---------|-----------|-----------|-------|------------|
|
|
728
|
+
| Video recording (WebM) | ✅ | ✅ | ✅ | ✅ (trace) |
|
|
729
|
+
| Interactive HTML viewer | ✅ (video + overlays) | ✅ | ✅ | ✅ (trace viewer) |
|
|
730
|
+
| Click ripple overlays | ✅ (canvas) | ✅ | ✅ (FFmpeg) | ❌ |
|
|
731
|
+
| Console ring buffer | ✅ (always-on, 50K) | ✅ | ❌ | ❌ |
|
|
732
|
+
| Network ring buffer | ✅ (always-on, 50K) | ❌ | ❌ | ✅ (HAR) |
|
|
733
|
+
| Multi-language error detection | ✅ (10+ languages) | ✅ | ❌ | ❌ |
|
|
734
|
+
| Snapshot diffing (a11y tree) | ✅ | ❌ | ❌ | ❌ |
|
|
735
|
+
| Annotated screenshots | ✅ (@ref labels) | ✅ | ❌ | ❌ |
|
|
736
|
+
| SUMMARY.md report | ✅ | ✅ | ✅ (3 formats) | ❌ |
|
|
737
|
+
| Demo documents (Showboat-style) | ✅ | ❌ | ❌ | ❌ |
|
|
738
|
+
| PR evidence upload | ✅ (`proof --pr`) | ✅ | ❌ | ❌ |
|
|
739
|
+
| Webhook sharing | ✅ (any URL) | ❌ | ❌ | ❌ |
|
|
740
|
+
| Self-contained viewer (offline) | ✅ (base64 video) | ✅ | ✅ | ✅ |
|
|
741
|
+
|
|
742
|
+
### Developer Experience
|
|
743
|
+
|
|
744
|
+
| Feature | Sweetlink | ProofShot | Chrome DevTools MCP |
|
|
745
|
+
|---------|-----------|-----------|---------------------|
|
|
746
|
+
| Devbar toolbar UI | ✅ | ❌ | ❌ |
|
|
747
|
+
| Record button in browser | ✅ | ❌ | ❌ |
|
|
748
|
+
| Viewer auto-opens on stop | ✅ | ❌ | ❌ |
|
|
749
|
+
| Copy Report to clipboard | ✅ (CLI + viewer) | ❌ | ❌ |
|
|
750
|
+
| Serve viewer on network | ✅ (`report --serve`) | ❌ | ❌ |
|
|
751
|
+
| Multi-instance (monorepo) | ✅ (per-port daemon) | ❌ | ❌ |
|
|
752
|
+
| Vite/Next.js auto-start | ✅ (plugin option) | ❌ | ❌ |
|
|
753
|
+
| Accessibility audit (axe-core) | ✅ | ❌ | ❌ |
|
|
754
|
+
| Web Vitals | ✅ | ❌ | ❌ |
|
|
755
|
+
| Ruler/measurement tool | ✅ | ❌ | ❌ |
|
|
580
756
|
|
|
581
757
|
## When to Use Alternatives
|
|
582
758
|
|
|
@@ -585,22 +761,21 @@ This 15x token savings enables 10+ autonomous iterations within Claude's budget.
|
|
|
585
761
|
**Use Sweetlink when:**
|
|
586
762
|
- You're debugging/iterating on a running dev server
|
|
587
763
|
- You need lightweight, token-efficient screenshots (~1000 tokens vs ~5000)
|
|
588
|
-
- You want
|
|
589
|
-
-
|
|
764
|
+
- You want always-on console/network capture without "start watching"
|
|
765
|
+
- You need session recording with interactive video viewer
|
|
766
|
+
- You want demo documents that serve as both tutorials and regression tests
|
|
590
767
|
- You're doing autonomous UI development loops (10+ iterations)
|
|
768
|
+
- You want a toolbar UI in the browser for visual debugging
|
|
591
769
|
|
|
592
770
|
**Use [Agent Browser](https://github.com/vercel-labs/agent-browser) when:**
|
|
593
|
-
- You
|
|
594
|
-
- You're testing production sites or external URLs
|
|
595
|
-
- Sweetlink isn't integrated into the target application
|
|
771
|
+
- You're testing production sites or external URLs where Sweetlink isn't installed
|
|
596
772
|
- You need Stagehand's AI-powered element selection
|
|
597
773
|
- You're building autonomous agents that interact with arbitrary websites
|
|
598
774
|
|
|
599
|
-
**Use Playwright
|
|
600
|
-
- You
|
|
601
|
-
- You
|
|
602
|
-
- You
|
|
603
|
-
- You require network interception or request mocking
|
|
775
|
+
**Use Playwright directly when:**
|
|
776
|
+
- You're writing structured E2E test suites with assertions
|
|
777
|
+
- You need network interception, request mocking, or multiple browser contexts
|
|
778
|
+
- You want trace viewer for debugging test failures
|
|
604
779
|
|
|
605
780
|
### Agent Browser Quick Start
|
|
606
781
|
|
|
@@ -27,7 +27,7 @@ CLI (pnpm sweetlink)
|
|
|
27
27
|
console, network --failed, record, proof
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
The daemon auto-starts on first `--hifi`/`snapshot` command and auto-stops after 30min idle. State file at `.sweetlink/daemon.json
|
|
30
|
+
The daemon auto-starts on first `--hifi`/`snapshot` command and auto-stops after 30min idle. State file at `.sweetlink/daemon-{port}.json` (scoped per app port for multi-instance support).
|
|
31
31
|
|
|
32
32
|
### Prerequisites
|
|
33
33
|
|
|
@@ -344,9 +344,29 @@ pnpm sweetlink record status
|
|
|
344
344
|
```bash
|
|
345
345
|
pnpm sweetlink daemon status
|
|
346
346
|
pnpm sweetlink daemon start --url http://localhost:3000
|
|
347
|
+
pnpm sweetlink daemon start --url http://localhost:3000 --headed # visible browser
|
|
347
348
|
pnpm sweetlink daemon stop
|
|
348
349
|
```
|
|
349
350
|
|
|
351
|
+
### proof — Upload session evidence to GitHub PR
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
pnpm sweetlink proof --pr 123
|
|
355
|
+
pnpm sweetlink proof --pr 123 --repo owner/repo
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Other useful commands
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
pnpm sweetlink wait --url http://localhost:3000 # Wait for server ready
|
|
362
|
+
pnpm sweetlink status # Check server health
|
|
363
|
+
pnpm sweetlink vitals # Get Web Vitals
|
|
364
|
+
pnpm sweetlink a11y # Accessibility audit
|
|
365
|
+
pnpm sweetlink schema # Page schema (meta, OG, JSON-LD)
|
|
366
|
+
pnpm sweetlink outline # Document heading structure
|
|
367
|
+
pnpm sweetlink cleanup --force # Clean up Sweetlink processes
|
|
368
|
+
```
|
|
369
|
+
|
|
350
370
|
## Agent-Browser Commands
|
|
351
371
|
|
|
352
372
|
### Basic Usage
|
package/dist/cli/sweetlink.js
CHANGED
|
@@ -1843,6 +1843,49 @@ const COMMAND_HELP = {
|
|
|
1843
1843
|
pnpm sweetlink snapshot -i
|
|
1844
1844
|
pnpm sweetlink click @e3
|
|
1845
1845
|
pnpm sweetlink record stop`,
|
|
1846
|
+
report: ` report [options]
|
|
1847
|
+
Print or share the latest session report.
|
|
1848
|
+
|
|
1849
|
+
Modes:
|
|
1850
|
+
(default) Print SUMMARY.md to stdout
|
|
1851
|
+
--clipboard Copy SUMMARY.md to clipboard (pbcopy on macOS, xclip on Linux)
|
|
1852
|
+
--serve Start a temporary HTTP server serving viewer.html
|
|
1853
|
+
--webhook <url> POST session data as JSON to a webhook URL
|
|
1854
|
+
|
|
1855
|
+
Options:
|
|
1856
|
+
--session <dir> Session directory (default: .sweetlink)
|
|
1857
|
+
|
|
1858
|
+
Examples:
|
|
1859
|
+
pnpm sweetlink report
|
|
1860
|
+
pnpm sweetlink report --clipboard
|
|
1861
|
+
pnpm sweetlink report --serve
|
|
1862
|
+
pnpm sweetlink report --webhook https://hooks.slack.com/...`,
|
|
1863
|
+
demo: ` demo <subcommand> [options]
|
|
1864
|
+
Build a Markdown demo document step-by-step.
|
|
1865
|
+
Each command appends a section. The result is a reproducible
|
|
1866
|
+
tutorial/proof document with embedded outputs and screenshots.
|
|
1867
|
+
|
|
1868
|
+
Subcommands:
|
|
1869
|
+
init <title> Start a new demo document
|
|
1870
|
+
note <text> Add a prose note section
|
|
1871
|
+
exec <command> Run command, capture output inline
|
|
1872
|
+
screenshot [--caption] Take hifi screenshot and embed
|
|
1873
|
+
snapshot Capture accessibility tree inline
|
|
1874
|
+
pop Remove the last section
|
|
1875
|
+
verify Re-run all exec sections and verify outputs
|
|
1876
|
+
status Show current demo state
|
|
1877
|
+
|
|
1878
|
+
Options:
|
|
1879
|
+
--url <url> Dev server URL (for screenshots/snapshots)
|
|
1880
|
+
--output <dir> Output directory (default: .sweetlink/demo)
|
|
1881
|
+
--caption <text> Caption for screenshot
|
|
1882
|
+
|
|
1883
|
+
Examples:
|
|
1884
|
+
pnpm sweetlink demo init "How to use the search feature"
|
|
1885
|
+
pnpm sweetlink demo note "First, navigate to the search page."
|
|
1886
|
+
pnpm sweetlink demo exec "pnpm test -- --grep search"
|
|
1887
|
+
pnpm sweetlink demo screenshot --caption "Search results"
|
|
1888
|
+
pnpm sweetlink demo verify`,
|
|
1846
1889
|
};
|
|
1847
1890
|
// Aliases that map to canonical command names
|
|
1848
1891
|
const COMMAND_ALIASES = {
|
|
@@ -1947,6 +1990,8 @@ if (hasFlag('--output-schema')) {
|
|
|
1947
1990
|
'console',
|
|
1948
1991
|
'record',
|
|
1949
1992
|
'proof',
|
|
1993
|
+
'report',
|
|
1994
|
+
'demo',
|
|
1950
1995
|
];
|
|
1951
1996
|
const schemaCommand = knownCommands.includes(commandType)
|
|
1952
1997
|
? commandType === 'measure'
|
|
@@ -2313,8 +2358,23 @@ async function handleStatusCommand() {
|
|
|
2313
2358
|
else if (subcommand === 'stop') {
|
|
2314
2359
|
const resp = await daemonRequest(state, 'record-stop');
|
|
2315
2360
|
const data = resp.data;
|
|
2316
|
-
|
|
2317
|
-
console.log(
|
|
2361
|
+
const m = data.manifest;
|
|
2362
|
+
console.log(`[Sweetlink] Recording stopped: ${m.sessionId}`);
|
|
2363
|
+
console.log(` Duration: ${m.duration.toFixed(1)}s | Actions: ${m.commands.length}${m.video ? ' | Video: ' + m.video : ''}`);
|
|
2364
|
+
// Auto-open the viewer
|
|
2365
|
+
if (data.viewerPath && !hasFlag('--no-open')) {
|
|
2366
|
+
console.log(` Viewer: ${data.viewerPath}`);
|
|
2367
|
+
const { execFile } = await import('child_process');
|
|
2368
|
+
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
2369
|
+
execFile(openCmd, [data.viewerPath], (err) => {
|
|
2370
|
+
if (err)
|
|
2371
|
+
console.error(' Could not open viewer:', err.message);
|
|
2372
|
+
});
|
|
2373
|
+
console.log(` Opened in browser.`);
|
|
2374
|
+
}
|
|
2375
|
+
else if (data.viewerPath) {
|
|
2376
|
+
console.log(` Viewer: ${data.viewerPath}`);
|
|
2377
|
+
}
|
|
2318
2378
|
result = data;
|
|
2319
2379
|
}
|
|
2320
2380
|
else {
|
|
@@ -2330,6 +2390,237 @@ async function handleStatusCommand() {
|
|
|
2330
2390
|
}
|
|
2331
2391
|
break;
|
|
2332
2392
|
}
|
|
2393
|
+
case 'report': {
|
|
2394
|
+
const sessionDirArg = getArg('--session') ?? '.sweetlink';
|
|
2395
|
+
// Find latest session directory
|
|
2396
|
+
if (!fs.existsSync(sessionDirArg)) {
|
|
2397
|
+
console.error(`[Sweetlink] Session directory not found: ${sessionDirArg}`);
|
|
2398
|
+
process.exit(1);
|
|
2399
|
+
}
|
|
2400
|
+
const reportSessions = fs.readdirSync(sessionDirArg)
|
|
2401
|
+
.filter((f) => f.startsWith('session-'))
|
|
2402
|
+
.sort()
|
|
2403
|
+
.reverse();
|
|
2404
|
+
if (reportSessions.length === 0) {
|
|
2405
|
+
console.error('[Sweetlink] No session found. Run `record start` and `record stop` first.');
|
|
2406
|
+
process.exit(1);
|
|
2407
|
+
}
|
|
2408
|
+
const reportSessionDir = path.join(sessionDirArg, reportSessions[0]);
|
|
2409
|
+
if (hasFlag('--clipboard')) {
|
|
2410
|
+
// Copy SUMMARY.md to clipboard
|
|
2411
|
+
const summaryPath = path.join(reportSessionDir, 'SUMMARY.md');
|
|
2412
|
+
if (!fs.existsSync(summaryPath)) {
|
|
2413
|
+
console.error(`[Sweetlink] SUMMARY.md not found at ${summaryPath}`);
|
|
2414
|
+
process.exit(1);
|
|
2415
|
+
}
|
|
2416
|
+
const summaryContent = fs.readFileSync(summaryPath, 'utf-8');
|
|
2417
|
+
const { execFileSync } = await import('child_process');
|
|
2418
|
+
const clipCmd = process.platform === 'darwin' ? 'pbcopy' : 'xclip';
|
|
2419
|
+
const clipArgs = process.platform === 'darwin' ? [] : ['-selection', 'clipboard'];
|
|
2420
|
+
try {
|
|
2421
|
+
execFileSync(clipCmd, clipArgs, { input: summaryContent });
|
|
2422
|
+
console.log('[Sweetlink] SUMMARY.md copied to clipboard.');
|
|
2423
|
+
}
|
|
2424
|
+
catch (err) {
|
|
2425
|
+
console.error(`[Sweetlink] Failed to copy to clipboard (${clipCmd}):`, err instanceof Error ? err.message : err);
|
|
2426
|
+
process.exit(1);
|
|
2427
|
+
}
|
|
2428
|
+
result = { mode: 'clipboard', session: reportSessions[0] };
|
|
2429
|
+
}
|
|
2430
|
+
else if (hasFlag('--serve')) {
|
|
2431
|
+
// Serve viewer.html on a random port
|
|
2432
|
+
const viewerPath = path.join(reportSessionDir, 'viewer.html');
|
|
2433
|
+
if (!fs.existsSync(viewerPath)) {
|
|
2434
|
+
console.error(`[Sweetlink] viewer.html not found at ${viewerPath}`);
|
|
2435
|
+
process.exit(1);
|
|
2436
|
+
}
|
|
2437
|
+
const viewerContent = fs.readFileSync(viewerPath, 'utf-8');
|
|
2438
|
+
const http = await import('http');
|
|
2439
|
+
const port = 10000 + Math.floor(Math.random() * 50000);
|
|
2440
|
+
const server = http.createServer((_req, res) => {
|
|
2441
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2442
|
+
res.end(viewerContent);
|
|
2443
|
+
});
|
|
2444
|
+
server.listen(port, '0.0.0.0', () => {
|
|
2445
|
+
const os = require('os');
|
|
2446
|
+
const nets = os.networkInterfaces();
|
|
2447
|
+
let lanIp = 'localhost';
|
|
2448
|
+
for (const name of Object.keys(nets)) {
|
|
2449
|
+
for (const net of nets[name]) {
|
|
2450
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
2451
|
+
lanIp = net.address;
|
|
2452
|
+
break;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
if (lanIp !== 'localhost')
|
|
2456
|
+
break;
|
|
2457
|
+
}
|
|
2458
|
+
console.log(`[Sweetlink] Serving viewer at:`);
|
|
2459
|
+
console.log(` Local: http://localhost:${port}`);
|
|
2460
|
+
console.log(` Network: http://${lanIp}:${port}`);
|
|
2461
|
+
console.log(' Press Ctrl+C to stop.');
|
|
2462
|
+
});
|
|
2463
|
+
// Keep running until Ctrl+C
|
|
2464
|
+
await new Promise(() => { });
|
|
2465
|
+
}
|
|
2466
|
+
else if (getArg('--webhook')) {
|
|
2467
|
+
// POST session data to webhook
|
|
2468
|
+
const webhookUrl = getArg('--webhook');
|
|
2469
|
+
const manifestPath = path.join(reportSessionDir, 'sweetlink-session.json');
|
|
2470
|
+
const summaryPath = path.join(reportSessionDir, 'SUMMARY.md');
|
|
2471
|
+
if (!fs.existsSync(manifestPath)) {
|
|
2472
|
+
console.error(`[Sweetlink] Manifest not found at ${manifestPath}`);
|
|
2473
|
+
process.exit(1);
|
|
2474
|
+
}
|
|
2475
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
2476
|
+
const summary = fs.existsSync(summaryPath) ? fs.readFileSync(summaryPath, 'utf-8') : '';
|
|
2477
|
+
const payload = { summary, manifest };
|
|
2478
|
+
// Include viewer HTML for Slack/Discord webhooks
|
|
2479
|
+
if (/slack|discord/i.test(webhookUrl)) {
|
|
2480
|
+
const viewerPath = path.join(reportSessionDir, 'viewer.html');
|
|
2481
|
+
if (fs.existsSync(viewerPath)) {
|
|
2482
|
+
payload.viewerHtml = fs.readFileSync(viewerPath, 'utf-8');
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
const body = JSON.stringify(payload);
|
|
2486
|
+
const res = await fetch(webhookUrl, {
|
|
2487
|
+
method: 'POST',
|
|
2488
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2489
|
+
body,
|
|
2490
|
+
});
|
|
2491
|
+
if (res.ok) {
|
|
2492
|
+
console.log(`[Sweetlink] Report posted to ${webhookUrl} (${res.status})`);
|
|
2493
|
+
}
|
|
2494
|
+
else {
|
|
2495
|
+
console.error(`[Sweetlink] Webhook failed: ${res.status} ${res.statusText}`);
|
|
2496
|
+
process.exit(1);
|
|
2497
|
+
}
|
|
2498
|
+
result = { mode: 'webhook', url: webhookUrl, status: res.status };
|
|
2499
|
+
}
|
|
2500
|
+
else {
|
|
2501
|
+
// Default: print SUMMARY.md to stdout
|
|
2502
|
+
const summaryPath = path.join(reportSessionDir, 'SUMMARY.md');
|
|
2503
|
+
if (!fs.existsSync(summaryPath)) {
|
|
2504
|
+
console.error(`[Sweetlink] SUMMARY.md not found at ${summaryPath}`);
|
|
2505
|
+
process.exit(1);
|
|
2506
|
+
}
|
|
2507
|
+
const summaryContent = fs.readFileSync(summaryPath, 'utf-8');
|
|
2508
|
+
process.stdout.write(summaryContent);
|
|
2509
|
+
result = { mode: 'stdout', session: reportSessions[0] };
|
|
2510
|
+
}
|
|
2511
|
+
break;
|
|
2512
|
+
}
|
|
2513
|
+
case 'demo': {
|
|
2514
|
+
const sub = args[1];
|
|
2515
|
+
const projRoot = findProjectRoot();
|
|
2516
|
+
const demoDir = getArg('--output') ?? path.join(projRoot, '.sweetlink', 'demo');
|
|
2517
|
+
const stateFile = path.join(demoDir, 'demo-state.json');
|
|
2518
|
+
// Lazy import demo module
|
|
2519
|
+
const demoMod = await import('../daemon/demo.js');
|
|
2520
|
+
if (sub === 'init') {
|
|
2521
|
+
const title = args[2];
|
|
2522
|
+
if (!title) {
|
|
2523
|
+
console.error('[Sweetlink] Error: demo init requires a title');
|
|
2524
|
+
process.exit(1);
|
|
2525
|
+
}
|
|
2526
|
+
const demoState = await demoMod.initDemo(title, demoDir, { url: getArg('--url') });
|
|
2527
|
+
await demoMod.writeDemo(demoState);
|
|
2528
|
+
console.log(`[Sweetlink] Demo initialized: ${demoState.filePath}`);
|
|
2529
|
+
result = { filePath: demoState.filePath };
|
|
2530
|
+
}
|
|
2531
|
+
else if (sub === 'note') {
|
|
2532
|
+
const text = args.slice(2).join(' ');
|
|
2533
|
+
if (!text) {
|
|
2534
|
+
console.error('[Sweetlink] Error: demo note requires text');
|
|
2535
|
+
process.exit(1);
|
|
2536
|
+
}
|
|
2537
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
2538
|
+
const updated = demoMod.addNote(state, text);
|
|
2539
|
+
await demoMod.writeDemo(updated);
|
|
2540
|
+
console.log(`[Sweetlink] Note added (${updated.sections.length} sections)`);
|
|
2541
|
+
result = { sections: updated.sections.length };
|
|
2542
|
+
}
|
|
2543
|
+
else if (sub === 'exec') {
|
|
2544
|
+
const cmd = args.slice(2).join(' ');
|
|
2545
|
+
if (!cmd) {
|
|
2546
|
+
console.error('[Sweetlink] Error: demo exec requires a command');
|
|
2547
|
+
process.exit(1);
|
|
2548
|
+
}
|
|
2549
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
2550
|
+
const updated = await demoMod.addExec(state, cmd, []);
|
|
2551
|
+
await demoMod.writeDemo(updated);
|
|
2552
|
+
const lastSection = updated.sections[updated.sections.length - 1];
|
|
2553
|
+
console.log(`[Sweetlink] Exec added: ${cmd} (exit ${lastSection.exitCode ?? 0})`);
|
|
2554
|
+
result = { sections: updated.sections.length, exitCode: lastSection.exitCode };
|
|
2555
|
+
}
|
|
2556
|
+
else if (sub === 'screenshot') {
|
|
2557
|
+
const targetUrl = getArg('--url') ?? 'http://localhost:3000';
|
|
2558
|
+
const caption = getArg('--caption') ?? 'Screenshot';
|
|
2559
|
+
const daemonState = await ensureDaemon(projRoot, targetUrl);
|
|
2560
|
+
const resp = await daemonRequest(daemonState, 'screenshot', {});
|
|
2561
|
+
const data = resp.data;
|
|
2562
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
2563
|
+
const updated = await demoMod.addScreenshot(state, Buffer.from(data.screenshot, 'base64'), caption);
|
|
2564
|
+
await demoMod.writeDemo(updated);
|
|
2565
|
+
console.log(`[Sweetlink] Screenshot added: ${caption}`);
|
|
2566
|
+
result = { sections: updated.sections.length };
|
|
2567
|
+
}
|
|
2568
|
+
else if (sub === 'snapshot') {
|
|
2569
|
+
const targetUrl = getArg('--url') ?? 'http://localhost:3000';
|
|
2570
|
+
const daemonState = await ensureDaemon(projRoot, targetUrl);
|
|
2571
|
+
const resp = await daemonRequest(daemonState, 'snapshot', { interactive: true });
|
|
2572
|
+
const data = resp.data;
|
|
2573
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
2574
|
+
const updated = demoMod.addSnapshot(state, data.tree);
|
|
2575
|
+
await demoMod.writeDemo(updated);
|
|
2576
|
+
console.log(`[Sweetlink] Snapshot added (${updated.sections.length} sections)`);
|
|
2577
|
+
result = { sections: updated.sections.length };
|
|
2578
|
+
}
|
|
2579
|
+
else if (sub === 'pop') {
|
|
2580
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
2581
|
+
const updated = demoMod.popSection(state);
|
|
2582
|
+
await demoMod.writeDemo(updated);
|
|
2583
|
+
console.log(`[Sweetlink] Last section removed (${updated.sections.length} remaining)`);
|
|
2584
|
+
result = { sections: updated.sections.length };
|
|
2585
|
+
}
|
|
2586
|
+
else if (sub === 'verify') {
|
|
2587
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
2588
|
+
const verifyResult = await demoMod.verifyDemo(state);
|
|
2589
|
+
if (verifyResult.passed) {
|
|
2590
|
+
console.log('[Sweetlink] Demo verified: all outputs match');
|
|
2591
|
+
}
|
|
2592
|
+
else {
|
|
2593
|
+
console.log(`[Sweetlink] Demo verification FAILED: ${verifyResult.failures.length} mismatch(es)`);
|
|
2594
|
+
for (const f of verifyResult.failures) {
|
|
2595
|
+
console.log(` Section ${f.index}: ${f.command}`);
|
|
2596
|
+
console.log(` Expected: ${f.expected.substring(0, 80)}...`);
|
|
2597
|
+
console.log(` Actual: ${f.actual.substring(0, 80)}...`);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
result = verifyResult;
|
|
2601
|
+
}
|
|
2602
|
+
else {
|
|
2603
|
+
// status
|
|
2604
|
+
if (fs.existsSync(stateFile)) {
|
|
2605
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
2606
|
+
console.log(`[Sweetlink] Demo: "${state.title}" (${state.sections.length} sections)`);
|
|
2607
|
+
console.log(` File: ${state.filePath}`);
|
|
2608
|
+
for (const s of state.sections) {
|
|
2609
|
+
const preview = s.type === 'note' ? s.content.substring(0, 60) :
|
|
2610
|
+
s.type === 'exec' ? `$ ${s.command}` :
|
|
2611
|
+
s.type === 'screenshot' ? `[image] ${s.screenshotFile}` :
|
|
2612
|
+
'[snapshot]';
|
|
2613
|
+
console.log(` ${s.type.padEnd(12)} ${preview}`);
|
|
2614
|
+
}
|
|
2615
|
+
result = state;
|
|
2616
|
+
}
|
|
2617
|
+
else {
|
|
2618
|
+
console.log('[Sweetlink] No demo in progress. Run `demo init <title>` to start.');
|
|
2619
|
+
result = null;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
break;
|
|
2623
|
+
}
|
|
2333
2624
|
case 'daemon': {
|
|
2334
2625
|
const subcommand = args[1];
|
|
2335
2626
|
const projRoot = findProjectRoot();
|
|
@@ -2340,7 +2631,8 @@ async function handleStatusCommand() {
|
|
|
2340
2631
|
}
|
|
2341
2632
|
else if (subcommand === 'start') {
|
|
2342
2633
|
const targetUrl = getArg('--url') ?? 'http://localhost:3000';
|
|
2343
|
-
const
|
|
2634
|
+
const headedFlag = hasFlag('--headed');
|
|
2635
|
+
const state = await ensureDaemon(projRoot, targetUrl, { headed: headedFlag });
|
|
2344
2636
|
console.log(`[Sweetlink] Daemon running on port ${state.port} (PID: ${state.pid})`);
|
|
2345
2637
|
result = {
|
|
2346
2638
|
running: true,
|