glance-cli 0.13.0 → 0.13.1
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/CHANGELOG.md +8 -0
- package/dist/cli.js +136 -1065
- package/package.json +3 -2
- package/src/cli/commands.ts +832 -0
- package/src/cli/config.ts +24 -0
- package/src/cli/display.ts +269 -0
- package/src/cli/errors.ts +31 -0
- package/src/cli/index.ts +237 -0
- package/src/cli/logger.ts +43 -0
- package/src/cli/types.ts +114 -0
- package/src/cli/utils.ts +239 -0
- package/src/cli/validators.ts +176 -0
- package/src/cli.ts +17 -0
- package/src/core/compat.ts +96 -0
- package/src/core/extractor.ts +532 -0
- package/src/core/fetcher.ts +592 -0
- package/src/core/formatter.ts +742 -0
- package/src/core/language-detector.ts +382 -0
- package/src/core/screenshot.ts +444 -0
- package/src/core/service-detector.ts +411 -0
- package/src/core/summarizer.ts +656 -0
- package/src/core/text-cleaner.ts +150 -0
- package/src/core/voice.ts +708 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production-Grade Screenshot Utility
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Timeout protection (prevents hanging)
|
|
6
|
+
* - Retry logic for transient failures
|
|
7
|
+
* - Better error messages with actionable hints
|
|
8
|
+
* - Resource cleanup (prevents memory leaks)
|
|
9
|
+
* - Configurable viewport and quality
|
|
10
|
+
* - Support for different formats (PNG, JPEG, WebP)
|
|
11
|
+
* - Page load strategies
|
|
12
|
+
* - Error categorization
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Puppeteer types for when we dynamically import
|
|
16
|
+
type Browser = import("puppeteer").Browser;
|
|
17
|
+
type Page = import("puppeteer").Page;
|
|
18
|
+
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
|
|
21
|
+
// === Configuration ===
|
|
22
|
+
const SCREENSHOT_CONFIG = {
|
|
23
|
+
DEFAULT_TIMEOUT: 30000, // 30s total timeout
|
|
24
|
+
PAGE_LOAD_TIMEOUT: 20000, // 20s for page load
|
|
25
|
+
VIEWPORT_WIDTH: 1920,
|
|
26
|
+
VIEWPORT_HEIGHT: 1080,
|
|
27
|
+
DEFAULT_FORMAT: "png" as const,
|
|
28
|
+
JPEG_QUALITY: 90,
|
|
29
|
+
MAX_RETRIES: 2,
|
|
30
|
+
RETRY_DELAY: 1000,
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
// === Custom Error Types ===
|
|
34
|
+
class ScreenshotError extends Error {
|
|
35
|
+
constructor(
|
|
36
|
+
message: string,
|
|
37
|
+
public code: string,
|
|
38
|
+
public userMessage: string,
|
|
39
|
+
public hint?: string,
|
|
40
|
+
) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "ScreenshotError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// === Screenshot Options ===
|
|
47
|
+
export interface ScreenshotOptions {
|
|
48
|
+
/** Output file path (determines format from extension) */
|
|
49
|
+
filePath?: string;
|
|
50
|
+
/** Full page screenshot (default: true) */
|
|
51
|
+
fullPage?: boolean;
|
|
52
|
+
/** Viewport width (default: 1920) */
|
|
53
|
+
width?: number;
|
|
54
|
+
/** Viewport height (default: 1080) */
|
|
55
|
+
height?: number;
|
|
56
|
+
/** JPEG quality 0-100 (default: 90) */
|
|
57
|
+
quality?: number;
|
|
58
|
+
/** Timeout in milliseconds (default: 30000) */
|
|
59
|
+
timeout?: number;
|
|
60
|
+
/** Wait strategy: 'networkidle2' | 'networkidle0' | 'load' | 'domcontentloaded' */
|
|
61
|
+
waitUntil?: "networkidle2" | "networkidle0" | "load" | "domcontentloaded";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// === Helper Functions ===
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Detect image format from file extension
|
|
68
|
+
*/
|
|
69
|
+
function getImageFormat(filePath: string): "png" | "jpeg" | "webp" {
|
|
70
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
71
|
+
|
|
72
|
+
switch (ext) {
|
|
73
|
+
case ".jpg":
|
|
74
|
+
case ".jpeg":
|
|
75
|
+
return "jpeg";
|
|
76
|
+
case ".webp":
|
|
77
|
+
return "webp";
|
|
78
|
+
default:
|
|
79
|
+
return "png";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate URL format
|
|
85
|
+
*/
|
|
86
|
+
function validateURL(url: string): void {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = new URL(url);
|
|
89
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
90
|
+
throw new Error("Invalid protocol");
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
throw new ScreenshotError(
|
|
94
|
+
`Invalid URL: ${url}`,
|
|
95
|
+
"INVALID_URL",
|
|
96
|
+
"Invalid URL format",
|
|
97
|
+
"URL must start with http:// or https://",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate file path
|
|
104
|
+
*/
|
|
105
|
+
function validateFilePath(filePath: string): void {
|
|
106
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
107
|
+
const validExtensions = [".png", ".jpg", ".jpeg", ".webp"];
|
|
108
|
+
|
|
109
|
+
if (!validExtensions.includes(ext)) {
|
|
110
|
+
throw new ScreenshotError(
|
|
111
|
+
`Invalid file extension: ${ext}`,
|
|
112
|
+
"INVALID_EXTENSION",
|
|
113
|
+
"Unsupported image format",
|
|
114
|
+
`Supported formats: ${validExtensions.join(", ")}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Launch browser with error handling
|
|
121
|
+
*/
|
|
122
|
+
async function launchBrowser(
|
|
123
|
+
puppeteer: typeof import("puppeteer"),
|
|
124
|
+
): Promise<Browser> {
|
|
125
|
+
try {
|
|
126
|
+
return await puppeteer.default.launch({
|
|
127
|
+
headless: true,
|
|
128
|
+
args: [
|
|
129
|
+
"--no-sandbox",
|
|
130
|
+
"--disable-setuid-sandbox",
|
|
131
|
+
"--disable-dev-shm-usage", // Prevent /dev/shm issues
|
|
132
|
+
"--disable-accelerated-2d-canvas",
|
|
133
|
+
"--no-first-run",
|
|
134
|
+
"--no-zygote",
|
|
135
|
+
"--disable-gpu",
|
|
136
|
+
],
|
|
137
|
+
// Timeout for browser launch
|
|
138
|
+
timeout: 10000,
|
|
139
|
+
});
|
|
140
|
+
} catch (err: unknown) {
|
|
141
|
+
if (
|
|
142
|
+
err instanceof Error &&
|
|
143
|
+
(err.message.includes("Could not find") ||
|
|
144
|
+
err.message.includes("Chromium"))
|
|
145
|
+
) {
|
|
146
|
+
throw new ScreenshotError(
|
|
147
|
+
err.message,
|
|
148
|
+
"BROWSER_NOT_FOUND",
|
|
149
|
+
"Chromium browser not found",
|
|
150
|
+
"Puppeteer's Chromium is missing. Reinstall: bun install puppeteer",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
155
|
+
throw new ScreenshotError(
|
|
156
|
+
errorMessage,
|
|
157
|
+
"BROWSER_LAUNCH_FAILED",
|
|
158
|
+
"Failed to launch browser",
|
|
159
|
+
"Check if you have enough system resources (memory, disk space)",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Take screenshot with retry logic
|
|
166
|
+
*/
|
|
167
|
+
async function captureWithRetry(
|
|
168
|
+
url: string,
|
|
169
|
+
options: Required<ScreenshotOptions>,
|
|
170
|
+
puppeteer: typeof import("puppeteer"),
|
|
171
|
+
attempt: number = 1,
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
let browser: Browser | undefined;
|
|
174
|
+
let page: Page | undefined;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Launch browser
|
|
178
|
+
browser = await launchBrowser(puppeteer);
|
|
179
|
+
|
|
180
|
+
// Create page with timeout
|
|
181
|
+
page = await browser.newPage();
|
|
182
|
+
|
|
183
|
+
// Set viewport
|
|
184
|
+
await page.setViewport({
|
|
185
|
+
width: options.width,
|
|
186
|
+
height: options.height,
|
|
187
|
+
deviceScaleFactor: 1,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Set page timeout
|
|
191
|
+
page.setDefaultTimeout(options.timeout);
|
|
192
|
+
page.setDefaultNavigationTimeout(SCREENSHOT_CONFIG.PAGE_LOAD_TIMEOUT);
|
|
193
|
+
|
|
194
|
+
// Navigate to URL
|
|
195
|
+
try {
|
|
196
|
+
await page.goto(url, {
|
|
197
|
+
waitUntil: options.waitUntil,
|
|
198
|
+
timeout: SCREENSHOT_CONFIG.PAGE_LOAD_TIMEOUT,
|
|
199
|
+
});
|
|
200
|
+
} catch (err: unknown) {
|
|
201
|
+
// Navigation errors
|
|
202
|
+
if (
|
|
203
|
+
err instanceof Error &&
|
|
204
|
+
err.message.includes("net::ERR_NAME_NOT_RESOLVED")
|
|
205
|
+
) {
|
|
206
|
+
throw new ScreenshotError(
|
|
207
|
+
err.message,
|
|
208
|
+
"DNS_ERROR",
|
|
209
|
+
"Cannot resolve domain name",
|
|
210
|
+
`Check if ${new URL(url).hostname} is accessible`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
err instanceof Error &&
|
|
216
|
+
err.message.includes("net::ERR_CONNECTION_REFUSED")
|
|
217
|
+
) {
|
|
218
|
+
throw new ScreenshotError(
|
|
219
|
+
err.message,
|
|
220
|
+
"CONNECTION_REFUSED",
|
|
221
|
+
"Connection refused",
|
|
222
|
+
"The server is not accepting connections. Check if the URL is correct.",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (
|
|
227
|
+
err instanceof Error &&
|
|
228
|
+
(err.message.includes("Timeout") || err.message.includes("timeout"))
|
|
229
|
+
) {
|
|
230
|
+
throw new ScreenshotError(
|
|
231
|
+
err.message,
|
|
232
|
+
"PAGE_TIMEOUT",
|
|
233
|
+
"Page took too long to load",
|
|
234
|
+
"The page is slow or unresponsive. Try again or check your connection.",
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Generic navigation error
|
|
239
|
+
const navErrorMessage = err instanceof Error ? err.message : String(err);
|
|
240
|
+
throw new ScreenshotError(
|
|
241
|
+
navErrorMessage,
|
|
242
|
+
"NAVIGATION_ERROR",
|
|
243
|
+
"Failed to load page",
|
|
244
|
+
"The page may be blocking automated access or has loading issues.",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Determine format and options
|
|
249
|
+
const format = getImageFormat(options.filePath);
|
|
250
|
+
const screenshotOptions: Record<string, unknown> = {
|
|
251
|
+
path: options.filePath,
|
|
252
|
+
fullPage: options.fullPage,
|
|
253
|
+
type: format,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Add quality for JPEG/WebP
|
|
257
|
+
if (format === "jpeg" || format === "webp") {
|
|
258
|
+
screenshotOptions.quality = options.quality;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Take screenshot
|
|
262
|
+
await page.screenshot(screenshotOptions);
|
|
263
|
+
|
|
264
|
+
// Success - cleanup and return
|
|
265
|
+
await page.close();
|
|
266
|
+
await browser.close();
|
|
267
|
+
} catch (err: unknown) {
|
|
268
|
+
// Cleanup on error
|
|
269
|
+
if (page) {
|
|
270
|
+
try {
|
|
271
|
+
await page.close();
|
|
272
|
+
} catch {
|
|
273
|
+
// Ignore cleanup errors
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (browser) {
|
|
278
|
+
try {
|
|
279
|
+
await browser.close();
|
|
280
|
+
} catch {
|
|
281
|
+
// Ignore cleanup errors
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Retry on transient errors
|
|
286
|
+
if (err instanceof ScreenshotError && err.code === "PAGE_TIMEOUT") {
|
|
287
|
+
if (attempt < SCREENSHOT_CONFIG.MAX_RETRIES) {
|
|
288
|
+
const delay = SCREENSHOT_CONFIG.RETRY_DELAY * 2 ** (attempt - 1);
|
|
289
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
290
|
+
return captureWithRetry(url, options, puppeteer, attempt + 1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Re-throw ScreenshotError as-is
|
|
295
|
+
if (err instanceof ScreenshotError) {
|
|
296
|
+
throw err;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Wrap unknown errors
|
|
300
|
+
const unknownErrorMessage =
|
|
301
|
+
err instanceof Error ? err.message : String(err);
|
|
302
|
+
throw new ScreenshotError(
|
|
303
|
+
unknownErrorMessage || "Unknown error",
|
|
304
|
+
"SCREENSHOT_FAILED",
|
|
305
|
+
"Screenshot capture failed",
|
|
306
|
+
"An unexpected error occurred. Try again or check the URL.",
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Main screenshot function
|
|
313
|
+
*
|
|
314
|
+
* @param url - Target URL to screenshot
|
|
315
|
+
* @param options - Screenshot options
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* // Basic usage
|
|
319
|
+
* await takeScreenshot("https://example.com");
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* // Custom options
|
|
323
|
+
* await takeScreenshot("https://example.com", {
|
|
324
|
+
* filePath: "output.jpg",
|
|
325
|
+
* fullPage: false,
|
|
326
|
+
* quality: 85,
|
|
327
|
+
* width: 1280,
|
|
328
|
+
* height: 720,
|
|
329
|
+
* });
|
|
330
|
+
*/
|
|
331
|
+
export async function takeScreenshot(
|
|
332
|
+
url: string,
|
|
333
|
+
options: ScreenshotOptions | string = {},
|
|
334
|
+
): Promise<void> {
|
|
335
|
+
// Handle legacy string parameter (just filePath)
|
|
336
|
+
const opts: ScreenshotOptions =
|
|
337
|
+
typeof options === "string" ? { filePath: options } : options;
|
|
338
|
+
|
|
339
|
+
// Apply defaults
|
|
340
|
+
const finalOptions: Required<ScreenshotOptions> = {
|
|
341
|
+
filePath: opts.filePath || "screenshot.png",
|
|
342
|
+
fullPage: opts.fullPage ?? true,
|
|
343
|
+
width: opts.width || SCREENSHOT_CONFIG.VIEWPORT_WIDTH,
|
|
344
|
+
height: opts.height || SCREENSHOT_CONFIG.VIEWPORT_HEIGHT,
|
|
345
|
+
quality: opts.quality || SCREENSHOT_CONFIG.JPEG_QUALITY,
|
|
346
|
+
timeout: opts.timeout || SCREENSHOT_CONFIG.DEFAULT_TIMEOUT,
|
|
347
|
+
waitUntil: opts.waitUntil || "networkidle2",
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Validate inputs
|
|
351
|
+
validateURL(url);
|
|
352
|
+
validateFilePath(finalOptions.filePath);
|
|
353
|
+
|
|
354
|
+
// Validate dimensions
|
|
355
|
+
if (finalOptions.width <= 0 || finalOptions.height <= 0) {
|
|
356
|
+
throw new ScreenshotError(
|
|
357
|
+
"Invalid dimensions",
|
|
358
|
+
"INVALID_DIMENSIONS",
|
|
359
|
+
"Width and height must be positive",
|
|
360
|
+
"Use values like width: 1920, height: 1080",
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Validate quality for JPEG/WebP
|
|
365
|
+
const format = getImageFormat(finalOptions.filePath);
|
|
366
|
+
if (
|
|
367
|
+
(format === "jpeg" || format === "webp") &&
|
|
368
|
+
(finalOptions.quality < 0 || finalOptions.quality > 100)
|
|
369
|
+
) {
|
|
370
|
+
throw new ScreenshotError(
|
|
371
|
+
"Invalid quality",
|
|
372
|
+
"INVALID_QUALITY",
|
|
373
|
+
"Quality must be between 0-100",
|
|
374
|
+
"Use values like quality: 90",
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Dynamically import Puppeteer and capture screenshot with retry
|
|
379
|
+
try {
|
|
380
|
+
const puppeteer = await import("puppeteer");
|
|
381
|
+
await captureWithRetry(url, finalOptions, puppeteer);
|
|
382
|
+
} catch (err: unknown) {
|
|
383
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
384
|
+
throw new ScreenshotError(
|
|
385
|
+
`Puppeteer not available: ${message}`,
|
|
386
|
+
"PUPPETEER_MISSING",
|
|
387
|
+
"Screenshot functionality unavailable",
|
|
388
|
+
"Install Puppeteer: npm install puppeteer",
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Check if Puppeteer is available and working
|
|
395
|
+
*/
|
|
396
|
+
export async function checkPuppeteerAvailability(): Promise<{
|
|
397
|
+
available: boolean;
|
|
398
|
+
error?: string;
|
|
399
|
+
hint?: string;
|
|
400
|
+
}> {
|
|
401
|
+
try {
|
|
402
|
+
const puppeteer = await import("puppeteer");
|
|
403
|
+
const browser = await puppeteer.default.launch({
|
|
404
|
+
headless: true,
|
|
405
|
+
args: ["--no-sandbox"],
|
|
406
|
+
timeout: 5000,
|
|
407
|
+
});
|
|
408
|
+
await browser.close();
|
|
409
|
+
return { available: true };
|
|
410
|
+
} catch (err: unknown) {
|
|
411
|
+
if (
|
|
412
|
+
err &&
|
|
413
|
+
typeof err === "object" &&
|
|
414
|
+
"message" in err &&
|
|
415
|
+
typeof err.message === "string" &&
|
|
416
|
+
(err.message.includes("Could not find") ||
|
|
417
|
+
err.message.includes("Chromium"))
|
|
418
|
+
) {
|
|
419
|
+
return {
|
|
420
|
+
available: false,
|
|
421
|
+
error: "Chromium browser not found",
|
|
422
|
+
hint: "Reinstall Puppeteer: bun install puppeteer",
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (
|
|
426
|
+
err &&
|
|
427
|
+
typeof err === "object" &&
|
|
428
|
+
"message" in err &&
|
|
429
|
+
typeof err.message === "string" &&
|
|
430
|
+
err.message.includes("Cannot resolve module")
|
|
431
|
+
) {
|
|
432
|
+
return {
|
|
433
|
+
available: false,
|
|
434
|
+
error: "Puppeteer not installed",
|
|
435
|
+
hint: "Install Puppeteer: npm install puppeteer",
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
available: false,
|
|
440
|
+
error: "Puppeteer initialization failed",
|
|
441
|
+
hint: err instanceof Error ? err.message : String(err),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|