@ytspar/sweetlink 1.10.0 → 1.11.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 +32 -23
- package/claude-skills/screenshot/SKILL.md +381 -0
- package/dist/browser/commands/outline.js.map +1 -1
- package/dist/browser/commands/screenshot.d.ts.map +1 -1
- package/dist/browser/commands/screenshot.js +21 -3
- package/dist/browser/commands/screenshot.js.map +1 -1
- package/dist/browser.js +2 -3
- package/dist/browser.js.map +1 -1
- package/dist/cdp.d.ts +1 -1
- package/dist/cdp.d.ts.map +1 -1
- package/dist/cdp.js +1 -1
- package/dist/cdp.js.map +1 -1
- package/dist/cli/sweetlink.js +390 -244
- package/dist/cli/sweetlink.js.map +1 -1
- package/dist/playwright.js.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/viewportUtils.js.map +1 -1
- package/package.json +12 -3
- package/scripts/setup-claude-context.mjs +93 -68
package/dist/cli/sweetlink.js
CHANGED
|
@@ -176,29 +176,91 @@ async function waitForServer(url, timeout = SERVER_READY_TIMEOUT) {
|
|
|
176
176
|
}
|
|
177
177
|
throw new Error(`Server not ready after ${timeout}ms: ${lastError?.message || 'Connection refused'}`);
|
|
178
178
|
}
|
|
179
|
-
async function sendCommand(command) {
|
|
179
|
+
async function sendCommand(command, timeoutMs = DEFAULT_TIMEOUT) {
|
|
180
180
|
return new Promise((resolve, reject) => {
|
|
181
181
|
const ws = new WebSocket(WS_URL);
|
|
182
|
-
const
|
|
182
|
+
const timer = setTimeout(() => {
|
|
183
183
|
ws.close();
|
|
184
184
|
reject(new Error('Command timeout - is the dev server running?'));
|
|
185
|
-
},
|
|
185
|
+
}, timeoutMs);
|
|
186
186
|
ws.on('open', () => {
|
|
187
187
|
ws.send(JSON.stringify(command));
|
|
188
188
|
});
|
|
189
189
|
ws.on('message', (data) => {
|
|
190
|
-
clearTimeout(
|
|
190
|
+
clearTimeout(timer);
|
|
191
191
|
const response = JSON.parse(data.toString());
|
|
192
192
|
ws.close();
|
|
193
193
|
resolve(response);
|
|
194
194
|
});
|
|
195
195
|
ws.on('error', (error) => {
|
|
196
|
-
clearTimeout(
|
|
196
|
+
clearTimeout(timer);
|
|
197
197
|
ws.close();
|
|
198
198
|
reject(error);
|
|
199
199
|
});
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Compare two URLs ignoring trailing slashes.
|
|
204
|
+
* Exported-style name for testability (re-implemented in tests).
|
|
205
|
+
*/
|
|
206
|
+
function urlsMatch(a, b) {
|
|
207
|
+
return a.replace(/\/+$/, '') === b.replace(/\/+$/, '');
|
|
208
|
+
}
|
|
209
|
+
const NAVIGATE_POLL_INTERVAL = 500; // ms between reconnection polls
|
|
210
|
+
const NAVIGATE_DEFAULT_TIMEOUT = 10000; // 10s default
|
|
211
|
+
/**
|
|
212
|
+
* Navigate the connected browser to a URL via WebSocket exec-js.
|
|
213
|
+
* After navigation the page reloads, dropping the WS connection.
|
|
214
|
+
* We poll with short-timeout exec-js commands until devbar reconnects.
|
|
215
|
+
*
|
|
216
|
+
* @returns true if the browser is on the target URL, false if no browser
|
|
217
|
+
* is connected or reconnection timed out.
|
|
218
|
+
*/
|
|
219
|
+
async function navigateBrowser(url, timeout = NAVIGATE_DEFAULT_TIMEOUT) {
|
|
220
|
+
// 1. Check current URL
|
|
221
|
+
try {
|
|
222
|
+
const response = await sendCommand({ type: 'exec-js', code: 'window.location.href' }, 3000);
|
|
223
|
+
if (response.success && response.data != null) {
|
|
224
|
+
const currentUrl = String(response.data.result ?? response.data);
|
|
225
|
+
if (urlsMatch(currentUrl, url)) {
|
|
226
|
+
console.log(`[Sweetlink] Browser already on ${url}`);
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// No browser connected — caller should escalate
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
// 2. Navigate
|
|
236
|
+
console.log(`[Sweetlink] Navigating browser to ${url}`);
|
|
237
|
+
try {
|
|
238
|
+
// Fire-and-forget: the page will unload, so the response may never arrive
|
|
239
|
+
await sendCommand({ type: 'exec-js', code: `window.location.href = ${JSON.stringify(url)}` }, 3000).catch(() => { });
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Expected — page unloaded before response
|
|
243
|
+
}
|
|
244
|
+
// 3. Poll for reconnection
|
|
245
|
+
const startTime = Date.now();
|
|
246
|
+
while (Date.now() - startTime < timeout) {
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, NAVIGATE_POLL_INTERVAL));
|
|
248
|
+
try {
|
|
249
|
+
const response = await sendCommand({ type: 'exec-js', code: 'window.location.href' }, 3000);
|
|
250
|
+
if (response.success) {
|
|
251
|
+
// Give devbar time to fully initialize after page load
|
|
252
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
253
|
+
console.log(`[Sweetlink] Browser reconnected on new page`);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Still reconnecting — keep polling
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
console.warn(`[Sweetlink] Browser did not reconnect within ${timeout}ms`);
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
202
264
|
const WAIT_FOR_POLL_INTERVAL = 200; // ms between DOM polls
|
|
203
265
|
const WAIT_FOR_DEFAULT_TIMEOUT = 10000; // 10s default
|
|
204
266
|
/**
|
|
@@ -230,6 +292,24 @@ async function waitForSelector(selector, timeout = WAIT_FOR_DEFAULT_TIMEOUT) {
|
|
|
230
292
|
}
|
|
231
293
|
throw new Error(`Timeout: selector "${selector}" not found after ${timeout}ms`);
|
|
232
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Run Playwright screenshot, report success, and return a ScreenshotData result.
|
|
297
|
+
* Consolidates the repeated ensureDir + screenshotViaPlaywright + reportSuccess + return pattern.
|
|
298
|
+
*/
|
|
299
|
+
async function takePlaywrightScreenshot(options, method) {
|
|
300
|
+
const outputPath = options.output || getDefaultScreenshotPath();
|
|
301
|
+
ensureDir(outputPath);
|
|
302
|
+
const result = await screenshotViaPlaywright({
|
|
303
|
+
selector: options.selector,
|
|
304
|
+
output: outputPath,
|
|
305
|
+
fullPage: options.fullPage,
|
|
306
|
+
viewport: options.viewport,
|
|
307
|
+
hover: options.hover,
|
|
308
|
+
url: options.url,
|
|
309
|
+
});
|
|
310
|
+
reportScreenshotSuccess(outputPath, result.width, result.height, method, options.selector);
|
|
311
|
+
return { path: getRelativePath(outputPath), width: result.width, height: result.height, method, selector: options.selector };
|
|
312
|
+
}
|
|
233
313
|
async function screenshot(options) {
|
|
234
314
|
// Convert --width/--height to viewport format if provided
|
|
235
315
|
if (options.width && !options.viewport) {
|
|
@@ -264,26 +344,21 @@ async function screenshot(options) {
|
|
|
264
344
|
// Check if CDP is available (unless force WS is specified)
|
|
265
345
|
// Hover requires CDP/Playwright
|
|
266
346
|
const requiresCDP = options.forceCDP || options.hover;
|
|
347
|
+
const playwrightOpts = {
|
|
348
|
+
selector: options.selector,
|
|
349
|
+
output: options.output,
|
|
350
|
+
fullPage: options.fullPage,
|
|
351
|
+
viewport: options.viewport,
|
|
352
|
+
hover: options.hover,
|
|
353
|
+
url: options.url,
|
|
354
|
+
};
|
|
267
355
|
// If we need CDP/Playwright (for hover or force-cdp), or if CDP is available, use Playwright
|
|
268
356
|
// Playwright will auto-launch if CDP is not available
|
|
269
357
|
const shouldTryPlaywright = requiresCDP || (!options.forceWS && (await detectCDP()));
|
|
270
358
|
if (shouldTryPlaywright) {
|
|
271
359
|
console.log('[Sweetlink] Using Playwright for screenshot');
|
|
272
|
-
// Determine output path - use default if not specified
|
|
273
|
-
const outputPath = options.output || getDefaultScreenshotPath();
|
|
274
|
-
ensureDir(outputPath);
|
|
275
360
|
try {
|
|
276
|
-
|
|
277
|
-
const result = await screenshotViaPlaywright({
|
|
278
|
-
selector: options.selector,
|
|
279
|
-
output: outputPath,
|
|
280
|
-
fullPage: options.fullPage,
|
|
281
|
-
viewport: options.viewport,
|
|
282
|
-
hover: options.hover,
|
|
283
|
-
url: options.url,
|
|
284
|
-
});
|
|
285
|
-
reportScreenshotSuccess(outputPath, result.width, result.height, 'Playwright (Auto-launch/CDP)', options.selector);
|
|
286
|
-
return { path: getRelativePath(outputPath), width: result.width, height: result.height, method: 'Playwright (Auto-launch/CDP)', selector: options.selector };
|
|
361
|
+
return await takePlaywrightScreenshot(playwrightOpts, 'Playwright (Auto-launch/CDP)');
|
|
287
362
|
}
|
|
288
363
|
catch (error) {
|
|
289
364
|
if (options.forceCDP) {
|
|
@@ -295,6 +370,19 @@ async function screenshot(options) {
|
|
|
295
370
|
}
|
|
296
371
|
}
|
|
297
372
|
// Fall back to WebSocket method
|
|
373
|
+
// Navigate the connected browser if --url is provided
|
|
374
|
+
if (options.url) {
|
|
375
|
+
const navigated = await navigateBrowser(options.url);
|
|
376
|
+
if (!navigated) {
|
|
377
|
+
if (options.forceWS) {
|
|
378
|
+
console.error('[Sweetlink] Could not navigate browser to', options.url);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
// Auto-escalate to Playwright (opens browser, navigates, screenshots)
|
|
382
|
+
console.log('[Sweetlink] No browser for navigation — escalating to Playwright');
|
|
383
|
+
return await takePlaywrightScreenshot(playwrightOpts, 'Playwright (auto-escalation)');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
298
386
|
console.log('[Sweetlink] Using WebSocket for screenshot');
|
|
299
387
|
const command = {
|
|
300
388
|
type: 'screenshot',
|
|
@@ -302,6 +390,7 @@ async function screenshot(options) {
|
|
|
302
390
|
options: {
|
|
303
391
|
fullPage: options.fullPage,
|
|
304
392
|
a11y: options.a11y,
|
|
393
|
+
scale: 0.5,
|
|
305
394
|
},
|
|
306
395
|
};
|
|
307
396
|
try {
|
|
@@ -311,19 +400,8 @@ async function screenshot(options) {
|
|
|
311
400
|
// This happens after dev server restart when browser page hasn't been refreshed
|
|
312
401
|
if (response.error?.includes('No browser client connected')) {
|
|
313
402
|
console.log('[Sweetlink] No browser client - auto-escalating to Playwright');
|
|
314
|
-
const outputPath = options.output || getDefaultScreenshotPath();
|
|
315
|
-
ensureDir(outputPath);
|
|
316
403
|
try {
|
|
317
|
-
|
|
318
|
-
selector: options.selector,
|
|
319
|
-
output: outputPath,
|
|
320
|
-
fullPage: options.fullPage,
|
|
321
|
-
viewport: options.viewport,
|
|
322
|
-
hover: options.hover,
|
|
323
|
-
url: options.url,
|
|
324
|
-
});
|
|
325
|
-
reportScreenshotSuccess(outputPath, result.width, result.height, 'Playwright (auto-escalation from WebSocket failure)', options.selector);
|
|
326
|
-
return { path: getRelativePath(outputPath), width: result.width, height: result.height, method: 'Playwright (auto-escalation)', selector: options.selector };
|
|
404
|
+
return await takePlaywrightScreenshot(playwrightOpts, 'Playwright (auto-escalation)');
|
|
327
405
|
}
|
|
328
406
|
catch (playwrightError) {
|
|
329
407
|
console.error('[Sweetlink] Playwright fallback also failed:', playwrightError instanceof Error ? playwrightError.message : playwrightError);
|
|
@@ -441,6 +519,38 @@ function deduplicateLogs(logs) {
|
|
|
441
519
|
return b.count - a.count;
|
|
442
520
|
});
|
|
443
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* Render console logs to stdout in human-readable text format with ANSI colors.
|
|
524
|
+
*/
|
|
525
|
+
function renderLogsAsText(logs, dedupedLogs) {
|
|
526
|
+
const LEVEL_COLORS = {
|
|
527
|
+
error: '\x1b[31m',
|
|
528
|
+
warn: '\x1b[33m',
|
|
529
|
+
info: '\x1b[36m',
|
|
530
|
+
log: '\x1b[37m',
|
|
531
|
+
};
|
|
532
|
+
const reset = '\x1b[0m';
|
|
533
|
+
console.log(`[Sweetlink] ✓ Found ${logs.length} log entries${dedupedLogs ? ` (${dedupedLogs.length} unique)` : ''}`);
|
|
534
|
+
if (logs.length === 0) {
|
|
535
|
+
console.log(' No logs found');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
console.log('\nConsole Logs:');
|
|
539
|
+
if (dedupedLogs) {
|
|
540
|
+
dedupedLogs.forEach((log) => {
|
|
541
|
+
const levelColor = LEVEL_COLORS[log.level] || '\x1b[37m';
|
|
542
|
+
const countStr = log.count > 1 ? ` (×${log.count})` : '';
|
|
543
|
+
console.log(` ${levelColor}[${log.level.toUpperCase()}]${reset}${countStr} - ${log.message}`);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
logs.forEach((log) => {
|
|
548
|
+
const levelColor = LEVEL_COLORS[log.level] || '\x1b[37m';
|
|
549
|
+
const time = new Date(log.timestamp).toLocaleTimeString();
|
|
550
|
+
console.log(` ${levelColor}[${log.level.toUpperCase()}]${reset} ${time} - ${log.message}`);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
444
554
|
async function getLogs(options) {
|
|
445
555
|
if (options.format === 'text') {
|
|
446
556
|
console.log('[Sweetlink] Getting console logs...');
|
|
@@ -504,39 +614,7 @@ async function getLogs(options) {
|
|
|
504
614
|
}
|
|
505
615
|
// Default text format
|
|
506
616
|
const displayLogs = options.dedupe ? deduplicateLogs(logs) : null;
|
|
507
|
-
|
|
508
|
-
if (logs.length > 0) {
|
|
509
|
-
console.log('\nConsole Logs:');
|
|
510
|
-
if (options.dedupe && displayLogs) {
|
|
511
|
-
displayLogs.forEach((log) => {
|
|
512
|
-
const levelColor = {
|
|
513
|
-
error: '\x1b[31m',
|
|
514
|
-
warn: '\x1b[33m',
|
|
515
|
-
info: '\x1b[36m',
|
|
516
|
-
log: '\x1b[37m',
|
|
517
|
-
}[log.level] || '\x1b[37m';
|
|
518
|
-
const reset = '\x1b[0m';
|
|
519
|
-
const countStr = log.count > 1 ? ` (×${log.count})` : '';
|
|
520
|
-
console.log(` ${levelColor}[${log.level.toUpperCase()}]${reset}${countStr} - ${log.message}`);
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
else {
|
|
524
|
-
logs.forEach((log) => {
|
|
525
|
-
const levelColor = {
|
|
526
|
-
error: '\x1b[31m',
|
|
527
|
-
warn: '\x1b[33m',
|
|
528
|
-
info: '\x1b[36m',
|
|
529
|
-
log: '\x1b[37m',
|
|
530
|
-
}[log.level] || '\x1b[37m';
|
|
531
|
-
const reset = '\x1b[0m';
|
|
532
|
-
const time = new Date(log.timestamp).toLocaleTimeString();
|
|
533
|
-
console.log(` ${levelColor}[${log.level.toUpperCase()}]${reset} ${time} - ${log.message}`);
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
console.log(' No logs found');
|
|
539
|
-
}
|
|
617
|
+
renderLogsAsText(logs, displayLogs);
|
|
540
618
|
return { total: logs.length, format: 'text', deduped: !!options.dedupe, logs: displayLogs || logs, outputPath: undefined };
|
|
541
619
|
}
|
|
542
620
|
catch (error) {
|
|
@@ -653,28 +731,29 @@ async function execJS(options) {
|
|
|
653
731
|
process.exit(1);
|
|
654
732
|
}
|
|
655
733
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
734
|
+
/**
|
|
735
|
+
* Generate JavaScript code that finds elements and clicks the one at the given index.
|
|
736
|
+
* Parameterized by the element-finding strategy: text content search or CSS selector.
|
|
737
|
+
*/
|
|
738
|
+
function generateClickCode(strategy, index) {
|
|
739
|
+
// The element-finding expression differs, but the bounds-check + click + return is shared
|
|
740
|
+
const escapedSelector = JSON.stringify(strategy.selector);
|
|
741
|
+
let findExpression;
|
|
742
|
+
let notFoundMsg;
|
|
743
|
+
if (strategy.type === 'text') {
|
|
744
|
+
const escapedText = JSON.stringify(strategy.text);
|
|
745
|
+
findExpression = `Array.from(document.querySelectorAll(${escapedSelector})).filter(el => el.textContent?.includes(${escapedText}))`;
|
|
746
|
+
notFoundMsg = `"No element found with text: " + ${escapedText}`;
|
|
661
747
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
// Escape for JSON to safely embed in JavaScript
|
|
668
|
-
const escapedText = JSON.stringify(text);
|
|
669
|
-
const escapedSelector = JSON.stringify(baseSelector);
|
|
670
|
-
description = selector ? `"${text}" within ${selector}` : `"${text}"`;
|
|
671
|
-
clickCode = `
|
|
748
|
+
else {
|
|
749
|
+
findExpression = `Array.from(document.querySelectorAll(${escapedSelector}))`;
|
|
750
|
+
notFoundMsg = `"No element found matching: " + ${escapedSelector}`;
|
|
751
|
+
}
|
|
752
|
+
return `
|
|
672
753
|
(() => {
|
|
673
|
-
const
|
|
674
|
-
const elements = Array.from(document.querySelectorAll(${escapedSelector}))
|
|
675
|
-
.filter(el => el.textContent?.includes(searchText));
|
|
754
|
+
const elements = ${findExpression};
|
|
676
755
|
if (elements.length === 0) {
|
|
677
|
-
return { success: false, error:
|
|
756
|
+
return { success: false, error: ${notFoundMsg} };
|
|
678
757
|
}
|
|
679
758
|
const target = elements[${index}];
|
|
680
759
|
if (!target) {
|
|
@@ -684,25 +763,23 @@ async function click(options) {
|
|
|
684
763
|
return { success: true, clicked: target.tagName + (target.className ? "." + target.className.split(" ")[0] : ""), found: elements.length };
|
|
685
764
|
})()
|
|
686
765
|
`;
|
|
766
|
+
}
|
|
767
|
+
async function click(options) {
|
|
768
|
+
const { selector, text, index = 0 } = options;
|
|
769
|
+
if (!selector && !text) {
|
|
770
|
+
console.error('[Sweetlink] Error: Either --selector or --text is required');
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
let clickCode;
|
|
774
|
+
let description;
|
|
775
|
+
if (text) {
|
|
776
|
+
const baseSelector = selector || '*';
|
|
777
|
+
description = selector ? `"${text}" within ${selector}` : `"${text}"`;
|
|
778
|
+
clickCode = generateClickCode({ type: 'text', text, selector: baseSelector }, index);
|
|
687
779
|
}
|
|
688
780
|
else {
|
|
689
|
-
// Find element by selector
|
|
690
|
-
const escapedSelector = JSON.stringify(selector);
|
|
691
781
|
description = `${selector}${index > 0 ? ` [${index}]` : ''}`;
|
|
692
|
-
clickCode =
|
|
693
|
-
(() => {
|
|
694
|
-
const elements = document.querySelectorAll(${escapedSelector});
|
|
695
|
-
if (elements.length === 0) {
|
|
696
|
-
return { success: false, error: "No element found matching: " + ${escapedSelector} };
|
|
697
|
-
}
|
|
698
|
-
const target = elements[${index}];
|
|
699
|
-
if (!target) {
|
|
700
|
-
return { success: false, error: "Index ${index} out of bounds, found " + elements.length + " elements" };
|
|
701
|
-
}
|
|
702
|
-
target.click();
|
|
703
|
-
return { success: true, clicked: target.tagName + (target.className ? "." + target.className.split(" ")[0] : ""), found: elements.length };
|
|
704
|
-
})()
|
|
705
|
-
`;
|
|
782
|
+
clickCode = generateClickCode({ type: 'selector', selector: selector }, index);
|
|
706
783
|
}
|
|
707
784
|
console.log(`[Sweetlink] Clicking: ${description}`);
|
|
708
785
|
// Debug: log the generated code
|
|
@@ -977,19 +1054,21 @@ async function findLsofPath() {
|
|
|
977
1054
|
* Find and kill process using a specific port (fallback method)
|
|
978
1055
|
*/
|
|
979
1056
|
async function killProcessOnPort(port) {
|
|
980
|
-
const {
|
|
1057
|
+
const { execFile } = await import('child_process');
|
|
981
1058
|
const { promisify } = await import('util');
|
|
982
|
-
const
|
|
1059
|
+
const execFileAsync = promisify(execFile);
|
|
983
1060
|
const lsofPath = await findLsofPath();
|
|
984
1061
|
try {
|
|
985
|
-
const { stdout } = await
|
|
1062
|
+
const { stdout } = await execFileAsync(lsofPath, ['-ti', `:${port}`]);
|
|
986
1063
|
const pids = stdout.trim().split('\n').filter(Boolean);
|
|
987
1064
|
if (pids.length === 0) {
|
|
988
1065
|
return false;
|
|
989
1066
|
}
|
|
990
1067
|
for (const pid of pids) {
|
|
1068
|
+
if (!/^\d+$/.test(pid))
|
|
1069
|
+
continue;
|
|
991
1070
|
try {
|
|
992
|
-
await
|
|
1071
|
+
await execFileAsync('/bin/kill', ['-9', pid]);
|
|
993
1072
|
console.log(` Killed process ${pid} on port ${port}`);
|
|
994
1073
|
}
|
|
995
1074
|
catch {
|
|
@@ -1265,59 +1344,65 @@ async function getVitals(options) {
|
|
|
1265
1344
|
process.exit(1);
|
|
1266
1345
|
}
|
|
1267
1346
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
Usage:
|
|
1273
|
-
pnpm sweetlink <command> [options]
|
|
1274
|
-
|
|
1275
|
-
Commands:
|
|
1276
|
-
|
|
1277
|
-
screenshot [options]
|
|
1347
|
+
// Per-command help text, keyed by canonical command name
|
|
1348
|
+
const COMMAND_HELP = {
|
|
1349
|
+
screenshot: ` screenshot [options]
|
|
1278
1350
|
Take a screenshot of the current page or element
|
|
1279
1351
|
|
|
1280
|
-
TWO-TIER STRATEGY
|
|
1281
|
-
Tier 1 (Default): html2canvas WebSocket
|
|
1282
|
-
|
|
1352
|
+
TWO-TIER STRATEGY:
|
|
1353
|
+
Tier 1 (Default): html2canvas via WebSocket
|
|
1354
|
+
- Captures viewport at 0.5x scale (PNG, typically 50-300KB)
|
|
1355
|
+
- Zero setup, works with any devbar-enabled page
|
|
1356
|
+
- Use --full-page to capture the entire scrollable page (larger files)
|
|
1357
|
+
|
|
1358
|
+
Tier 2 (Escalation): Playwright/CDP
|
|
1359
|
+
- Pixel-perfect native Chrome rendering
|
|
1360
|
+
- Respects --viewport/--width/--height for custom dimensions
|
|
1361
|
+
- Use --force-cdp to force this method, or it auto-activates for --hover
|
|
1362
|
+
- Auto-launches headless browser if no browser connected
|
|
1283
1363
|
|
|
1284
1364
|
Options:
|
|
1285
|
-
--url <url>
|
|
1365
|
+
--url <url> Navigate browser to URL before capturing (default: http://localhost:3000)
|
|
1366
|
+
Navigates connected browser via WS; if none connected, opens one via Playwright
|
|
1286
1367
|
--selector <css-selector> CSS selector of element to screenshot
|
|
1287
1368
|
--output <path> Output file path (default: screenshot-<timestamp>.png)
|
|
1288
|
-
--full-page Capture full page (
|
|
1289
|
-
--width <pixels> Viewport width (e.g., 768 for tablet, 375 for mobile)
|
|
1290
|
-
--height <pixels> Viewport height (default: width * 1.5)
|
|
1291
|
-
--viewport <preset|WxH> Viewport preset (mobile, tablet, desktop) or WIDTHxHEIGHT
|
|
1292
|
-
--force-cdp Force CDP method
|
|
1293
|
-
--force-ws Force WebSocket method (default)
|
|
1369
|
+
--full-page Capture full scrollable page (default: viewport only)
|
|
1370
|
+
--width <pixels> Viewport width for Playwright (e.g., 768 for tablet, 375 for mobile)
|
|
1371
|
+
--height <pixels> Viewport height for Playwright (default: width * 1.5)
|
|
1372
|
+
--viewport <preset|WxH> Viewport preset for Playwright (mobile, tablet, desktop) or WIDTHxHEIGHT
|
|
1373
|
+
--force-cdp Force Playwright/CDP method
|
|
1374
|
+
--force-ws Force WebSocket/html2canvas method (default)
|
|
1294
1375
|
--no-wait Skip server readiness check (use if server is already running)
|
|
1295
1376
|
--wait-timeout <ms> Max time to wait for server (default: 30000ms)
|
|
1296
1377
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
pnpm sweetlink screenshot --width 768 # Tablet viewport (768x1152)
|
|
1302
|
-
pnpm sweetlink screenshot --width 375 --height 667 # iPhone SE viewport
|
|
1303
|
-
pnpm sweetlink screenshot --viewport tablet # Preset: 768x1024
|
|
1304
|
-
pnpm sweetlink screenshot --force-cdp --full-page # Tier 2 (escalation)
|
|
1378
|
+
Size comparison:
|
|
1379
|
+
Tier 1 (WS, viewport): ~50-300KB PNG at 0.5x scale
|
|
1380
|
+
Tier 1 (WS, --full-page): ~1-5MB PNG (entire page)
|
|
1381
|
+
Tier 2 (Playwright): ~200-800KB PNG at native resolution
|
|
1305
1382
|
|
|
1306
|
-
|
|
1383
|
+
Examples:
|
|
1384
|
+
pnpm sweetlink screenshot # Viewport screenshot (small)
|
|
1385
|
+
pnpm sweetlink screenshot --url "http://localhost:3000/company/foo" # Navigate then capture
|
|
1386
|
+
pnpm sweetlink screenshot --selector ".company-card" # Element screenshot
|
|
1387
|
+
pnpm sweetlink screenshot --full-page # Full scrollable page
|
|
1388
|
+
pnpm sweetlink screenshot --force-cdp --viewport tablet # Playwright at 768x1024
|
|
1389
|
+
pnpm sweetlink screenshot --force-cdp --width 375 --height 667 # Playwright at iPhone SE`,
|
|
1390
|
+
query: ` query --selector <css-selector> [options]
|
|
1307
1391
|
Query DOM elements and return data
|
|
1308
1392
|
|
|
1309
1393
|
Options:
|
|
1310
1394
|
--selector <css-selector> CSS selector to query (required)
|
|
1311
1395
|
--property <name> Property to get from elements
|
|
1396
|
+
--url <url> Navigate browser to URL before querying
|
|
1312
1397
|
--wait-for <css-selector> Wait for selector to exist before querying (handles hydration)
|
|
1313
1398
|
--wait-timeout <ms> Max wait time for --wait-for (default: 10000ms)
|
|
1314
1399
|
|
|
1315
1400
|
Examples:
|
|
1316
1401
|
pnpm sweetlink query --selector "h1"
|
|
1317
1402
|
pnpm sweetlink query --selector ".card" --property "offsetWidth"
|
|
1318
|
-
pnpm sweetlink query --selector "
|
|
1319
|
-
|
|
1320
|
-
logs [options]
|
|
1403
|
+
pnpm sweetlink query --selector "h1" --url "http://localhost:3000/about"
|
|
1404
|
+
pnpm sweetlink query --selector "img" --wait-for "img[src*='hero']"`,
|
|
1405
|
+
logs: ` logs [options]
|
|
1321
1406
|
Get console logs from the browser
|
|
1322
1407
|
|
|
1323
1408
|
Options:
|
|
@@ -1338,9 +1423,8 @@ Commands:
|
|
|
1338
1423
|
pnpm sweetlink logs --dedupe # Remove duplicates
|
|
1339
1424
|
pnpm sweetlink logs --format json # Full JSON output
|
|
1340
1425
|
pnpm sweetlink logs --format summary # LLM-optimized summary
|
|
1341
|
-
pnpm sweetlink logs --format json --dedupe # JSON with deduplication
|
|
1342
|
-
|
|
1343
|
-
exec --code <javascript>
|
|
1426
|
+
pnpm sweetlink logs --format json --dedupe # JSON with deduplication`,
|
|
1427
|
+
exec: ` exec --code <javascript>
|
|
1344
1428
|
Execute JavaScript in the browser context
|
|
1345
1429
|
|
|
1346
1430
|
Code is evaluated as an expression. Bare \`return\` statements are auto-wrapped in an IIFE.
|
|
@@ -1348,17 +1432,18 @@ Commands:
|
|
|
1348
1432
|
|
|
1349
1433
|
Options:
|
|
1350
1434
|
--code <javascript> JavaScript code to execute (required)
|
|
1435
|
+
--url <url> Navigate browser to URL before executing
|
|
1351
1436
|
--wait-for <css-selector> Wait for selector to exist before executing (handles hydration)
|
|
1352
1437
|
--wait-timeout <ms> Max wait time for --wait-for (default: 10000ms)
|
|
1353
1438
|
|
|
1354
1439
|
Examples:
|
|
1355
1440
|
pnpm sweetlink exec --code "document.title"
|
|
1356
1441
|
pnpm sweetlink exec --code "document.querySelectorAll('.card').length"
|
|
1442
|
+
pnpm sweetlink exec --code "document.title" --url "http://localhost:3000/about"
|
|
1357
1443
|
pnpm sweetlink exec --code "const x = 1 + 2; return x;"
|
|
1358
1444
|
pnpm sweetlink exec --code "fetch('/api/health').then(r => r.status)"
|
|
1359
|
-
pnpm sweetlink exec --code "document.querySelectorAll('img').length" --wait-for "img[src*='hero']"
|
|
1360
|
-
|
|
1361
|
-
click [options]
|
|
1445
|
+
pnpm sweetlink exec --code "document.querySelectorAll('img').length" --wait-for "img[src*='hero']"`,
|
|
1446
|
+
click: ` click [options]
|
|
1362
1447
|
Click an element in the browser
|
|
1363
1448
|
|
|
1364
1449
|
Options:
|
|
@@ -1373,9 +1458,8 @@ Commands:
|
|
|
1373
1458
|
pnpm sweetlink click --selector "button.submit"
|
|
1374
1459
|
pnpm sweetlink click --text "Submit"
|
|
1375
1460
|
pnpm sweetlink click --selector "th" --text "Rank"
|
|
1376
|
-
pnpm sweetlink click --selector ".tab" --index 2
|
|
1377
|
-
|
|
1378
|
-
network [options] (requires CDP)
|
|
1461
|
+
pnpm sweetlink click --selector ".tab" --index 2`,
|
|
1462
|
+
network: ` network [options] (requires CDP)
|
|
1379
1463
|
Get network requests from the browser
|
|
1380
1464
|
|
|
1381
1465
|
Options:
|
|
@@ -1383,9 +1467,8 @@ Commands:
|
|
|
1383
1467
|
|
|
1384
1468
|
Examples:
|
|
1385
1469
|
pnpm sweetlink network
|
|
1386
|
-
pnpm sweetlink network --filter "/api/"
|
|
1387
|
-
|
|
1388
|
-
refresh [options]
|
|
1470
|
+
pnpm sweetlink network --filter "/api/"`,
|
|
1471
|
+
refresh: ` refresh [options]
|
|
1389
1472
|
Refresh the browser page
|
|
1390
1473
|
|
|
1391
1474
|
Options:
|
|
@@ -1393,9 +1476,8 @@ Commands:
|
|
|
1393
1476
|
|
|
1394
1477
|
Examples:
|
|
1395
1478
|
pnpm sweetlink refresh
|
|
1396
|
-
pnpm sweetlink refresh --hard
|
|
1397
|
-
|
|
1398
|
-
schema [options]
|
|
1479
|
+
pnpm sweetlink refresh --hard`,
|
|
1480
|
+
schema: ` schema [options]
|
|
1399
1481
|
Extract page schema (JSON-LD, Open Graph, Twitter, meta tags, microdata)
|
|
1400
1482
|
|
|
1401
1483
|
Options:
|
|
@@ -1405,9 +1487,8 @@ Commands:
|
|
|
1405
1487
|
Examples:
|
|
1406
1488
|
pnpm sweetlink schema
|
|
1407
1489
|
pnpm sweetlink schema --format json
|
|
1408
|
-
pnpm sweetlink schema --output .tmp/schema.json --format json
|
|
1409
|
-
|
|
1410
|
-
outline [options]
|
|
1490
|
+
pnpm sweetlink schema --output .tmp/schema.json --format json`,
|
|
1491
|
+
outline: ` outline [options]
|
|
1411
1492
|
Extract document outline (headings, sections, landmarks)
|
|
1412
1493
|
|
|
1413
1494
|
Options:
|
|
@@ -1417,9 +1498,8 @@ Commands:
|
|
|
1417
1498
|
Examples:
|
|
1418
1499
|
pnpm sweetlink outline
|
|
1419
1500
|
pnpm sweetlink outline --format json
|
|
1420
|
-
pnpm sweetlink outline --output .tmp/outline.md
|
|
1421
|
-
|
|
1422
|
-
a11y [options]
|
|
1501
|
+
pnpm sweetlink outline --output .tmp/outline.md`,
|
|
1502
|
+
a11y: ` a11y [options]
|
|
1423
1503
|
Run accessibility audit (requires axe-core via devbar)
|
|
1424
1504
|
|
|
1425
1505
|
Options:
|
|
@@ -1429,9 +1509,8 @@ Commands:
|
|
|
1429
1509
|
Examples:
|
|
1430
1510
|
pnpm sweetlink a11y
|
|
1431
1511
|
pnpm sweetlink a11y --format json
|
|
1432
|
-
pnpm sweetlink a11y --output .tmp/a11y-report.json --format json
|
|
1433
|
-
|
|
1434
|
-
vitals [options]
|
|
1512
|
+
pnpm sweetlink a11y --output .tmp/a11y-report.json --format json`,
|
|
1513
|
+
vitals: ` vitals [options]
|
|
1435
1514
|
Collect Core Web Vitals (FCP, LCP, CLS, INP, page size)
|
|
1436
1515
|
|
|
1437
1516
|
Options:
|
|
@@ -1439,9 +1518,8 @@ Commands:
|
|
|
1439
1518
|
|
|
1440
1519
|
Examples:
|
|
1441
1520
|
pnpm sweetlink vitals
|
|
1442
|
-
pnpm sweetlink vitals --format json
|
|
1443
|
-
|
|
1444
|
-
ruler [options]
|
|
1521
|
+
pnpm sweetlink vitals --format json`,
|
|
1522
|
+
ruler: ` ruler [options]
|
|
1445
1523
|
Measure elements and inject visual overlay for alignment verification.
|
|
1446
1524
|
Shows bounding boxes, center lines, dimensions, and alignment offsets.
|
|
1447
1525
|
|
|
@@ -1466,9 +1544,8 @@ Commands:
|
|
|
1466
1544
|
pnpm sweetlink ruler --selector "article h2" --selector "article header > div:first-child"
|
|
1467
1545
|
pnpm sweetlink ruler --preset card-header --output .tmp/ruler.png
|
|
1468
1546
|
pnpm sweetlink ruler --preset card-header --format json
|
|
1469
|
-
pnpm sweetlink ruler --selector ".logo" --selector ".nav-item" --show-position
|
|
1470
|
-
|
|
1471
|
-
wait [options]
|
|
1547
|
+
pnpm sweetlink ruler --selector ".logo" --selector ".nav-item" --show-position`,
|
|
1548
|
+
wait: ` wait [options]
|
|
1472
1549
|
Wait for server to be ready (blocks until available or timeout)
|
|
1473
1550
|
Eliminates need for external sleep commands in scripts.
|
|
1474
1551
|
|
|
@@ -1479,9 +1556,8 @@ Commands:
|
|
|
1479
1556
|
Examples:
|
|
1480
1557
|
pnpm sweetlink wait
|
|
1481
1558
|
pnpm sweetlink wait --url "http://localhost:3000"
|
|
1482
|
-
pnpm sweetlink wait --timeout 60000
|
|
1483
|
-
|
|
1484
|
-
status [options]
|
|
1559
|
+
pnpm sweetlink wait --timeout 60000`,
|
|
1560
|
+
status: ` status [options]
|
|
1485
1561
|
Quick server status check (non-blocking, instant)
|
|
1486
1562
|
|
|
1487
1563
|
Options:
|
|
@@ -1489,14 +1565,14 @@ Commands:
|
|
|
1489
1565
|
|
|
1490
1566
|
Examples:
|
|
1491
1567
|
pnpm sweetlink status
|
|
1492
|
-
pnpm sweetlink status --url "http://localhost:8080"
|
|
1493
|
-
|
|
1494
|
-
cleanup [options]
|
|
1568
|
+
pnpm sweetlink status --url "http://localhost:8080"`,
|
|
1569
|
+
cleanup: ` cleanup [options]
|
|
1495
1570
|
Find and close stale Sweetlink servers that weren't properly shut down.
|
|
1496
1571
|
Useful when ports are stuck after crashes or forced process kills.
|
|
1497
1572
|
|
|
1498
1573
|
Options:
|
|
1499
1574
|
--force Force kill processes if graceful shutdown fails
|
|
1575
|
+
--verbose Show detailed scan progress
|
|
1500
1576
|
|
|
1501
1577
|
What it does:
|
|
1502
1578
|
1. Scans common Sweetlink port ranges (9223-9233, 11396-11406, etc.)
|
|
@@ -1506,8 +1582,23 @@ Commands:
|
|
|
1506
1582
|
|
|
1507
1583
|
Examples:
|
|
1508
1584
|
pnpm sweetlink cleanup # Graceful shutdown
|
|
1509
|
-
pnpm sweetlink cleanup --force # Force kill if needed
|
|
1585
|
+
pnpm sweetlink cleanup --force # Force kill if needed`,
|
|
1586
|
+
setup: ` setup
|
|
1587
|
+
Install Claude Code integration (screenshot skill and context files).
|
|
1588
|
+
Creates symlinks in your .claude/ directory so Claude can use the /screenshot
|
|
1589
|
+
skill and sweetlink agent guide automatically.
|
|
1510
1590
|
|
|
1591
|
+
Re-run after upgrading sweetlink to pick up any skill updates.
|
|
1592
|
+
|
|
1593
|
+
Examples:
|
|
1594
|
+
pnpm sweetlink setup`,
|
|
1595
|
+
};
|
|
1596
|
+
// Aliases that map to canonical command names
|
|
1597
|
+
const COMMAND_ALIASES = {
|
|
1598
|
+
measure: 'ruler',
|
|
1599
|
+
accessibility: 'a11y',
|
|
1600
|
+
};
|
|
1601
|
+
const GLOBAL_HELP = `
|
|
1511
1602
|
Global Flags:
|
|
1512
1603
|
--json Output structured JSON (envelope with ok, command, data, duration)
|
|
1513
1604
|
--output-schema Print TypeScript types for --json output, then exit
|
|
@@ -1528,10 +1619,33 @@ Environment Variables:
|
|
|
1528
1619
|
CHROME_CDP_PORT Chrome DevTools Protocol port (default: 9222)
|
|
1529
1620
|
|
|
1530
1621
|
Documentation:
|
|
1531
|
-
Agent
|
|
1532
|
-
|
|
1533
|
-
|
|
1622
|
+
Agent Guide: .claude/context/sweetlink-agent-guide.md`;
|
|
1623
|
+
function showHelp() {
|
|
1624
|
+
console.log(`
|
|
1625
|
+
Sweetlink CLI - Autonomous Development Bridge
|
|
1626
|
+
|
|
1627
|
+
Usage:
|
|
1628
|
+
pnpm sweetlink <command> [options]
|
|
1629
|
+
|
|
1630
|
+
Commands:
|
|
1534
1631
|
`);
|
|
1632
|
+
for (const help of Object.values(COMMAND_HELP)) {
|
|
1633
|
+
console.log(help);
|
|
1634
|
+
console.log('');
|
|
1635
|
+
}
|
|
1636
|
+
console.log(GLOBAL_HELP);
|
|
1637
|
+
}
|
|
1638
|
+
function showCommandHelp(command) {
|
|
1639
|
+
const canonical = COMMAND_ALIASES[command] || command;
|
|
1640
|
+
const help = COMMAND_HELP[canonical];
|
|
1641
|
+
if (!help) {
|
|
1642
|
+
console.error(`[Sweetlink] Unknown command: ${command}`);
|
|
1643
|
+
console.error(`Run "pnpm sweetlink --help" to see all available commands.`);
|
|
1644
|
+
process.exit(1);
|
|
1645
|
+
}
|
|
1646
|
+
console.log(`\nSweetlink CLI - ${canonical}\n`);
|
|
1647
|
+
console.log(help);
|
|
1648
|
+
console.log(GLOBAL_HELP);
|
|
1535
1649
|
}
|
|
1536
1650
|
// CLI argument parsing
|
|
1537
1651
|
const args = process.argv.slice(2);
|
|
@@ -1549,6 +1663,11 @@ function getArg(flag) {
|
|
|
1549
1663
|
function hasFlag(flag) {
|
|
1550
1664
|
return args.includes(flag);
|
|
1551
1665
|
}
|
|
1666
|
+
// Per-command --help: `pnpm sweetlink screenshot --help`
|
|
1667
|
+
if (hasFlag('--help') || hasFlag('-h')) {
|
|
1668
|
+
showCommandHelp(commandType);
|
|
1669
|
+
process.exit(0);
|
|
1670
|
+
}
|
|
1552
1671
|
// Handle --output-schema before main switch
|
|
1553
1672
|
if (hasFlag('--output-schema')) {
|
|
1554
1673
|
// If commandType is a known command, print just that schema; otherwise print all
|
|
@@ -1560,31 +1679,82 @@ if (hasFlag('--output-schema')) {
|
|
|
1560
1679
|
process.exit(0);
|
|
1561
1680
|
}
|
|
1562
1681
|
const jsonMode = hasFlag('--json');
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
}
|
|
1570
|
-
// Capture the last console.error message for error envelopes
|
|
1682
|
+
/**
|
|
1683
|
+
* Set up JSON mode: suppress console.log/warn, capture errors, and intercept process.exit
|
|
1684
|
+
* to emit structured JSON envelopes on failure.
|
|
1685
|
+
*/
|
|
1686
|
+
function setupJsonMode(command, startTime) {
|
|
1687
|
+
console.log = () => { };
|
|
1688
|
+
console.warn = () => { };
|
|
1571
1689
|
let lastErrorMsg = '';
|
|
1572
1690
|
const origError = console.error;
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
};
|
|
1578
|
-
}
|
|
1579
|
-
// Intercept process.exit in json mode to emit error envelope
|
|
1691
|
+
console.error = (...errorArgs) => {
|
|
1692
|
+
lastErrorMsg = errorArgs.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
|
|
1693
|
+
origError(...errorArgs);
|
|
1694
|
+
};
|
|
1580
1695
|
const origExit = process.exit;
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1696
|
+
process.exit = ((code) => {
|
|
1697
|
+
if (code && code !== 0) {
|
|
1698
|
+
emitJson({ ok: false, command, data: null, error: lastErrorMsg || `Process exited with code ${code}`, duration: Date.now() - startTime });
|
|
1699
|
+
}
|
|
1700
|
+
origExit(code);
|
|
1701
|
+
});
|
|
1702
|
+
return { origExit, getLastError: () => lastErrorMsg };
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Handle the `wait` command: wait for a server to be ready.
|
|
1706
|
+
*/
|
|
1707
|
+
async function handleWaitCommand() {
|
|
1708
|
+
const waitUrl = getArg('--url') || 'http://localhost:3000';
|
|
1709
|
+
const waitTimeout = getArg('--timeout')
|
|
1710
|
+
? parseInt(getArg('--timeout'), 10)
|
|
1711
|
+
: SERVER_READY_TIMEOUT;
|
|
1712
|
+
const waitStart = Date.now();
|
|
1713
|
+
try {
|
|
1714
|
+
await waitForServer(waitUrl, waitTimeout);
|
|
1715
|
+
console.log('[Sweetlink] ✓ Server is ready');
|
|
1716
|
+
return { url: waitUrl, ready: true, elapsed: Date.now() - waitStart };
|
|
1717
|
+
}
|
|
1718
|
+
catch (error) {
|
|
1719
|
+
console.error('[Sweetlink] ✗ Server not available:', error instanceof Error ? error.message : error);
|
|
1720
|
+
process.exit(1);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Handle the `status` command: quick non-blocking server health check.
|
|
1725
|
+
*/
|
|
1726
|
+
async function handleStatusCommand() {
|
|
1727
|
+
const statusUrl = getArg('--url') || 'http://localhost:3000';
|
|
1728
|
+
try {
|
|
1729
|
+
const parsedUrl = new URL(statusUrl);
|
|
1730
|
+
const healthCheckUrl = `${parsedUrl.protocol}//${parsedUrl.host}`;
|
|
1731
|
+
const controller = new AbortController();
|
|
1732
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
1733
|
+
const response = await fetch(healthCheckUrl, {
|
|
1734
|
+
method: 'HEAD',
|
|
1735
|
+
signal: controller.signal,
|
|
1587
1736
|
});
|
|
1737
|
+
clearTimeout(timeoutId);
|
|
1738
|
+
if (response.ok || response.status === 304) {
|
|
1739
|
+
console.log(`[Sweetlink] ✓ Server at ${healthCheckUrl} is running`);
|
|
1740
|
+
return { url: statusUrl, running: true, statusCode: response.status };
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
console.log(`[Sweetlink] ⚠ Server responded with status ${response.status}`);
|
|
1744
|
+
process.exit(1);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
catch {
|
|
1748
|
+
console.log(`[Sweetlink] ✗ Server at ${statusUrl} is not responding`);
|
|
1749
|
+
process.exit(1);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
(async () => {
|
|
1753
|
+
const startTime = Date.now();
|
|
1754
|
+
let origExit = process.exit;
|
|
1755
|
+
if (jsonMode) {
|
|
1756
|
+
const jsonSetup = setupJsonMode(commandType, startTime);
|
|
1757
|
+
origExit = jsonSetup.origExit;
|
|
1588
1758
|
}
|
|
1589
1759
|
try {
|
|
1590
1760
|
let result;
|
|
@@ -1614,8 +1784,12 @@ const jsonMode = hasFlag('--json');
|
|
|
1614
1784
|
console.error('[Sweetlink] Error: --selector is required for query command');
|
|
1615
1785
|
process.exit(1);
|
|
1616
1786
|
}
|
|
1617
|
-
if (
|
|
1618
|
-
|
|
1787
|
+
if (getArg('--url')) {
|
|
1788
|
+
const navigated = await navigateBrowser(getArg('--url'));
|
|
1789
|
+
if (!navigated) {
|
|
1790
|
+
console.error('[Sweetlink] Could not navigate browser to', getArg('--url'));
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1619
1793
|
}
|
|
1620
1794
|
result = await queryDOM({
|
|
1621
1795
|
selector,
|
|
@@ -1641,8 +1815,12 @@ const jsonMode = hasFlag('--json');
|
|
|
1641
1815
|
console.error('[Sweetlink] Error: --code is required for exec command');
|
|
1642
1816
|
process.exit(1);
|
|
1643
1817
|
}
|
|
1644
|
-
if (
|
|
1645
|
-
|
|
1818
|
+
if (getArg('--url')) {
|
|
1819
|
+
const navigated = await navigateBrowser(getArg('--url'));
|
|
1820
|
+
if (!navigated) {
|
|
1821
|
+
console.error('[Sweetlink] Could not navigate browser to', getArg('--url'));
|
|
1822
|
+
process.exit(1);
|
|
1823
|
+
}
|
|
1646
1824
|
}
|
|
1647
1825
|
result = await execJS({
|
|
1648
1826
|
code,
|
|
@@ -1691,52 +1869,12 @@ const jsonMode = hasFlag('--json');
|
|
|
1691
1869
|
});
|
|
1692
1870
|
break;
|
|
1693
1871
|
}
|
|
1694
|
-
case 'wait':
|
|
1695
|
-
|
|
1696
|
-
const waitUrl = getArg('--url') || 'http://localhost:3000';
|
|
1697
|
-
const waitTimeout = getArg('--timeout')
|
|
1698
|
-
? parseInt(getArg('--timeout'), 10)
|
|
1699
|
-
: SERVER_READY_TIMEOUT;
|
|
1700
|
-
const waitStart = Date.now();
|
|
1701
|
-
try {
|
|
1702
|
-
await waitForServer(waitUrl, waitTimeout);
|
|
1703
|
-
console.log('[Sweetlink] ✓ Server is ready');
|
|
1704
|
-
result = { url: waitUrl, ready: true, elapsed: Date.now() - waitStart };
|
|
1705
|
-
}
|
|
1706
|
-
catch (error) {
|
|
1707
|
-
console.error('[Sweetlink] ✗ Server not available:', error instanceof Error ? error.message : error);
|
|
1708
|
-
process.exit(1);
|
|
1709
|
-
}
|
|
1872
|
+
case 'wait':
|
|
1873
|
+
result = await handleWaitCommand();
|
|
1710
1874
|
break;
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
// Quick server status check (non-blocking)
|
|
1714
|
-
const statusUrl = getArg('--url') || 'http://localhost:3000';
|
|
1715
|
-
try {
|
|
1716
|
-
const parsedUrl = new URL(statusUrl);
|
|
1717
|
-
const healthCheckUrl = `${parsedUrl.protocol}//${parsedUrl.host}`;
|
|
1718
|
-
const controller = new AbortController();
|
|
1719
|
-
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
1720
|
-
const response = await fetch(healthCheckUrl, {
|
|
1721
|
-
method: 'HEAD',
|
|
1722
|
-
signal: controller.signal,
|
|
1723
|
-
});
|
|
1724
|
-
clearTimeout(timeoutId);
|
|
1725
|
-
if (response.ok || response.status === 304) {
|
|
1726
|
-
console.log(`[Sweetlink] ✓ Server at ${healthCheckUrl} is running`);
|
|
1727
|
-
result = { url: statusUrl, running: true, statusCode: response.status };
|
|
1728
|
-
}
|
|
1729
|
-
else {
|
|
1730
|
-
console.log(`[Sweetlink] ⚠ Server responded with status ${response.status}`);
|
|
1731
|
-
process.exit(1);
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
catch {
|
|
1735
|
-
console.log(`[Sweetlink] ✗ Server at ${statusUrl} is not responding`);
|
|
1736
|
-
process.exit(1);
|
|
1737
|
-
}
|
|
1875
|
+
case 'status':
|
|
1876
|
+
result = await handleStatusCommand();
|
|
1738
1877
|
break;
|
|
1739
|
-
}
|
|
1740
1878
|
case 'schema':
|
|
1741
1879
|
result = await getSchema({
|
|
1742
1880
|
format: getArg('--format'),
|
|
@@ -1767,6 +1905,14 @@ const jsonMode = hasFlag('--json');
|
|
|
1767
1905
|
verbose: hasFlag('--verbose'),
|
|
1768
1906
|
});
|
|
1769
1907
|
break;
|
|
1908
|
+
case 'setup': {
|
|
1909
|
+
// Run the setup script to symlink Claude context and skills
|
|
1910
|
+
const { execFileSync } = await import('child_process');
|
|
1911
|
+
const scriptDir = path.dirname(import.meta.url.replace('file://', ''));
|
|
1912
|
+
const setupScript = path.resolve(scriptDir, '..', '..', 'scripts', 'setup-claude-context.mjs');
|
|
1913
|
+
execFileSync('node', [setupScript], { stdio: 'inherit' });
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1770
1916
|
default:
|
|
1771
1917
|
console.error(`[Sweetlink] Unknown command: ${commandType}`);
|
|
1772
1918
|
console.log('Run "pnpm sweetlink --help" for usage information');
|