@ytspar/sweetlink 1.13.0 → 1.14.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 (78) hide show
  1. package/README.md +91 -12
  2. package/claude-skills/screenshot/SKILL.md +121 -20
  3. package/dist/cli/outputSchemas.d.ts +16 -0
  4. package/dist/cli/outputSchemas.d.ts.map +1 -1
  5. package/dist/cli/outputSchemas.js +33 -0
  6. package/dist/cli/outputSchemas.js.map +1 -1
  7. package/dist/cli/sweetlink-dev.js +0 -0
  8. package/dist/cli/sweetlink.js +347 -11
  9. package/dist/cli/sweetlink.js.map +1 -1
  10. package/dist/daemon/browser.d.ts +51 -0
  11. package/dist/daemon/browser.d.ts.map +1 -0
  12. package/dist/daemon/browser.js +153 -0
  13. package/dist/daemon/browser.js.map +1 -0
  14. package/dist/daemon/client.d.ts +32 -0
  15. package/dist/daemon/client.d.ts.map +1 -0
  16. package/dist/daemon/client.js +133 -0
  17. package/dist/daemon/client.js.map +1 -0
  18. package/dist/daemon/cursor.d.ts +15 -0
  19. package/dist/daemon/cursor.d.ts.map +1 -0
  20. package/dist/daemon/cursor.js +76 -0
  21. package/dist/daemon/cursor.js.map +1 -0
  22. package/dist/daemon/devices.d.ts +39 -0
  23. package/dist/daemon/devices.d.ts.map +1 -0
  24. package/dist/daemon/devices.js +101 -0
  25. package/dist/daemon/devices.js.map +1 -0
  26. package/dist/daemon/diff.d.ts +20 -0
  27. package/dist/daemon/diff.d.ts.map +1 -0
  28. package/dist/daemon/diff.js +181 -0
  29. package/dist/daemon/diff.js.map +1 -0
  30. package/dist/daemon/evidence.d.ts +29 -0
  31. package/dist/daemon/evidence.d.ts.map +1 -0
  32. package/dist/daemon/evidence.js +130 -0
  33. package/dist/daemon/evidence.js.map +1 -0
  34. package/dist/daemon/index.d.ts +10 -0
  35. package/dist/daemon/index.d.ts.map +1 -0
  36. package/dist/daemon/index.js +90 -0
  37. package/dist/daemon/index.js.map +1 -0
  38. package/dist/daemon/listeners.d.ts +55 -0
  39. package/dist/daemon/listeners.d.ts.map +1 -0
  40. package/dist/daemon/listeners.js +129 -0
  41. package/dist/daemon/listeners.js.map +1 -0
  42. package/dist/daemon/recording.d.ts +44 -0
  43. package/dist/daemon/recording.d.ts.map +1 -0
  44. package/dist/daemon/recording.js +133 -0
  45. package/dist/daemon/recording.js.map +1 -0
  46. package/dist/daemon/refs.d.ts +70 -0
  47. package/dist/daemon/refs.d.ts.map +1 -0
  48. package/dist/daemon/refs.js +185 -0
  49. package/dist/daemon/refs.js.map +1 -0
  50. package/dist/daemon/ringBuffer.d.ts +26 -0
  51. package/dist/daemon/ringBuffer.d.ts.map +1 -0
  52. package/dist/daemon/ringBuffer.js +54 -0
  53. package/dist/daemon/ringBuffer.js.map +1 -0
  54. package/dist/daemon/server.d.ts +23 -0
  55. package/dist/daemon/server.d.ts.map +1 -0
  56. package/dist/daemon/server.js +508 -0
  57. package/dist/daemon/server.js.map +1 -0
  58. package/dist/daemon/session.d.ts +41 -0
  59. package/dist/daemon/session.d.ts.map +1 -0
  60. package/dist/daemon/session.js +8 -0
  61. package/dist/daemon/session.js.map +1 -0
  62. package/dist/daemon/stateFile.d.ts +49 -0
  63. package/dist/daemon/stateFile.d.ts.map +1 -0
  64. package/dist/daemon/stateFile.js +162 -0
  65. package/dist/daemon/stateFile.js.map +1 -0
  66. package/dist/daemon/types.d.ts +72 -0
  67. package/dist/daemon/types.d.ts.map +1 -0
  68. package/dist/daemon/types.js +28 -0
  69. package/dist/daemon/types.js.map +1 -0
  70. package/dist/daemon/viewer.d.ts +33 -0
  71. package/dist/daemon/viewer.d.ts.map +1 -0
  72. package/dist/daemon/viewer.js +226 -0
  73. package/dist/daemon/viewer.js.map +1 -0
  74. package/dist/daemon/visualDiff.d.ts +34 -0
  75. package/dist/daemon/visualDiff.d.ts.map +1 -0
  76. package/dist/daemon/visualDiff.js +80 -0
  77. package/dist/daemon/visualDiff.js.map +1 -0
  78. package/package.json +20 -12
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Daemon State File Management
3
+ *
4
+ * Handles reading, writing, and cleanup of .sweetlink/daemon.json.
5
+ * Uses atomic writes (tmp + rename) and lockfiles to prevent race conditions.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { DAEMON_LOCK_FILE, DAEMON_STATE_DIR, DAEMON_STATE_FILE } from './types.js';
10
+ /**
11
+ * Get the .sweetlink directory path for a given project root
12
+ */
13
+ export function getStateDir(projectRoot) {
14
+ return path.join(projectRoot, DAEMON_STATE_DIR);
15
+ }
16
+ /**
17
+ * Get the daemon.json file path
18
+ */
19
+ export function getStateFilePath(projectRoot) {
20
+ return path.join(getStateDir(projectRoot), DAEMON_STATE_FILE);
21
+ }
22
+ /**
23
+ * Get the daemon.lock file path
24
+ */
25
+ export function getLockFilePath(projectRoot) {
26
+ return path.join(getStateDir(projectRoot), DAEMON_LOCK_FILE);
27
+ }
28
+ /**
29
+ * Write daemon state to .sweetlink/daemon.json atomically.
30
+ * Creates the directory if it doesn't exist.
31
+ * Sets file permissions to 600 (owner read/write only) for security.
32
+ */
33
+ export function writeDaemonState(projectRoot, state) {
34
+ const dir = getStateDir(projectRoot);
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+ const stateFile = getStateFilePath(projectRoot);
39
+ const tmpFile = stateFile + '.tmp';
40
+ // Atomic write: write to tmp, then rename
41
+ fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2), { mode: 0o600 });
42
+ fs.renameSync(tmpFile, stateFile);
43
+ }
44
+ /**
45
+ * Read daemon state from .sweetlink/daemon.json.
46
+ * Returns null if the file doesn't exist or is invalid.
47
+ */
48
+ export function readDaemonState(projectRoot) {
49
+ const stateFile = getStateFilePath(projectRoot);
50
+ try {
51
+ const content = fs.readFileSync(stateFile, 'utf-8');
52
+ const state = JSON.parse(content);
53
+ // Validate required fields
54
+ if (typeof state.pid !== 'number' ||
55
+ typeof state.port !== 'number' ||
56
+ typeof state.token !== 'string' ||
57
+ typeof state.startedAt !== 'string' ||
58
+ typeof state.url !== 'string') {
59
+ return null;
60
+ }
61
+ return state;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ /**
68
+ * Remove daemon state file and lock file
69
+ */
70
+ export function removeDaemonState(projectRoot) {
71
+ const stateFile = getStateFilePath(projectRoot);
72
+ const lockFile = getLockFilePath(projectRoot);
73
+ try {
74
+ fs.unlinkSync(stateFile);
75
+ }
76
+ catch {
77
+ // File may not exist
78
+ }
79
+ try {
80
+ fs.unlinkSync(lockFile);
81
+ }
82
+ catch {
83
+ // File may not exist
84
+ }
85
+ }
86
+ /**
87
+ * Check if a daemon is alive by sending an HTTP ping.
88
+ * Returns true if the daemon responds, false otherwise.
89
+ */
90
+ export async function isDaemonAlive(state) {
91
+ try {
92
+ const controller = new AbortController();
93
+ const timeout = setTimeout(() => controller.abort(), 2000);
94
+ const response = await fetch(`http://127.0.0.1:${state.port}/api/ping`, {
95
+ method: 'POST',
96
+ headers: {
97
+ 'Content-Type': 'application/json',
98
+ Authorization: `Bearer ${state.token}`,
99
+ },
100
+ body: JSON.stringify({ action: 'ping' }),
101
+ signal: controller.signal,
102
+ });
103
+ clearTimeout(timeout);
104
+ return response.ok;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ /**
111
+ * Acquire a lockfile to prevent concurrent daemon starts.
112
+ * Returns true if the lock was acquired, false if another process holds it.
113
+ */
114
+ export function acquireLock(projectRoot) {
115
+ const dir = getStateDir(projectRoot);
116
+ if (!fs.existsSync(dir)) {
117
+ fs.mkdirSync(dir, { recursive: true });
118
+ }
119
+ const lockFile = getLockFilePath(projectRoot);
120
+ try {
121
+ // 'wx' flag: write exclusive — fails if file exists
122
+ fs.writeFileSync(lockFile, String(process.pid), { flag: 'wx', mode: 0o600 });
123
+ return true;
124
+ }
125
+ catch {
126
+ // Check if the lock is stale (holding process is dead)
127
+ try {
128
+ const pid = parseInt(fs.readFileSync(lockFile, 'utf-8').trim(), 10);
129
+ if (!isNaN(pid)) {
130
+ try {
131
+ // Signal 0 checks if process exists without killing it
132
+ process.kill(pid, 0);
133
+ // Process is alive — lock is valid
134
+ return false;
135
+ }
136
+ catch {
137
+ // Process is dead — stale lock, remove and retry
138
+ fs.unlinkSync(lockFile);
139
+ fs.writeFileSync(lockFile, String(process.pid), { flag: 'wx', mode: 0o600 });
140
+ return true;
141
+ }
142
+ }
143
+ }
144
+ catch {
145
+ // Can't read lock file — another process may be writing it
146
+ }
147
+ return false;
148
+ }
149
+ }
150
+ /**
151
+ * Release the lockfile
152
+ */
153
+ export function releaseLock(projectRoot) {
154
+ const lockFile = getLockFilePath(projectRoot);
155
+ try {
156
+ fs.unlinkSync(lockFile);
157
+ }
158
+ catch {
159
+ // File may not exist
160
+ }
161
+ }
162
+ //# sourceMappingURL=stateFile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stateFile.js","sourceRoot":"","sources":["../../src/daemon/stateFile.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnF;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,WAAmB;IAC7C,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;AAClD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB;IAClD,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,iBAAiB,CAAC,CAAC;AAChE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,WAAmB;IACjD,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC,CAAC;AAC/D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,KAAkB;IACtE,MAAM,GAAG,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;IACrC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,SAAS,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IAEnC,0CAA0C;IAC1C,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3E,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,WAAmB;IACjD,MAAM,SAAS,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAChD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACpD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAgB,CAAC;QACjD,2BAA2B;QAC3B,IACE,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ;YAC7B,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;YAC9B,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;YAC/B,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ;YACnC,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,EAC7B,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAAmB;IACnD,MAAM,SAAS,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAC9C,IAAI,CAAC;QACH,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,qBAAqB;IACvB,CAAC;IACD,IAAI,CAAC;QACH,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,qBAAqB;IACvB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAkB;IACpD,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,oBAAoB,KAAK,CAAC,IAAI,WAAW,EAAE;YACtE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,KAAK,CAAC,KAAK,EAAE;aACvC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;YACxC,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;QACH,YAAY,CAAC,OAAO,CAAC,CAAC;QACtB,OAAO,QAAQ,CAAC,EAAE,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,WAAmB;IAC7C,MAAM,GAAG,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;IACrC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,QAAQ,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAC9C,IAAI,CAAC;QACH,oDAAoD;QACpD,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7E,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,uDAAuD;QACvD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,QAAQ,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;YACpE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBAChB,IAAI,CAAC;oBACH,uDAAuD;oBACvD,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;oBACrB,mCAAmC;oBACnC,OAAO,KAAK,CAAC;gBACf,CAAC;gBAAC,MAAM,CAAC;oBACP,iDAAiD;oBACjD,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;oBACxB,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;oBAC7E,OAAO,IAAI,CAAC;gBACd,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,2DAA2D;QAC7D,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,WAAmB;IAC7C,MAAM,QAAQ,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAC9C,IAAI,CAAC;QACH,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,qBAAqB;IACvB,CAAC;AACH,CAAC"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Daemon Types
3
+ *
4
+ * Types for the persistent Playwright daemon process.
5
+ * Separate from main types.ts to avoid polluting the browser bundle.
6
+ */
7
+ /** Persisted daemon state written to .sweetlink/daemon.json */
8
+ export interface DaemonState {
9
+ pid: number;
10
+ port: number;
11
+ token: string;
12
+ startedAt: string;
13
+ url: string;
14
+ lastActivity: string;
15
+ }
16
+ /** Actions the daemon HTTP server handles */
17
+ export type DaemonAction = 'ping' | 'shutdown' | 'screenshot' | 'screenshot-responsive' | 'snapshot' | 'click-ref' | 'fill-ref' | 'hover-ref' | 'press-key' | 'console-read' | 'network-read' | 'dialog-read' | 'screenshot-devices' | 'visual-diff' | 'record-start' | 'record-stop' | 'record-status' | 'generate-viewer';
18
+ /** Request body for daemon HTTP POST */
19
+ export interface DaemonRequest {
20
+ action: DaemonAction;
21
+ params?: Record<string, unknown>;
22
+ }
23
+ /** Response from daemon HTTP server */
24
+ export interface DaemonResponse {
25
+ ok: boolean;
26
+ data?: Record<string, unknown>;
27
+ error?: string;
28
+ }
29
+ export interface ScreenshotParams {
30
+ selector?: string;
31
+ fullPage?: boolean;
32
+ viewport?: string;
33
+ output?: string;
34
+ }
35
+ export interface ScreenshotResponseData {
36
+ screenshot: string;
37
+ width: number;
38
+ height: number;
39
+ path?: string;
40
+ }
41
+ export interface ResponsiveScreenshotParams {
42
+ viewports?: number[];
43
+ fullPage?: boolean;
44
+ output?: string;
45
+ }
46
+ export interface ResponsiveScreenshotResponseData {
47
+ screenshots: Array<{
48
+ width: number;
49
+ height: number;
50
+ screenshot: string;
51
+ label: string;
52
+ }>;
53
+ }
54
+ /** Minimum port for daemon HTTP server */
55
+ export declare const DAEMON_PORT_MIN = 10000;
56
+ /** Maximum port for daemon HTTP server */
57
+ export declare const DAEMON_PORT_MAX = 60000;
58
+ /** Idle timeout before daemon auto-stops (ms) */
59
+ export declare const DAEMON_IDLE_TIMEOUT_MS: number;
60
+ /** Max time to wait for daemon to start (ms) */
61
+ export declare const DAEMON_SPAWN_TIMEOUT_MS = 15000;
62
+ /** Poll interval when waiting for daemon state file (ms) */
63
+ export declare const DAEMON_POLL_INTERVAL_MS = 200;
64
+ /** State directory name */
65
+ export declare const DAEMON_STATE_DIR = ".sweetlink";
66
+ /** State file name */
67
+ export declare const DAEMON_STATE_FILE = "daemon.json";
68
+ /** Lock file name */
69
+ export declare const DAEMON_LOCK_FILE = "daemon.lock";
70
+ /** Default responsive viewports */
71
+ export declare const DEFAULT_RESPONSIVE_VIEWPORTS: number[];
72
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/daemon/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,+DAA+D;AAC/D,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;CACtB;AAMD,6CAA6C;AAC7C,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,UAAU,GACV,YAAY,GACZ,uBAAuB,GACvB,UAAU,GACV,WAAW,GACX,UAAU,GACV,WAAW,GACX,WAAW,GACX,cAAc,GACd,cAAc,GACd,aAAa,GACb,oBAAoB,GACpB,aAAa,GACb,cAAc,GACd,aAAa,GACb,eAAe,GACf,iBAAiB,CAAC;AAEtB,wCAAwC;AACxC,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gCAAgC;IAC/C,WAAW,EAAE,KAAK,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;CACJ;AAMD,0CAA0C;AAC1C,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC,0CAA0C;AAC1C,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC,iDAAiD;AACjD,eAAO,MAAM,sBAAsB,QAAiB,CAAC;AAErD,gDAAgD;AAChD,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,4DAA4D;AAC5D,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAE3C,2BAA2B;AAC3B,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAE7C,sBAAsB;AACtB,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAE/C,qBAAqB;AACrB,eAAO,MAAM,gBAAgB,gBAAgB,CAAC;AAE9C,mCAAmC;AACnC,eAAO,MAAM,4BAA4B,UAAmB,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Daemon Types
3
+ *
4
+ * Types for the persistent Playwright daemon process.
5
+ * Separate from main types.ts to avoid polluting the browser bundle.
6
+ */
7
+ // ============================================================================
8
+ // Constants
9
+ // ============================================================================
10
+ /** Minimum port for daemon HTTP server */
11
+ export const DAEMON_PORT_MIN = 10000;
12
+ /** Maximum port for daemon HTTP server */
13
+ export const DAEMON_PORT_MAX = 60000;
14
+ /** Idle timeout before daemon auto-stops (ms) */
15
+ export const DAEMON_IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
16
+ /** Max time to wait for daemon to start (ms) */
17
+ export const DAEMON_SPAWN_TIMEOUT_MS = 15_000;
18
+ /** Poll interval when waiting for daemon state file (ms) */
19
+ export const DAEMON_POLL_INTERVAL_MS = 200;
20
+ /** State directory name */
21
+ export const DAEMON_STATE_DIR = '.sweetlink';
22
+ /** State file name */
23
+ export const DAEMON_STATE_FILE = 'daemon.json';
24
+ /** Lock file name */
25
+ export const DAEMON_LOCK_FILE = 'daemon.lock';
26
+ /** Default responsive viewports */
27
+ export const DEFAULT_RESPONSIVE_VIEWPORTS = [375, 768, 1280];
28
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/daemon/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuFH,+EAA+E;AAC/E,YAAY;AACZ,+EAA+E;AAE/E,0CAA0C;AAC1C,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AAErC,0CAA0C;AAC1C,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AAErC,iDAAiD;AACjD,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,aAAa;AAEnE,gDAAgD;AAChD,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C,4DAA4D;AAC5D,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAE3C,2BAA2B;AAC3B,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC;AAE7C,sBAAsB;AACtB,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAC;AAE/C,qBAAqB;AACrB,MAAM,CAAC,MAAM,gBAAgB,GAAG,aAAa,CAAC;AAE9C,mCAAmC;AACnC,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Interactive Viewer Generator
3
+ *
4
+ * Generates a self-contained HTML file from a session manifest.
5
+ * No external dependencies — works offline.
6
+ *
7
+ * Features:
8
+ * - Screenshot timeline with step markers
9
+ * - Dual-pane layout (screenshots + action list)
10
+ * - Click to jump to action
11
+ * - Error badge
12
+ * - Console/network log tabs
13
+ *
14
+ * Security: All user-generated content (console messages, URLs, action names)
15
+ * is escaped before insertion into the HTML template to prevent XSS.
16
+ */
17
+ import type { SessionManifest } from './session.js';
18
+ import type { ConsoleEntry, NetworkEntry } from './listeners.js';
19
+ export interface ViewerOptions {
20
+ /** Directory containing session artifacts */
21
+ sessionDir: string;
22
+ /** Output path for the viewer HTML */
23
+ outputPath?: string;
24
+ /** Console entries to embed */
25
+ consoleEntries?: ConsoleEntry[];
26
+ /** Network entries to embed */
27
+ networkEntries?: NetworkEntry[];
28
+ }
29
+ /**
30
+ * Generate a self-contained HTML viewer from a session manifest.
31
+ */
32
+ export declare function generateViewer(manifest: SessionManifest, options: ViewerOptions): Promise<string>;
33
+ //# sourceMappingURL=viewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewer.d.ts","sourceRoot":"","sources":["../../src/daemon/viewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAChC,+BAA+B;IAC/B,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;CACjC;AAcD;;GAEG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,CAyMjB"}
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Interactive Viewer Generator
3
+ *
4
+ * Generates a self-contained HTML file from a session manifest.
5
+ * No external dependencies — works offline.
6
+ *
7
+ * Features:
8
+ * - Screenshot timeline with step markers
9
+ * - Dual-pane layout (screenshots + action list)
10
+ * - Click to jump to action
11
+ * - Error badge
12
+ * - Console/network log tabs
13
+ *
14
+ * Security: All user-generated content (console messages, URLs, action names)
15
+ * is escaped before insertion into the HTML template to prevent XSS.
16
+ */
17
+ import { promises as fs } from 'fs';
18
+ import * as path from 'path';
19
+ /**
20
+ * Escape HTML special characters to prevent XSS.
21
+ */
22
+ function escapeHtml(str) {
23
+ return str
24
+ .replace(/&/g, '&amp;')
25
+ .replace(/</g, '&lt;')
26
+ .replace(/>/g, '&gt;')
27
+ .replace(/"/g, '&quot;')
28
+ .replace(/'/g, '&#39;');
29
+ }
30
+ /**
31
+ * Generate a self-contained HTML viewer from a session manifest.
32
+ */
33
+ export async function generateViewer(manifest, options) {
34
+ // Load screenshots as base64
35
+ const screenshots = [];
36
+ for (const cmd of manifest.commands) {
37
+ if (cmd.screenshot) {
38
+ const screenshotPath = path.join(options.sessionDir, cmd.screenshot);
39
+ try {
40
+ const buffer = await fs.readFile(screenshotPath);
41
+ screenshots.push({
42
+ name: cmd.screenshot,
43
+ data: buffer.toString('base64'),
44
+ action: `${cmd.action} ${cmd.args.join(' ')}`,
45
+ timestamp: cmd.timestamp,
46
+ });
47
+ }
48
+ catch {
49
+ // Screenshot file may not exist
50
+ }
51
+ }
52
+ }
53
+ const totalErrors = manifest.errors.console + manifest.errors.network + manifest.errors.server;
54
+ // Sanitize entries for safe embedding in script tag
55
+ // JSON.stringify handles escaping for script context
56
+ const sanitizedConsole = (options.consoleEntries ?? []).map(e => ({
57
+ timestamp: e.timestamp,
58
+ level: e.level,
59
+ message: String(e.message).slice(0, 500),
60
+ location: e.location ? String(e.location).slice(0, 200) : undefined,
61
+ }));
62
+ const sanitizedNetwork = (options.networkEntries ?? []).map(e => ({
63
+ timestamp: e.timestamp,
64
+ method: e.method,
65
+ url: String(e.url).slice(0, 200),
66
+ status: e.status,
67
+ duration: e.duration,
68
+ }));
69
+ // Build timeline HTML with escaped content
70
+ const timelineHtml = manifest.commands.map((cmd, i) => {
71
+ const action = escapeHtml(`${cmd.action} ${cmd.args.join(' ')}`);
72
+ return `<div class="action-item${i === 0 ? ' active' : ''}" data-index="${i}">
73
+ <span class="action-ts">${cmd.timestamp.toFixed(1)}s</span>
74
+ <span class="action-cmd">${action}</span>
75
+ </div>`;
76
+ }).join('\n');
77
+ // Build scrub markers
78
+ const scrubMarkers = screenshots.map((_, i) => `<div class="scrub-marker${i === 0 ? ' active' : ''}" data-index="${i}"></div>`).join('');
79
+ const firstScreenshot = screenshots.length > 0
80
+ ? `<img id="screenshot-img" src="data:image/png;base64,${screenshots[0].data}" />`
81
+ : '<div class="empty">No screenshots captured</div>';
82
+ const html = `<!DOCTYPE html>
83
+ <html lang="en">
84
+ <head>
85
+ <meta charset="UTF-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
87
+ <title>Sweetlink Session: ${escapeHtml(manifest.sessionId)}</title>
88
+ <style>
89
+ * { margin: 0; padding: 0; box-sizing: border-box; }
90
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; }
91
+ header { background: #1a1a1a; padding: 12px 20px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #333; }
92
+ header h1 { font-size: 14px; font-weight: 600; }
93
+ .badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; }
94
+ .badge.green { background: #064e3b; color: #6ee7b7; }
95
+ .badge.red { background: #7f1d1d; color: #fca5a5; }
96
+ .main { display: flex; flex: 1; overflow: hidden; }
97
+ .screenshot-pane { flex: 62%; display: flex; flex-direction: column; border-right: 1px solid #333; }
98
+ .screenshot-view { flex: 1; display: flex; align-items: center; justify-content: center; overflow: hidden; background: #111; padding: 16px; }
99
+ .screenshot-view img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 4px; }
100
+ .scrub-bar { height: 40px; background: #1a1a1a; display: flex; align-items: center; padding: 0 16px; gap: 4px; border-top: 1px solid #333; overflow-x: auto; }
101
+ .scrub-marker { width: 8px; height: 8px; border-radius: 50%; background: #555; cursor: pointer; flex-shrink: 0; transition: all 0.15s; }
102
+ .scrub-marker:hover, .scrub-marker.active { background: #3b82f6; transform: scale(1.5); }
103
+ .sidebar { flex: 38%; display: flex; flex-direction: column; }
104
+ .tabs { display: flex; background: #1a1a1a; border-bottom: 1px solid #333; }
105
+ .tab { padding: 8px 16px; font-size: 12px; cursor: pointer; border-bottom: 2px solid transparent; color: #888; }
106
+ .tab.active { color: #e0e0e0; border-bottom-color: #3b82f6; }
107
+ .tab-content { flex: 1; overflow-y: auto; padding: 8px; }
108
+ .tab-panel { display: none; }
109
+ .tab-panel.active { display: block; }
110
+ .action-item { padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; font-family: monospace; margin-bottom: 2px; display: flex; gap: 8px; }
111
+ .action-item:hover { background: #1a1a1a; }
112
+ .action-item.active { background: #1e3a5f; }
113
+ .action-ts { color: #888; min-width: 50px; }
114
+ .action-cmd { color: #93c5fd; }
115
+ .log-entry { padding: 4px 8px; font-size: 12px; font-family: monospace; border-bottom: 1px solid #1a1a1a; }
116
+ .log-entry.error { color: #fca5a5; }
117
+ .log-entry.warning { color: #fcd34d; }
118
+ .empty { color: #666; text-align: center; padding: 40px; }
119
+ </style>
120
+ </head>
121
+ <body>
122
+ <header>
123
+ <h1>Sweetlink Session</h1>
124
+ <div style="display:flex;gap:12px;align-items:center">
125
+ <span style="font-size:12px;color:#888">${manifest.duration.toFixed(1)}s &middot; ${manifest.commands.length} actions</span>
126
+ <span class="badge ${totalErrors === 0 ? 'green' : 'red'}">${totalErrors === 0 ? '0 errors' : totalErrors + ' errors'}</span>
127
+ </div>
128
+ </header>
129
+ <div class="main">
130
+ <div class="screenshot-pane">
131
+ <div class="screenshot-view" id="screenshot-view">
132
+ ${firstScreenshot}
133
+ </div>
134
+ <div class="scrub-bar" id="scrub-bar">
135
+ ${scrubMarkers}
136
+ </div>
137
+ </div>
138
+ <div class="sidebar">
139
+ <div class="tabs">
140
+ <div class="tab active" data-tab="timeline">Timeline</div>
141
+ <div class="tab" data-tab="console">Console</div>
142
+ <div class="tab" data-tab="network">Network</div>
143
+ </div>
144
+ <div class="tab-content">
145
+ <div class="tab-panel active" id="tab-timeline">
146
+ ${timelineHtml || '<div class="empty">No actions recorded</div>'}
147
+ </div>
148
+ <div class="tab-panel" id="tab-console">
149
+ <div id="console-entries"></div>
150
+ </div>
151
+ <div class="tab-panel" id="tab-network">
152
+ <div id="network-entries"></div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ <script>
158
+ // Screenshot data (base64 encoded, safe)
159
+ const screenshots = ${JSON.stringify(screenshots.map(s => ({ data: s.data, timestamp: s.timestamp })))};
160
+ // Sanitized log entries (JSON.stringify handles escaping)
161
+ const consoleEntries = ${JSON.stringify(sanitizedConsole)};
162
+ const networkEntries = ${JSON.stringify(sanitizedNetwork)};
163
+
164
+ // Tab switching
165
+ document.querySelectorAll('.tab').forEach(function(tab) {
166
+ tab.addEventListener('click', function() {
167
+ document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
168
+ document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
169
+ tab.classList.add('active');
170
+ document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
171
+ });
172
+ });
173
+
174
+ // Screenshot navigation
175
+ function showScreenshot(index) {
176
+ if (index < 0 || index >= screenshots.length) return;
177
+ var img = document.getElementById('screenshot-img');
178
+ if (img) img.src = 'data:image/png;base64,' + screenshots[index].data;
179
+ document.querySelectorAll('.scrub-marker').forEach(function(m, i) { m.classList.toggle('active', i === index); });
180
+ document.querySelectorAll('.action-item').forEach(function(a, i) { a.classList.toggle('active', i === index); });
181
+ }
182
+
183
+ document.querySelectorAll('.scrub-marker').forEach(function(marker) {
184
+ marker.addEventListener('click', function() { showScreenshot(parseInt(marker.dataset.index)); });
185
+ });
186
+ document.querySelectorAll('.action-item').forEach(function(item) {
187
+ item.addEventListener('click', function() { showScreenshot(parseInt(item.dataset.index)); });
188
+ });
189
+
190
+ // Populate console entries using safe DOM methods
191
+ var consoleEl = document.getElementById('console-entries');
192
+ if (consoleEntries.length === 0) {
193
+ consoleEl.textContent = 'No console messages';
194
+ consoleEl.className = 'empty';
195
+ } else {
196
+ consoleEntries.forEach(function(e) {
197
+ var div = document.createElement('div');
198
+ div.className = 'log-entry' + (e.level === 'error' ? ' error' : e.level === 'warning' ? ' warning' : '');
199
+ var time = new Date(e.timestamp).toISOString().slice(11, 19);
200
+ div.textContent = '[' + time + '] ' + e.level.toUpperCase() + ' ' + e.message;
201
+ consoleEl.appendChild(div);
202
+ });
203
+ }
204
+
205
+ // Populate network entries using safe DOM methods
206
+ var networkEl = document.getElementById('network-entries');
207
+ if (networkEntries.length === 0) {
208
+ networkEl.textContent = 'No network requests';
209
+ networkEl.className = 'empty';
210
+ } else {
211
+ networkEntries.forEach(function(e) {
212
+ var div = document.createElement('div');
213
+ div.className = 'log-entry' + ((e.status >= 400 || e.status === 0) ? ' error' : '');
214
+ var time = new Date(e.timestamp).toISOString().slice(11, 19);
215
+ div.textContent = '[' + time + '] ' + e.status + ' ' + e.method + ' ' + e.url + ' ' + e.duration + 'ms';
216
+ networkEl.appendChild(div);
217
+ });
218
+ }
219
+ </script>
220
+ </body>
221
+ </html>`;
222
+ const outputPath = options.outputPath ?? path.join(options.sessionDir, 'viewer.html');
223
+ await fs.writeFile(outputPath, html, 'utf-8');
224
+ return outputPath;
225
+ }
226
+ //# sourceMappingURL=viewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewer.js","sourceRoot":"","sources":["../../src/daemon/viewer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Visual Diff
3
+ *
4
+ * Pixel-by-pixel comparison of screenshots using a simple diff algorithm.
5
+ * Uses raw pixel comparison without external dependencies.
6
+ */
7
+ export interface VisualDiffResult {
8
+ mismatchPercentage: number;
9
+ mismatchCount: number;
10
+ totalPixels: number;
11
+ diffImagePath?: string;
12
+ pass: boolean;
13
+ }
14
+ /**
15
+ * Simple visual diff using Playwright's built-in comparison.
16
+ * Takes two PNG buffers and returns mismatch info.
17
+ *
18
+ * For pixel-perfect comparison, we compare raw buffer bytes.
19
+ * For a production-grade solution, pixelmatch can be added as optional dep.
20
+ */
21
+ export declare function visualDiff(baseline: Buffer, current: Buffer, options?: {
22
+ threshold?: number;
23
+ outputPath?: string;
24
+ }): Promise<VisualDiffResult>;
25
+ /**
26
+ * Compare screenshots from a directory against baselines.
27
+ */
28
+ export declare function diffDirectory(baselineDir: string, currentDir: string, options?: {
29
+ threshold?: number;
30
+ }): Promise<Array<{
31
+ file: string;
32
+ result: VisualDiffResult;
33
+ }>>;
34
+ //# sourceMappingURL=visualDiff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visualDiff.d.ts","sourceRoot":"","sources":["../../src/daemon/visualDiff.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,MAAM,WAAW,gBAAgB;IAC/B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,OAAO,CAAC;CACf;AAMD;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,gBAAgB,CAAC,CAmC3B;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/B,OAAO,CAAC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAAC,CAAC,CA6B5D"}