chrome-devtools-mcp-for-extension 0.6.2 → 0.6.4
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 +363 -188
- package/build/src/browser.js +50 -51
- package/build/src/tools/bookmarks.js +7 -30
- package/package.json +1 -1
- package/build/src/tools/webstore-auto-screenshot.js +0 -159
- package/build/src/tools/webstore-submission.js +0 -332
package/build/src/browser.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
-
import { promises as fsPromises } from 'node:fs';
|
|
8
7
|
import os from 'node:os';
|
|
9
8
|
import path from 'node:path';
|
|
10
9
|
import puppeteer from 'puppeteer-core';
|
|
@@ -43,6 +42,33 @@ async function ensureBrowserConnected(browserURL) {
|
|
|
43
42
|
});
|
|
44
43
|
return browser;
|
|
45
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Get the last used Chrome profile directory name from Local State
|
|
47
|
+
*/
|
|
48
|
+
function getLastUsedProfile(userDataDir) {
|
|
49
|
+
const localStatePath = path.join(userDataDir, 'Local State');
|
|
50
|
+
try {
|
|
51
|
+
const localStateContent = fs.readFileSync(localStatePath, 'utf8');
|
|
52
|
+
const localState = JSON.parse(localStateContent);
|
|
53
|
+
return localState?.profile?.last_used || 'Default';
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.warn(`Could not read Local State: ${error instanceof Error ? error.message : String(error)}`);
|
|
57
|
+
return 'Default';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if Chrome profile is already in use (locked)
|
|
62
|
+
* Throws an error if the profile is locked
|
|
63
|
+
*/
|
|
64
|
+
function assertProfileNotInUse(userDataDir) {
|
|
65
|
+
const lockFiles = ['SingletonLock', 'SingletonCookie', 'SingletonSocket'];
|
|
66
|
+
const hasLock = lockFiles.some(lockFile => fs.existsSync(path.join(userDataDir, lockFile)));
|
|
67
|
+
if (hasLock) {
|
|
68
|
+
throw new Error(`Chrome is already using this profile: ${userDataDir}\n` +
|
|
69
|
+
`Please close Chrome and try again.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
46
72
|
function scanExtensionsDirectory(extensionsDir) {
|
|
47
73
|
const extensionPaths = [];
|
|
48
74
|
try {
|
|
@@ -234,6 +260,7 @@ export async function launch(options) {
|
|
|
234
260
|
: 'chrome-profile';
|
|
235
261
|
let userDataDir = options.userDataDir;
|
|
236
262
|
let usingSystemProfile = false;
|
|
263
|
+
let profileDirectory = 'Default';
|
|
237
264
|
if (!isolated && !userDataDir) {
|
|
238
265
|
// Try to use system Chrome profile directly
|
|
239
266
|
const systemProfile = detectSystemChromeProfile(channel) || detectAnySystemChromeProfile();
|
|
@@ -241,9 +268,13 @@ export async function launch(options) {
|
|
|
241
268
|
// Use system profile directly for better user experience
|
|
242
269
|
userDataDir = systemProfile.path;
|
|
243
270
|
usingSystemProfile = true;
|
|
271
|
+
// Check if profile is already in use
|
|
272
|
+
assertProfileNotInUse(userDataDir);
|
|
273
|
+
// Detect last used profile directory
|
|
274
|
+
profileDirectory = getLastUsedProfile(userDataDir);
|
|
244
275
|
console.error(`✅ Using system Chrome profile: ${systemProfile.channel}`);
|
|
245
276
|
console.error(` Path: ${userDataDir}`);
|
|
246
|
-
console.error(`
|
|
277
|
+
console.error(` Profile Directory: ${profileDirectory}`);
|
|
247
278
|
}
|
|
248
279
|
else {
|
|
249
280
|
// Fallback to isolated profile if no system Chrome found
|
|
@@ -256,7 +287,7 @@ export async function launch(options) {
|
|
|
256
287
|
}
|
|
257
288
|
const args = [
|
|
258
289
|
'--hide-crash-restore-bubble',
|
|
259
|
-
|
|
290
|
+
`--profile-directory=${profileDirectory}`,
|
|
260
291
|
];
|
|
261
292
|
if (customDevTools) {
|
|
262
293
|
args.push(`--custom-devtools-frontend=file://${customDevTools}`);
|
|
@@ -333,6 +364,7 @@ export async function launch(options) {
|
|
|
333
364
|
console.error(` Channel: ${puppeterChannel || 'default'}`);
|
|
334
365
|
console.error(` Executable: ${executablePath || 'auto-detected'}`);
|
|
335
366
|
console.error(` User Data Dir: ${userDataDir || 'temporary'}`);
|
|
367
|
+
console.error(` Profile Directory: ${profileDirectory}`);
|
|
336
368
|
console.error(` Profile Type: ${usingSystemProfile ? 'System Profile (auto-detected)' : 'Custom Profile'}`);
|
|
337
369
|
console.error(` Headless: ${headless}`);
|
|
338
370
|
console.error(` Args: ${JSON.stringify(args, null, 2)}`);
|
|
@@ -349,6 +381,11 @@ export async function launch(options) {
|
|
|
349
381
|
args,
|
|
350
382
|
ignoreDefaultArgs: ['--disable-extensions', '--enable-automation'],
|
|
351
383
|
});
|
|
384
|
+
// Log actual spawn args for debugging
|
|
385
|
+
const spawnArgs = browser.process()?.spawnargs;
|
|
386
|
+
if (spawnArgs) {
|
|
387
|
+
console.error(`Actual spawn args: ${spawnArgs.join(' ')}`);
|
|
388
|
+
}
|
|
352
389
|
if (options.logFile) {
|
|
353
390
|
// FIXME: we are probably subscribing too late to catch startup logs. We
|
|
354
391
|
// should expose the process earlier or expose the getRecentLogs() getter.
|
|
@@ -424,55 +461,17 @@ export async function launch(options) {
|
|
|
424
461
|
return browser;
|
|
425
462
|
}
|
|
426
463
|
catch (error) {
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
console.error(`⚠️ Chrome is already running with system profile.`);
|
|
433
|
-
console.error(`📋 Creating a temporary copy of system profile for automation...`);
|
|
434
|
-
// Create a temporary copy of the system profile
|
|
435
|
-
const tempProfileDir = path.join(os.tmpdir(), `chrome-mcp-${Date.now()}`);
|
|
436
|
-
try {
|
|
437
|
-
// Copy only essential data (bookmarks, extensions) without locking files
|
|
438
|
-
await fsPromises.mkdir(tempProfileDir, { recursive: true });
|
|
439
|
-
// Copy bookmarks if they exist
|
|
440
|
-
const bookmarksPath = path.join(userDataDir, 'Default', 'Bookmarks');
|
|
441
|
-
try {
|
|
442
|
-
await fsPromises.access(bookmarksPath);
|
|
443
|
-
const bookmarksDestDir = path.join(tempProfileDir, 'Default');
|
|
444
|
-
await fsPromises.mkdir(bookmarksDestDir, { recursive: true });
|
|
445
|
-
await fsPromises.copyFile(bookmarksPath, path.join(bookmarksDestDir, 'Bookmarks'));
|
|
446
|
-
console.error(`✅ Bookmarks copied from system profile`);
|
|
447
|
-
}
|
|
448
|
-
catch {
|
|
449
|
-
// Bookmarks don't exist or can't be accessed, skip
|
|
450
|
-
}
|
|
451
|
-
// Retry with the temporary profile by recursively calling launch
|
|
452
|
-
console.error(`🔄 Retrying with temporary profile copy...`);
|
|
453
|
-
const retryOptions = { ...options };
|
|
454
|
-
retryOptions.userDataDir = tempProfileDir;
|
|
455
|
-
return launch(retryOptions);
|
|
456
|
-
}
|
|
457
|
-
catch (copyError) {
|
|
458
|
-
console.error(`❌ Failed to create temporary profile copy: ${copyError}`);
|
|
459
|
-
// Fall through to regular error handling
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
// For non-system profiles, maintain the blocking behavior
|
|
463
|
-
if (!usingSystemProfile &&
|
|
464
|
-
userDataDir &&
|
|
465
|
-
(error.message.includes('The browser is already running') ||
|
|
466
|
-
error.message.includes('Target closed') ||
|
|
467
|
-
error.message.includes('Connection closed'))) {
|
|
468
|
-
throw new Error(`The browser is already running for custom profile: ${userDataDir}. Use --isolated to run multiple browser instances.`, {
|
|
469
|
-
cause: error,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
// If system profile failed for other reasons, suggest fallback options
|
|
464
|
+
// Fail fast with clear error message - no silent fallback
|
|
465
|
+
console.error(`❌ Failed to launch Chrome`);
|
|
466
|
+
console.error(` User Data Dir: ${userDataDir}`);
|
|
467
|
+
console.error(` Profile Directory: ${profileDirectory}`);
|
|
468
|
+
console.error(` Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
473
469
|
if (usingSystemProfile) {
|
|
474
|
-
console.error(
|
|
475
|
-
console.error(
|
|
470
|
+
console.error('');
|
|
471
|
+
console.error('💡 Troubleshooting:');
|
|
472
|
+
console.error(' 1. Close all Chrome windows and try again');
|
|
473
|
+
console.error(' 2. Use --isolated flag to use temporary profile');
|
|
474
|
+
console.error(' 3. Use --userDataDir to specify custom profile location');
|
|
476
475
|
}
|
|
477
476
|
throw error;
|
|
478
477
|
}
|
|
@@ -110,63 +110,40 @@ function loadChromeBookmarks() {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
|
-
* Get all bookmarks:
|
|
113
|
+
* Get all bookmarks: returns only default development bookmarks for privacy
|
|
114
|
+
* User's personal Chrome bookmarks are not loaded to protect privacy
|
|
114
115
|
*/
|
|
115
116
|
function getBookmarks() {
|
|
116
|
-
|
|
117
|
-
const chromeBookmarks = loadChromeBookmarks();
|
|
118
|
-
// Merge bookmarks: Chrome bookmarks take precedence, but defaults are always included
|
|
119
|
-
return {
|
|
120
|
-
...defaultBookmarks,
|
|
121
|
-
...chromeBookmarks
|
|
122
|
-
};
|
|
117
|
+
return getDefaultBookmarks();
|
|
123
118
|
}
|
|
124
119
|
export const listBookmarks = defineTool({
|
|
125
120
|
name: 'list_bookmarks',
|
|
126
|
-
description: `List all available bookmarks
|
|
121
|
+
description: `List all available bookmarks. Returns hardcoded development bookmarks only. User's personal Chrome bookmarks are not loaded to protect privacy.`,
|
|
127
122
|
annotations: {
|
|
128
123
|
category: ToolCategories.NAVIGATION_AUTOMATION,
|
|
129
124
|
readOnlyHint: true,
|
|
130
125
|
},
|
|
131
126
|
schema: {},
|
|
132
127
|
handler: async (_request, response, _context) => {
|
|
133
|
-
const defaultBookmarks = getDefaultBookmarks();
|
|
134
|
-
const chromeBookmarks = loadChromeBookmarks();
|
|
135
128
|
const allBookmarks = getBookmarks();
|
|
136
129
|
const bookmarkNames = Object.keys(allBookmarks);
|
|
137
130
|
if (bookmarkNames.length === 0) {
|
|
138
131
|
response.appendResponseLine('No bookmarks configured.');
|
|
139
|
-
response.appendResponseLine('');
|
|
140
|
-
response.appendResponseLine('💡 **Tip:** Chrome bookmarks could not be loaded. Check if Chrome is installed and has bookmarks.');
|
|
141
132
|
return;
|
|
142
133
|
}
|
|
143
|
-
response.appendResponseLine('📚 **Available Bookmarks:**');
|
|
144
|
-
response.appendResponseLine('');
|
|
145
|
-
// Show loading status
|
|
146
|
-
const chromeBookmarkCount = Object.keys(chromeBookmarks).length;
|
|
147
|
-
const defaultBookmarkCount = Object.keys(defaultBookmarks).length;
|
|
148
|
-
if (chromeBookmarkCount > 0) {
|
|
149
|
-
response.appendResponseLine(`✅ Loaded ${chromeBookmarkCount} bookmarks from Chrome profile${chromeBookmarkCount >= 100 ? ' (limited to 100)' : ''}`);
|
|
150
|
-
response.appendResponseLine(`📋 ${defaultBookmarkCount} default development bookmarks included`);
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
response.appendResponseLine(`⚠️ Could not load Chrome bookmarks, using ${defaultBookmarkCount} default bookmarks`);
|
|
154
|
-
}
|
|
134
|
+
response.appendResponseLine('📚 **Available Development Bookmarks:**');
|
|
155
135
|
response.appendResponseLine('');
|
|
156
136
|
bookmarkNames.forEach(name => {
|
|
157
137
|
const url = allBookmarks[name];
|
|
158
|
-
|
|
159
|
-
response.appendResponseLine(`${source} **${name}**: ${url}`);
|
|
138
|
+
response.appendResponseLine(`🔧 **${name}**: ${url}`);
|
|
160
139
|
});
|
|
161
140
|
response.appendResponseLine('');
|
|
162
|
-
response.appendResponseLine('**Legend:** 🌐 = Chrome bookmark, 🔧 = Default bookmark');
|
|
163
|
-
response.appendResponseLine('');
|
|
164
141
|
response.appendResponseLine(`Use \`navigate_bookmark name="<bookmark_name>"\` to navigate to any of these URLs.`);
|
|
165
142
|
},
|
|
166
143
|
});
|
|
167
144
|
export const navigateBookmark = defineTool({
|
|
168
145
|
name: 'navigate_bookmark',
|
|
169
|
-
description: `Navigate to a bookmark URL from
|
|
146
|
+
description: `Navigate to a bookmark URL from default development bookmarks. User's personal Chrome bookmarks are not loaded to protect privacy.`,
|
|
170
147
|
annotations: {
|
|
171
148
|
category: ToolCategories.NAVIGATION_AUTOMATION,
|
|
172
149
|
readOnlyHint: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "MCP server for Chrome extension development with Web Store automation. Fork of chrome-devtools-mcp with extension-specific tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./build/src/index.js",
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2025 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import z from 'zod';
|
|
9
|
-
import { ToolCategories } from './categories.js';
|
|
10
|
-
import { defineTool } from './ToolDefinition.js';
|
|
11
|
-
// Tool for generating extension screenshots automatically
|
|
12
|
-
export const generateExtensionScreenshots = defineTool({
|
|
13
|
-
name: 'generate_extension_screenshots',
|
|
14
|
-
description: `Automatically generate screenshots for Chrome Web Store submission`,
|
|
15
|
-
annotations: {
|
|
16
|
-
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
17
|
-
readOnlyHint: false,
|
|
18
|
-
},
|
|
19
|
-
schema: {
|
|
20
|
-
extensionPath: z.string().describe('Path to the extension directory'),
|
|
21
|
-
extensionId: z.string().optional().describe('Extension ID if already installed'),
|
|
22
|
-
},
|
|
23
|
-
handler: async (request, response, context) => {
|
|
24
|
-
const { extensionPath, extensionId } = request.params;
|
|
25
|
-
const screenshotsDir = path.join(path.dirname(extensionPath), 'screenshots');
|
|
26
|
-
// Create screenshots directory
|
|
27
|
-
if (!fs.existsSync(screenshotsDir)) {
|
|
28
|
-
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
const page = context.getSelectedPage();
|
|
31
|
-
response.appendResponseLine('📸 **Generating Extension Screenshots**');
|
|
32
|
-
response.appendResponseLine('='.repeat(40));
|
|
33
|
-
response.appendResponseLine('');
|
|
34
|
-
try {
|
|
35
|
-
// Set viewport to Chrome Web Store recommended size
|
|
36
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
37
|
-
// Screenshot 1: Extension in action on a popular website
|
|
38
|
-
response.appendResponseLine('1️⃣ Taking screenshot of extension in action...');
|
|
39
|
-
// Navigate to a demo page
|
|
40
|
-
await page.goto('https://www.example.com', { waitUntil: 'networkidle0' });
|
|
41
|
-
// If extension has a popup, try to capture it
|
|
42
|
-
if (extensionId) {
|
|
43
|
-
// Open extension popup if possible
|
|
44
|
-
const extensionUrl = `chrome-extension://${extensionId}/popup.html`;
|
|
45
|
-
// Try to open popup in new tab for screenshot
|
|
46
|
-
const popupPage = await page.browser().newPage();
|
|
47
|
-
await popupPage.setViewport({ width: 1280, height: 800 });
|
|
48
|
-
try {
|
|
49
|
-
await popupPage.goto(extensionUrl, { waitUntil: 'networkidle0' });
|
|
50
|
-
// Take screenshot of popup
|
|
51
|
-
const popupScreenshot = await popupPage.screenshot({
|
|
52
|
-
type: 'png',
|
|
53
|
-
fullPage: false,
|
|
54
|
-
});
|
|
55
|
-
const popupPath = path.join(screenshotsDir, 'screenshot-1-popup.png');
|
|
56
|
-
fs.writeFileSync(popupPath, popupScreenshot);
|
|
57
|
-
response.appendResponseLine(`✅ Popup screenshot saved: ${popupPath}`);
|
|
58
|
-
await popupPage.close();
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
response.appendResponseLine(`⚠️ Could not capture popup: ${error}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// Screenshot 2: Extension toolbar icon
|
|
65
|
-
response.appendResponseLine('2️⃣ Taking screenshot with extension icon visible...');
|
|
66
|
-
// Navigate to chrome://extensions to show the extension
|
|
67
|
-
await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
|
|
68
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
69
|
-
const extensionsScreenshot = await page.screenshot({
|
|
70
|
-
type: 'png',
|
|
71
|
-
fullPage: false,
|
|
72
|
-
});
|
|
73
|
-
const extensionsPath = path.join(screenshotsDir, 'screenshot-2-extensions.png');
|
|
74
|
-
fs.writeFileSync(extensionsPath, extensionsScreenshot);
|
|
75
|
-
response.appendResponseLine(`✅ Extensions page screenshot saved: ${extensionsPath}`);
|
|
76
|
-
// Screenshot 3: Options page (if exists)
|
|
77
|
-
response.appendResponseLine('3️⃣ Taking screenshot of options page...');
|
|
78
|
-
if (extensionId) {
|
|
79
|
-
const optionsUrl = `chrome-extension://${extensionId}/options.html`;
|
|
80
|
-
try {
|
|
81
|
-
await page.goto(optionsUrl, { waitUntil: 'networkidle0' });
|
|
82
|
-
const optionsScreenshot = await page.screenshot({
|
|
83
|
-
type: 'png',
|
|
84
|
-
fullPage: false,
|
|
85
|
-
});
|
|
86
|
-
const optionsPath = path.join(screenshotsDir, 'screenshot-3-options.png');
|
|
87
|
-
fs.writeFileSync(optionsPath, optionsScreenshot);
|
|
88
|
-
response.appendResponseLine(`✅ Options page screenshot saved: ${optionsPath}`);
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
response.appendResponseLine(`⚠️ No options page found`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// Screenshot 4: Extension working on a real website
|
|
95
|
-
response.appendResponseLine('4️⃣ Taking screenshot on a real website...');
|
|
96
|
-
// Navigate to a website where the extension might be useful
|
|
97
|
-
await page.goto('https://www.google.com', { waitUntil: 'networkidle0' });
|
|
98
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
99
|
-
const websiteScreenshot = await page.screenshot({
|
|
100
|
-
type: 'png',
|
|
101
|
-
fullPage: false,
|
|
102
|
-
});
|
|
103
|
-
const websitePath = path.join(screenshotsDir, 'screenshot-4-website.png');
|
|
104
|
-
fs.writeFileSync(websitePath, websiteScreenshot);
|
|
105
|
-
response.appendResponseLine(`✅ Website screenshot saved: ${websitePath}`);
|
|
106
|
-
// Generate promotional images (smaller versions)
|
|
107
|
-
response.appendResponseLine('');
|
|
108
|
-
response.appendResponseLine('5️⃣ Generating promotional images...');
|
|
109
|
-
// Small promo tile: 440x280
|
|
110
|
-
await page.setViewport({ width: 440, height: 280 });
|
|
111
|
-
await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
|
|
112
|
-
const smallPromo = await page.screenshot({
|
|
113
|
-
type: 'png',
|
|
114
|
-
fullPage: false,
|
|
115
|
-
});
|
|
116
|
-
const smallPromoPath = path.join(screenshotsDir, 'promo-small-440x280.png');
|
|
117
|
-
fs.writeFileSync(smallPromoPath, smallPromo);
|
|
118
|
-
response.appendResponseLine(`✅ Small promo tile saved: ${smallPromoPath}`);
|
|
119
|
-
// Large promo tile: 920x680
|
|
120
|
-
await page.setViewport({ width: 920, height: 680 });
|
|
121
|
-
const largePromo = await page.screenshot({
|
|
122
|
-
type: 'png',
|
|
123
|
-
fullPage: false,
|
|
124
|
-
});
|
|
125
|
-
const largePromoPath = path.join(screenshotsDir, 'promo-large-920x680.png');
|
|
126
|
-
fs.writeFileSync(largePromoPath, largePromo);
|
|
127
|
-
response.appendResponseLine(`✅ Large promo tile saved: ${largePromoPath}`);
|
|
128
|
-
// Marquee promo: 1400x560
|
|
129
|
-
await page.setViewport({ width: 1400, height: 560 });
|
|
130
|
-
const marqueePromo = await page.screenshot({
|
|
131
|
-
type: 'png',
|
|
132
|
-
fullPage: false,
|
|
133
|
-
});
|
|
134
|
-
const marqueePromoPath = path.join(screenshotsDir, 'promo-marquee-1400x560.png');
|
|
135
|
-
fs.writeFileSync(marqueePromoPath, marqueePromo);
|
|
136
|
-
response.appendResponseLine(`✅ Marquee promo saved: ${marqueePromoPath}`);
|
|
137
|
-
response.appendResponseLine('');
|
|
138
|
-
response.appendResponseLine('='.repeat(40));
|
|
139
|
-
response.appendResponseLine('✅ **Screenshots Generated Successfully!**');
|
|
140
|
-
response.appendResponseLine('');
|
|
141
|
-
response.appendResponseLine(`📁 Screenshots saved in: ${screenshotsDir}`);
|
|
142
|
-
response.appendResponseLine('');
|
|
143
|
-
response.appendResponseLine('**Chrome Web Store Requirements:**');
|
|
144
|
-
response.appendResponseLine('• Screenshots: 1280x800 or 640x400 (PNG or JPG)');
|
|
145
|
-
response.appendResponseLine('• Small promo tile: 440x280');
|
|
146
|
-
response.appendResponseLine('• Large promo tile: 920x680');
|
|
147
|
-
response.appendResponseLine('• Marquee promo: 1400x560');
|
|
148
|
-
response.appendResponseLine('');
|
|
149
|
-
response.appendResponseLine('💡 **Tip:** Edit these screenshots to highlight your extension features!');
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
response.appendResponseLine(`❌ Error generating screenshots: ${error}`);
|
|
153
|
-
}
|
|
154
|
-
finally {
|
|
155
|
-
// Reset viewport
|
|
156
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
});
|