chrome-devtools-mcp-for-extension 0.6.3 → 0.6.5
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 +24 -55
- package/build/src/browser.js +36 -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/README.md
CHANGED
|
@@ -25,19 +25,6 @@ AI-powered Chrome extension development with automated testing, debugging, and W
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
## 📊 How It Compares
|
|
29
|
-
|
|
30
|
-
| Feature | This Tool | Puppeteer/Playwright | Original chrome-devtools-mcp |
|
|
31
|
-
|---------|-----------|----------------------|------------------------------|
|
|
32
|
-
| Extension Support | ✅ Always enabled | ❌ Disabled by default | ⚠️ Manual config required |
|
|
33
|
-
| Setup Required | ❌ None | ✅ Complex config files | ✅ Multiple flags needed |
|
|
34
|
-
| Real User Profile | ✅ Direct access | ❌ Temporary profiles | ⚠️ Optional |
|
|
35
|
-
| Profile Copying | ❌ No copying needed | ⚠️ Manual setup | ⚠️ Manual setup |
|
|
36
|
-
| Web Store Automation | ✅ Built-in | ❌ None | ❌ None |
|
|
37
|
-
| Extension Debugging | ✅ Service worker + console | ⚠️ Limited | ❌ None |
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
28
|
## 🚀 Quick Start
|
|
42
29
|
|
|
43
30
|
### 1. Install (30 seconds)
|
|
@@ -47,14 +34,10 @@ AI-powered Chrome extension development with automated testing, debugging, and W
|
|
|
47
34
|
claude mcp add --scope user chrome-devtools-extension npx chrome-devtools-mcp-for-extension@latest
|
|
48
35
|
```
|
|
49
36
|
|
|
50
|
-
<details>
|
|
51
37
|
<summary>Other MCP clients (Cursor, VS Code Copilot, Cline)</summary>
|
|
52
38
|
|
|
53
39
|
Add to your MCP configuration file:
|
|
54
40
|
|
|
55
|
-
**Cursor**: `~/.cursor/extensions_config.json`
|
|
56
|
-
**VS Code Copilot**: `.vscode/settings.json`
|
|
57
|
-
|
|
58
41
|
```json
|
|
59
42
|
{
|
|
60
43
|
"mcpServers": {
|
|
@@ -66,7 +49,6 @@ Add to your MCP configuration file:
|
|
|
66
49
|
}
|
|
67
50
|
```
|
|
68
51
|
|
|
69
|
-
</details>
|
|
70
52
|
|
|
71
53
|
### 2. Restart your AI client
|
|
72
54
|
|
|
@@ -79,39 +61,6 @@ See [Common Workflows](#-common-workflows) below for typical use cases
|
|
|
79
61
|
|
|
80
62
|
---
|
|
81
63
|
|
|
82
|
-
## 💡 See The Difference
|
|
83
|
-
|
|
84
|
-
### Traditional Approach (Puppeteer)
|
|
85
|
-
```javascript
|
|
86
|
-
// ❌ Complex 15+ line setup
|
|
87
|
-
const browser = await puppeteer.launch({
|
|
88
|
-
headless: false,
|
|
89
|
-
args: [
|
|
90
|
-
'--disable-extensions-except=/path/to/your/extension',
|
|
91
|
-
'--load-extension=/path/to/your/extension',
|
|
92
|
-
'--user-data-dir=/tmp/test-profile',
|
|
93
|
-
// ... 10+ more flags
|
|
94
|
-
],
|
|
95
|
-
ignoreDefaultArgs: ['--disable-extensions'],
|
|
96
|
-
});
|
|
97
|
-
// Still doesn't use your real Chrome environment!
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Zero-Config Approach (This Tool)
|
|
101
|
-
```bash
|
|
102
|
-
# ✅ One command
|
|
103
|
-
claude mcp add chrome-devtools-extension npx chrome-devtools-mcp-for-extension@latest
|
|
104
|
-
|
|
105
|
-
# Then just ask:
|
|
106
|
-
"Test my extension on youtube.com"
|
|
107
|
-
"Debug why my content script isn't working"
|
|
108
|
-
"Submit my extension to Chrome Web Store"
|
|
109
|
-
|
|
110
|
-
# That's it! Uses your actual Chrome with all your extensions
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
64
|
## ✨ Core Capabilities
|
|
116
65
|
|
|
117
66
|
- 🧩 **Extension Development**: Load, debug, and reload Chrome extensions during development
|
|
@@ -166,6 +115,21 @@ Quick reference for the 3 core extension tools:
|
|
|
166
115
|
| `reload_extension` | Hot-reload during development | "Reload my-extension" |
|
|
167
116
|
| `inspect_service_worker` | Debug background scripts | "Debug service worker for my-extension" |
|
|
168
117
|
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 📊 How It Compares
|
|
122
|
+
|
|
123
|
+
| Feature | This Tool | Puppeteer/Playwright | Original chrome-devtools-mcp |
|
|
124
|
+
|---------|-----------|----------------------|------------------------------|
|
|
125
|
+
| Extension Support | ✅ Always enabled | ❌ Disabled by default | ⚠️ Manual config required |
|
|
126
|
+
| Setup Required | ❌ None | ✅ Complex config files | ✅ Multiple flags needed |
|
|
127
|
+
| Real User Profile | ✅ Direct access | ❌ Temporary profiles | ⚠️ Optional |
|
|
128
|
+
| Profile Copying | ❌ No copying needed | ⚠️ Manual setup | ⚠️ Manual setup |
|
|
129
|
+
| Web Store Automation | ✅ Built-in | ❌ None | ❌ None |
|
|
130
|
+
| Extension Debugging | ✅ Service worker + console | ⚠️ Limited | ❌ None |
|
|
131
|
+
|
|
132
|
+
---
|
|
169
133
|
<details>
|
|
170
134
|
<summary>📖 Detailed Tool Documentation</summary>
|
|
171
135
|
|
|
@@ -479,11 +443,16 @@ interface ManifestValidation {
|
|
|
479
443
|
- `--load-extension` may be restricted in newer Chrome versions
|
|
480
444
|
- **Solution**: Use system profile (default) instead of `--loadExtension` flag
|
|
481
445
|
|
|
482
|
-
##
|
|
446
|
+
## Concurrent Chrome Usage
|
|
447
|
+
|
|
448
|
+
**Can I use Chrome while the MCP server is running?**
|
|
449
|
+
|
|
450
|
+
Yes! The MCP server can run alongside your regular Chrome browser. Chrome is robust enough to handle concurrent access to the same profile.
|
|
483
451
|
|
|
484
|
-
**
|
|
485
|
-
|
|
486
|
-
-
|
|
452
|
+
**Note:** If you experience any issues with concurrent usage, you can use the `--isolated` flag to run with a separate profile:
|
|
453
|
+
```bash
|
|
454
|
+
npx chrome-devtools-mcp-for-extension@latest --isolated
|
|
455
|
+
```
|
|
487
456
|
|
|
488
457
|
</details>
|
|
489
458
|
|
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,21 @@ 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
|
+
}
|
|
46
60
|
function scanExtensionsDirectory(extensionsDir) {
|
|
47
61
|
const extensionPaths = [];
|
|
48
62
|
try {
|
|
@@ -234,6 +248,7 @@ export async function launch(options) {
|
|
|
234
248
|
: 'chrome-profile';
|
|
235
249
|
let userDataDir = options.userDataDir;
|
|
236
250
|
let usingSystemProfile = false;
|
|
251
|
+
let profileDirectory = 'Default';
|
|
237
252
|
if (!isolated && !userDataDir) {
|
|
238
253
|
// Try to use system Chrome profile directly
|
|
239
254
|
const systemProfile = detectSystemChromeProfile(channel) || detectAnySystemChromeProfile();
|
|
@@ -241,9 +256,11 @@ export async function launch(options) {
|
|
|
241
256
|
// Use system profile directly for better user experience
|
|
242
257
|
userDataDir = systemProfile.path;
|
|
243
258
|
usingSystemProfile = true;
|
|
259
|
+
// Detect last used profile directory
|
|
260
|
+
profileDirectory = getLastUsedProfile(userDataDir);
|
|
244
261
|
console.error(`✅ Using system Chrome profile: ${systemProfile.channel}`);
|
|
245
262
|
console.error(` Path: ${userDataDir}`);
|
|
246
|
-
console.error(`
|
|
263
|
+
console.error(` Profile Directory: ${profileDirectory}`);
|
|
247
264
|
}
|
|
248
265
|
else {
|
|
249
266
|
// Fallback to isolated profile if no system Chrome found
|
|
@@ -256,7 +273,7 @@ export async function launch(options) {
|
|
|
256
273
|
}
|
|
257
274
|
const args = [
|
|
258
275
|
'--hide-crash-restore-bubble',
|
|
259
|
-
|
|
276
|
+
`--profile-directory=${profileDirectory}`,
|
|
260
277
|
];
|
|
261
278
|
if (customDevTools) {
|
|
262
279
|
args.push(`--custom-devtools-frontend=file://${customDevTools}`);
|
|
@@ -333,6 +350,7 @@ export async function launch(options) {
|
|
|
333
350
|
console.error(` Channel: ${puppeterChannel || 'default'}`);
|
|
334
351
|
console.error(` Executable: ${executablePath || 'auto-detected'}`);
|
|
335
352
|
console.error(` User Data Dir: ${userDataDir || 'temporary'}`);
|
|
353
|
+
console.error(` Profile Directory: ${profileDirectory}`);
|
|
336
354
|
console.error(` Profile Type: ${usingSystemProfile ? 'System Profile (auto-detected)' : 'Custom Profile'}`);
|
|
337
355
|
console.error(` Headless: ${headless}`);
|
|
338
356
|
console.error(` Args: ${JSON.stringify(args, null, 2)}`);
|
|
@@ -349,6 +367,11 @@ export async function launch(options) {
|
|
|
349
367
|
args,
|
|
350
368
|
ignoreDefaultArgs: ['--disable-extensions', '--enable-automation'],
|
|
351
369
|
});
|
|
370
|
+
// Log actual spawn args for debugging
|
|
371
|
+
const spawnArgs = browser.process()?.spawnargs;
|
|
372
|
+
if (spawnArgs) {
|
|
373
|
+
console.error(`Actual spawn args: ${spawnArgs.join(' ')}`);
|
|
374
|
+
}
|
|
352
375
|
if (options.logFile) {
|
|
353
376
|
// FIXME: we are probably subscribing too late to catch startup logs. We
|
|
354
377
|
// should expose the process earlier or expose the getRecentLogs() getter.
|
|
@@ -424,55 +447,17 @@ export async function launch(options) {
|
|
|
424
447
|
return browser;
|
|
425
448
|
}
|
|
426
449
|
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
|
|
450
|
+
// Fail fast with clear error message - no silent fallback
|
|
451
|
+
console.error(`❌ Failed to launch Chrome`);
|
|
452
|
+
console.error(` User Data Dir: ${userDataDir}`);
|
|
453
|
+
console.error(` Profile Directory: ${profileDirectory}`);
|
|
454
|
+
console.error(` Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
473
455
|
if (usingSystemProfile) {
|
|
474
|
-
console.error(
|
|
475
|
-
console.error(
|
|
456
|
+
console.error('');
|
|
457
|
+
console.error('💡 Troubleshooting:');
|
|
458
|
+
console.error(' 1. Close all Chrome windows and try again');
|
|
459
|
+
console.error(' 2. Use --isolated flag to use temporary profile');
|
|
460
|
+
console.error(' 3. Use --userDataDir to specify custom profile location');
|
|
476
461
|
}
|
|
477
462
|
throw error;
|
|
478
463
|
}
|
|
@@ -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.5",
|
|
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
|
-
});
|
|
@@ -1,332 +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 archiver from 'archiver';
|
|
9
|
-
import z from 'zod';
|
|
10
|
-
import { ToolCategories } from './categories.js';
|
|
11
|
-
import { defineTool } from './ToolDefinition.js';
|
|
12
|
-
// Main submission tool - now with browser automation!
|
|
13
|
-
export const submitToWebStore = defineTool({
|
|
14
|
-
name: 'submit_to_webstore',
|
|
15
|
-
description: `Automatically submit a Chrome extension to the Web Store using browser automation`,
|
|
16
|
-
annotations: {
|
|
17
|
-
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
18
|
-
readOnlyHint: false,
|
|
19
|
-
},
|
|
20
|
-
schema: {
|
|
21
|
-
extensionPath: z.string().describe('Path to the extension directory'),
|
|
22
|
-
autoSubmit: z.boolean().optional().default(false).describe('Automatically submit via browser (requires login)'),
|
|
23
|
-
},
|
|
24
|
-
handler: async (request, response, context) => {
|
|
25
|
-
const { extensionPath } = request.params;
|
|
26
|
-
const outputPath = path.join(path.dirname(extensionPath), `${path.basename(extensionPath)}-submission.zip`);
|
|
27
|
-
response.appendResponseLine('🚀 **Chrome Web Store Submission Process**');
|
|
28
|
-
response.appendResponseLine('='.repeat(40));
|
|
29
|
-
response.appendResponseLine('');
|
|
30
|
-
// Step 1: Validate manifest
|
|
31
|
-
response.appendResponseLine('**Step 1: Validating manifest.json...**');
|
|
32
|
-
const manifestPath = path.join(extensionPath, 'manifest.json');
|
|
33
|
-
let manifest;
|
|
34
|
-
let manifestValid = true;
|
|
35
|
-
try {
|
|
36
|
-
if (!fs.existsSync(manifestPath)) {
|
|
37
|
-
response.appendResponseLine('❌ manifest.json not found');
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
|
41
|
-
manifest = JSON.parse(manifestContent);
|
|
42
|
-
// Validation checks
|
|
43
|
-
const errors = [];
|
|
44
|
-
const warnings = [];
|
|
45
|
-
const suggestions = [];
|
|
46
|
-
// Required fields
|
|
47
|
-
if (manifest.manifest_version !== 3) {
|
|
48
|
-
errors.push('Must use Manifest V3 (manifest_version: 3)');
|
|
49
|
-
manifestValid = false;
|
|
50
|
-
}
|
|
51
|
-
if (!manifest.name || manifest.name.length > 45) {
|
|
52
|
-
errors.push('Name is required and must be <= 45 characters');
|
|
53
|
-
manifestValid = false;
|
|
54
|
-
}
|
|
55
|
-
if (!manifest.version || !/^\d+\.\d+\.\d+(\.\d+)?$/.test(manifest.version)) {
|
|
56
|
-
errors.push('Version must follow format: 1.0.0 or 1.0.0.0');
|
|
57
|
-
manifestValid = false;
|
|
58
|
-
}
|
|
59
|
-
if (!manifest.description || manifest.description.length > 132) {
|
|
60
|
-
warnings.push('Description should be provided and <= 132 characters');
|
|
61
|
-
}
|
|
62
|
-
// Icons
|
|
63
|
-
if (!manifest.icons || !manifest.icons['128']) {
|
|
64
|
-
warnings.push('Should include 128x128 icon');
|
|
65
|
-
}
|
|
66
|
-
// Permissions check
|
|
67
|
-
const dangerousPermissions = [
|
|
68
|
-
'debugger',
|
|
69
|
-
'devtools',
|
|
70
|
-
'management',
|
|
71
|
-
'privacy',
|
|
72
|
-
'proxy',
|
|
73
|
-
'system.cpu',
|
|
74
|
-
'system.memory',
|
|
75
|
-
'vpnProvider',
|
|
76
|
-
];
|
|
77
|
-
const usedDangerousPerms = manifest.permissions?.filter(p => dangerousPermissions.includes(p)) || [];
|
|
78
|
-
if (usedDangerousPerms.length > 0) {
|
|
79
|
-
warnings.push(`Uses sensitive permissions: ${usedDangerousPerms.join(', ')}`);
|
|
80
|
-
}
|
|
81
|
-
// Host permissions
|
|
82
|
-
if (manifest.host_permissions?.includes('<all_urls>')) {
|
|
83
|
-
warnings.push('Using <all_urls> requires strong justification');
|
|
84
|
-
}
|
|
85
|
-
// Service worker check
|
|
86
|
-
if (manifest.background?.service_worker) {
|
|
87
|
-
const swPath = path.join(extensionPath, manifest.background.service_worker);
|
|
88
|
-
if (!fs.existsSync(swPath)) {
|
|
89
|
-
errors.push(`Service worker file not found: ${manifest.background.service_worker}`);
|
|
90
|
-
manifestValid = false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// Suggestions
|
|
94
|
-
if (!manifest.icons?.['16'] || !manifest.icons?.['48']) {
|
|
95
|
-
suggestions.push('Consider adding 16x16 and 48x48 icons');
|
|
96
|
-
}
|
|
97
|
-
// Display results
|
|
98
|
-
if (manifestValid) {
|
|
99
|
-
response.appendResponseLine('✅ Manifest is valid');
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
response.appendResponseLine('❌ Manifest has errors');
|
|
103
|
-
}
|
|
104
|
-
if (errors.length > 0) {
|
|
105
|
-
response.appendResponseLine('\n**Errors:**');
|
|
106
|
-
errors.forEach(err => response.appendResponseLine(`- ❌ ${err}`));
|
|
107
|
-
}
|
|
108
|
-
if (warnings.length > 0) {
|
|
109
|
-
response.appendResponseLine('\n**Warnings:**');
|
|
110
|
-
warnings.forEach(warn => response.appendResponseLine(`- ⚠️ ${warn}`));
|
|
111
|
-
}
|
|
112
|
-
if (suggestions.length > 0) {
|
|
113
|
-
response.appendResponseLine('\n**Suggestions:**');
|
|
114
|
-
suggestions.forEach(sug => response.appendResponseLine(`- 💡 ${sug}`));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
response.appendResponseLine(`❌ Failed to parse manifest: ${error}`);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (!manifestValid) {
|
|
122
|
-
response.appendResponseLine('');
|
|
123
|
-
response.appendResponseLine('❌ **Submission blocked:** Fix manifest errors first');
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
response.appendResponseLine('');
|
|
127
|
-
// Step 2: Generate store listing
|
|
128
|
-
response.appendResponseLine('**Step 2: Generating store listing...**');
|
|
129
|
-
let description = `${manifest.name} is a Chrome extension that ${manifest.description || 'enhances your browsing experience'}.\n\n`;
|
|
130
|
-
description += '## Features\n\n';
|
|
131
|
-
// Infer features from permissions
|
|
132
|
-
if (manifest.permissions?.includes('tabs')) {
|
|
133
|
-
description += '• Manage and organize your browser tabs\n';
|
|
134
|
-
}
|
|
135
|
-
if (manifest.permissions?.includes('storage')) {
|
|
136
|
-
description += '• Save your preferences and settings\n';
|
|
137
|
-
}
|
|
138
|
-
if (manifest.permissions?.includes('notifications')) {
|
|
139
|
-
description += '• Receive helpful notifications\n';
|
|
140
|
-
}
|
|
141
|
-
if (manifest.content_scripts) {
|
|
142
|
-
description += '• Enhance website functionality\n';
|
|
143
|
-
}
|
|
144
|
-
if (manifest.action?.default_popup) {
|
|
145
|
-
description += '• Quick access from toolbar\n';
|
|
146
|
-
}
|
|
147
|
-
// Guess category
|
|
148
|
-
const { permissions = [], host_permissions = [] } = manifest;
|
|
149
|
-
let category = 'Productivity'; // Default
|
|
150
|
-
if (permissions.includes('tabs') || permissions.includes('bookmarks')) {
|
|
151
|
-
category = 'Productivity';
|
|
152
|
-
}
|
|
153
|
-
else if (permissions.includes('downloads')) {
|
|
154
|
-
category = 'Tools';
|
|
155
|
-
}
|
|
156
|
-
else if (host_permissions.some(h => h.includes('youtube') || h.includes('video'))) {
|
|
157
|
-
category = 'Entertainment';
|
|
158
|
-
}
|
|
159
|
-
else if (host_permissions.some(h => h.includes('facebook') || h.includes('twitter'))) {
|
|
160
|
-
category = 'Social & Communication';
|
|
161
|
-
}
|
|
162
|
-
response.appendResponseLine(`**Name:** ${manifest.name}`);
|
|
163
|
-
response.appendResponseLine(`**Summary:** ${manifest.description || `${manifest.name} - Chrome Extension`}`);
|
|
164
|
-
response.appendResponseLine(`**Category:** ${category}`);
|
|
165
|
-
response.appendResponseLine('');
|
|
166
|
-
response.appendResponseLine('**Generated Description Preview:**');
|
|
167
|
-
response.appendResponseLine(description.substring(0, 200) + '...');
|
|
168
|
-
response.appendResponseLine('');
|
|
169
|
-
// Step 3: Create submission package
|
|
170
|
-
response.appendResponseLine('**Step 3: Creating submission package...**');
|
|
171
|
-
try {
|
|
172
|
-
await new Promise((resolve, reject) => {
|
|
173
|
-
const output = fs.createWriteStream(outputPath);
|
|
174
|
-
const archive = archiver('zip', {
|
|
175
|
-
zlib: { level: 9 } // Maximum compression
|
|
176
|
-
});
|
|
177
|
-
output.on('close', () => {
|
|
178
|
-
const sizeKB = (archive.pointer() / 1024).toFixed(2);
|
|
179
|
-
response.appendResponseLine(`📦 Package created: ${outputPath}`);
|
|
180
|
-
response.appendResponseLine(` Size: ${sizeKB} KB`);
|
|
181
|
-
resolve();
|
|
182
|
-
});
|
|
183
|
-
archive.on('error', (err) => {
|
|
184
|
-
response.appendResponseLine(`❌ Failed to create package: ${err}`);
|
|
185
|
-
reject(err);
|
|
186
|
-
});
|
|
187
|
-
archive.pipe(output);
|
|
188
|
-
// Add extension files, excluding unnecessary ones
|
|
189
|
-
archive.glob('**/*', {
|
|
190
|
-
cwd: extensionPath,
|
|
191
|
-
ignore: [
|
|
192
|
-
'node_modules/**',
|
|
193
|
-
'.git/**',
|
|
194
|
-
'.gitignore',
|
|
195
|
-
'*.map',
|
|
196
|
-
'.DS_Store',
|
|
197
|
-
'Thumbs.db',
|
|
198
|
-
'*.log',
|
|
199
|
-
'test/**',
|
|
200
|
-
'tests/**',
|
|
201
|
-
'docs/**',
|
|
202
|
-
],
|
|
203
|
-
});
|
|
204
|
-
archive.finalize();
|
|
205
|
-
});
|
|
206
|
-
response.appendResponseLine('✅ Package created successfully!');
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
response.appendResponseLine(`❌ Failed to create package: ${error}`);
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
response.appendResponseLine('');
|
|
213
|
-
response.appendResponseLine('='.repeat(40));
|
|
214
|
-
response.appendResponseLine('**📋 Final Checklist:**');
|
|
215
|
-
response.appendResponseLine('');
|
|
216
|
-
response.appendResponseLine('Before submitting to Chrome Web Store:');
|
|
217
|
-
response.appendResponseLine('1. ✅ Manifest validated');
|
|
218
|
-
response.appendResponseLine('2. ✅ ZIP package created');
|
|
219
|
-
response.appendResponseLine('3. ⬜ Add screenshots (1280x800 recommended)');
|
|
220
|
-
response.appendResponseLine('4. ⬜ Add promotional images');
|
|
221
|
-
response.appendResponseLine('5. ⬜ Write privacy policy (if needed)');
|
|
222
|
-
response.appendResponseLine('6. ⬜ Pay $5 developer registration fee (first time only)');
|
|
223
|
-
response.appendResponseLine('');
|
|
224
|
-
response.appendResponseLine('**Submit at:** https://chrome.google.com/webstore/devconsole');
|
|
225
|
-
// Step 4: Auto-submit via browser automation
|
|
226
|
-
if (request.params.autoSubmit) {
|
|
227
|
-
response.appendResponseLine('');
|
|
228
|
-
response.appendResponseLine('='.repeat(40));
|
|
229
|
-
response.appendResponseLine('**🤖 Step 4: Automated Browser Submission**');
|
|
230
|
-
response.appendResponseLine('');
|
|
231
|
-
const page = context.getSelectedPage();
|
|
232
|
-
try {
|
|
233
|
-
// Navigate to Chrome Web Store Developer Dashboard
|
|
234
|
-
response.appendResponseLine('Navigating to Developer Dashboard...');
|
|
235
|
-
await page.goto('https://chrome.google.com/webstore/devconsole', {
|
|
236
|
-
waitUntil: 'networkidle0',
|
|
237
|
-
});
|
|
238
|
-
// Check if user is logged in
|
|
239
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
240
|
-
const currentUrl = page.url();
|
|
241
|
-
if (currentUrl.includes('accounts.google.com')) {
|
|
242
|
-
response.appendResponseLine('⚠️ Login required. Please log in manually.');
|
|
243
|
-
response.appendResponseLine('After logging in, run this command again.');
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
// Check if this is a new submission or update
|
|
247
|
-
response.appendResponseLine('Checking for existing extensions...');
|
|
248
|
-
// Look for "Add new item" button
|
|
249
|
-
const addNewButton = await page.$('button[aria-label="Add new item"], a[href*="register"]');
|
|
250
|
-
if (addNewButton) {
|
|
251
|
-
response.appendResponseLine('Creating new extension submission...');
|
|
252
|
-
await addNewButton.click();
|
|
253
|
-
await page.waitForNavigation({ waitUntil: 'networkidle0' });
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
response.appendResponseLine('❌ Could not find "Add new item" button');
|
|
257
|
-
response.appendResponseLine('Please ensure you are on the Developer Dashboard');
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
// Upload the ZIP file
|
|
261
|
-
response.appendResponseLine('Uploading extension package...');
|
|
262
|
-
const fileInput = await page.$('input[type="file"]');
|
|
263
|
-
if (fileInput) {
|
|
264
|
-
await fileInput.uploadFile(outputPath);
|
|
265
|
-
response.appendResponseLine('✅ Package uploaded');
|
|
266
|
-
// Wait for processing
|
|
267
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
268
|
-
// Look for any errors
|
|
269
|
-
const errorElements = await page.$$('.error-message, [role="alert"]');
|
|
270
|
-
if (errorElements.length > 0) {
|
|
271
|
-
const errorText = await page.evaluate(() => {
|
|
272
|
-
const errors = document.querySelectorAll('.error-message, [role="alert"]');
|
|
273
|
-
return Array.from(errors).map(e => e.textContent).join('\n');
|
|
274
|
-
});
|
|
275
|
-
response.appendResponseLine(`⚠️ Upload errors detected: ${errorText}`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
response.appendResponseLine('❌ Could not find file upload input');
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
// Fill in store listing information
|
|
283
|
-
response.appendResponseLine('Filling in store listing...');
|
|
284
|
-
// Title field (usually pre-filled from manifest)
|
|
285
|
-
const titleInput = await page.$('input[name="title"], input[aria-label*="title"]');
|
|
286
|
-
if (titleInput) {
|
|
287
|
-
const currentTitle = await titleInput.evaluate(el => el.value);
|
|
288
|
-
if (!currentTitle) {
|
|
289
|
-
await titleInput.type(manifest.name);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
// Summary/Short description
|
|
293
|
-
const summaryInput = await page.$('textarea[name="summary"], textarea[aria-label*="summary"]');
|
|
294
|
-
if (summaryInput) {
|
|
295
|
-
await summaryInput.click({ clickCount: 3 }); // Select all
|
|
296
|
-
await summaryInput.type(manifest.description || `${manifest.name} - Chrome Extension`);
|
|
297
|
-
}
|
|
298
|
-
// Detailed description
|
|
299
|
-
const descriptionInput = await page.$('textarea[name="description"], textarea[aria-label*="description"]');
|
|
300
|
-
if (descriptionInput) {
|
|
301
|
-
await descriptionInput.click({ clickCount: 3 });
|
|
302
|
-
await descriptionInput.type(description);
|
|
303
|
-
}
|
|
304
|
-
// Category selection
|
|
305
|
-
const categorySelect = await page.$('select[name="category"], select[aria-label*="category"]');
|
|
306
|
-
if (categorySelect) {
|
|
307
|
-
await categorySelect.select(category.toLowerCase().replace(/\s+/g, '_'));
|
|
308
|
-
}
|
|
309
|
-
// Language
|
|
310
|
-
const languageSelect = await page.$('select[name="language"], select[aria-label*="language"]');
|
|
311
|
-
if (languageSelect) {
|
|
312
|
-
await languageSelect.select('en');
|
|
313
|
-
}
|
|
314
|
-
response.appendResponseLine('✅ Store listing filled');
|
|
315
|
-
// Screenshots reminder
|
|
316
|
-
response.appendResponseLine('');
|
|
317
|
-
response.appendResponseLine('⚠️ **Manual steps required:**');
|
|
318
|
-
response.appendResponseLine('1. Add at least 1 screenshot (1280x800 or 640x400)');
|
|
319
|
-
response.appendResponseLine('2. Add promotional images if needed');
|
|
320
|
-
response.appendResponseLine('3. Review all information');
|
|
321
|
-
response.appendResponseLine('4. Click "Save draft" or "Submit for review"');
|
|
322
|
-
response.appendResponseLine('');
|
|
323
|
-
response.appendResponseLine('The browser is now on the submission page.');
|
|
324
|
-
response.appendResponseLine('Complete the remaining steps manually.');
|
|
325
|
-
}
|
|
326
|
-
catch (error) {
|
|
327
|
-
response.appendResponseLine(`❌ Automation error: ${error}`);
|
|
328
|
-
response.appendResponseLine('You may need to complete the submission manually.');
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
},
|
|
332
|
-
});
|