@xcanwin/manyoyo 5.8.9 → 5.8.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.8.9",
3
+ "version": "5.8.11",
4
4
  "imageVersion": "1.9.0-common",
5
5
  "playwrightCliVersion": "0.1.1",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
@@ -1,116 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- function createPlaywrightBootstrapManager(options = {}) {
7
- const plugin = options.plugin;
8
- const isCliScene = options.isCliScene || (() => false);
9
-
10
- return {
11
- buildInitScriptContent() {
12
- const lines = [
13
- "'use strict';",
14
- '(function () {',
15
- ` const platformValue = ${JSON.stringify(plugin.config.navigatorPlatform)};`,
16
- ' try {',
17
- ' const navProto = Object.getPrototypeOf(navigator);',
18
- " Object.defineProperty(navProto, 'platform', {",
19
- ' configurable: true,',
20
- ' get: () => platformValue',
21
- ' });',
22
- ' } catch (_) {}'
23
- ];
24
-
25
- if (plugin.config.disableWebRTC) {
26
- lines.push(
27
- ' try {',
28
- ' const scope = globalThis;',
29
- " const blocked = ['RTCPeerConnection', 'webkitRTCPeerConnection', 'RTCIceCandidate', 'RTCRtpSender', 'RTCRtpReceiver', 'RTCRtpTransceiver', 'RTCDataChannel'];",
30
- ' for (const name of blocked) {',
31
- " Object.defineProperty(scope, name, { configurable: true, writable: true, value: undefined });",
32
- ' }',
33
- ' if (navigator.mediaDevices) {',
34
- ' const errorFactory = () => {',
35
- ' try {',
36
- " return new DOMException('WebRTC is disabled', 'NotAllowedError');",
37
- ' } catch (_) {',
38
- " const error = new Error('WebRTC is disabled');",
39
- " error.name = 'NotAllowedError';",
40
- ' return error;',
41
- ' }',
42
- ' };',
43
- " Object.defineProperty(navigator.mediaDevices, 'getUserMedia', {",
44
- ' configurable: true,',
45
- ' writable: true,',
46
- ' value: async () => { throw errorFactory(); }',
47
- ' });',
48
- ' }',
49
- ' } catch (_) {}'
50
- );
51
- }
52
-
53
- lines.push('})();', '');
54
- return lines.join('\n');
55
- },
56
- ensureSceneInitScript(sceneName) {
57
- const filePath = plugin.sceneInitScriptPath(sceneName);
58
- const content = this.buildInitScriptContent();
59
- fs.writeFileSync(filePath, content, 'utf8');
60
- return filePath;
61
- },
62
- defaultBrowserName(sceneName) {
63
- if (isCliScene(sceneName)) {
64
- return 'chromium';
65
- }
66
- const cfg = plugin.buildSceneConfig(sceneName);
67
- const browserName = cfg && cfg.browser && cfg.browser.browserName;
68
- return String(browserName || 'chromium');
69
- },
70
- ensureContainerScenePrerequisites(sceneName) {
71
- if (!plugin.sceneConfigMissing(sceneName)) {
72
- return;
73
- }
74
- const tag = String(plugin.config.dockerTag || 'latest').trim() || 'latest';
75
- const image = `mcr.microsoft.com/playwright/mcp:${tag}`;
76
- plugin.runCmd([plugin.config.containerRuntime, 'pull', image], { check: true });
77
- },
78
- ensureHostScenePrerequisites(sceneName) {
79
- if (!isCliScene(sceneName) && !plugin.sceneConfigMissing(sceneName)) {
80
- return;
81
- }
82
- plugin.runCmd([plugin.playwrightBinPath(sceneName), 'install', '--with-deps', plugin.defaultBrowserName(sceneName)], { check: true });
83
- },
84
- localBinPath(binName) {
85
- const filename = process.platform === 'win32' ? `${binName}.cmd` : binName;
86
- const binPath = path.join(plugin.projectRoot, 'node_modules', '.bin', filename);
87
- if (!fs.existsSync(binPath)) {
88
- throw new Error(`local binary not found: ${binPath}. Run npm install first.`);
89
- }
90
- return binPath;
91
- },
92
- playwrightBinPath(sceneName) {
93
- if (!isCliScene(sceneName)) {
94
- return plugin.localBinPath('playwright');
95
- }
96
-
97
- const filename = process.platform === 'win32' ? 'playwright.cmd' : 'playwright';
98
- const candidates = [
99
- path.join(plugin.projectRoot, 'node_modules', '@playwright', 'mcp', 'node_modules', '.bin', filename),
100
- path.join(plugin.projectRoot, 'node_modules', '.bin', filename)
101
- ];
102
-
103
- for (const candidate of candidates) {
104
- if (fs.existsSync(candidate)) {
105
- return candidate;
106
- }
107
- }
108
-
109
- throw new Error(`local binary not found for ${sceneName}. Run npm install first.`);
110
- }
111
- };
112
- }
113
-
114
- module.exports = {
115
- createPlaywrightBootstrapManager
116
- };
@@ -1,95 +0,0 @@
1
- 'use strict';
2
-
3
- const os = require('os');
4
-
5
- function createPlaywrightCommandOutputManager(options = {}) {
6
- const plugin = options.plugin;
7
- const isMcpScene = options.isMcpScene || (() => false);
8
- const playwrightCliVersion = options.playwrightCliVersion || '';
9
-
10
- return {
11
- detectCurrentIPv4() {
12
- const interfaces = os.networkInterfaces();
13
- for (const values of Object.values(interfaces)) {
14
- if (!Array.isArray(values)) {
15
- continue;
16
- }
17
- for (const item of values) {
18
- if (!item || item.internal) {
19
- continue;
20
- }
21
- if (item.family === 'IPv4') {
22
- return item.address;
23
- }
24
- }
25
- }
26
- return '';
27
- },
28
- resolveMcpAddHost(hostArg) {
29
- if (!hostArg) {
30
- return plugin.config.mcpDefaultHost;
31
- }
32
- const value = String(hostArg).trim();
33
- if (!value) {
34
- return '';
35
- }
36
- if (value === 'current-ip') {
37
- return this.detectCurrentIPv4();
38
- }
39
- return value;
40
- },
41
- printMcpAdd(hostArg) {
42
- const host = this.resolveMcpAddHost(hostArg);
43
- if (!host) {
44
- plugin.writeStderr('[mcp-add] failed: cannot determine host. Use --host <host> to set one explicitly.');
45
- return 1;
46
- }
47
-
48
- const scenes = plugin.resolveTargets('all').filter(sceneName => isMcpScene(sceneName));
49
- for (const sceneName of scenes) {
50
- const url = `http://${host}:${plugin.scenePort(sceneName)}/mcp`;
51
- plugin.writeStdout(`claude mcp add -t http -s user playwright-${sceneName} ${url}`);
52
- }
53
- plugin.writeStdout('');
54
- for (const sceneName of scenes) {
55
- const url = `http://${host}:${plugin.scenePort(sceneName)}/mcp`;
56
- plugin.writeStdout(`codex mcp add playwright-${sceneName} --url ${url}`);
57
- }
58
- plugin.writeStdout('');
59
- for (const sceneName of scenes) {
60
- const url = `http://${host}:${plugin.scenePort(sceneName)}/mcp`;
61
- plugin.writeStdout(`gemini mcp add -t http -s user playwright-${sceneName} ${url}`);
62
- }
63
-
64
- return 0;
65
- },
66
- printCliAdd() {
67
- const lines = [
68
- 'PLAYWRIGHT_CLI_INSTALL_DIR="${TMPDIR:-/tmp}/manyoyo-playwright-cli-install-$$"',
69
- 'mkdir -p "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright"',
70
- 'echo \'{"browser":{"browserName":"chromium","launchOptions":{"channel":"chromium"}}}\' > "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright/cli.config.json"',
71
- 'cd "$PLAYWRIGHT_CLI_INSTALL_DIR"',
72
- `npm install -g @playwright/cli@${playwrightCliVersion}`,
73
- 'playwright-cli install --skills',
74
- 'PLAYWRIGHT_CLI_SKILL_SOURCE="$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli"',
75
- 'for target in ~/.claude/skills/playwright-cli ~/.codex/skills/playwright-cli ~/.gemini/skills/playwright-cli; do',
76
- ' mkdir -p "$target"',
77
- ' cp -R "$PLAYWRIGHT_CLI_SKILL_SOURCE/." "$target/"',
78
- 'done',
79
- 'cd "$OLDPWD"',
80
- 'rm -rf "$PLAYWRIGHT_CLI_INSTALL_DIR"'
81
- ];
82
- plugin.writeStdout(lines.join('\n'));
83
- return 0;
84
- },
85
- printSummary() {
86
- const scenes = plugin.resolveTargets('all');
87
- plugin.writeStdout(`playwright\truntime=${plugin.config.runtime}\tscenes=${scenes.join(',')}`);
88
- return 0;
89
- }
90
- };
91
- }
92
-
93
- module.exports = {
94
- createPlaywrightCommandOutputManager
95
- };
@@ -1,94 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- function createPlaywrightContainerRuntimeManager(options = {}) {
7
- const plugin = options.plugin;
8
- const sceneDefs = options.sceneDefs || {};
9
-
10
- return {
11
- containerEnv(sceneName, cfgPath, actionOptions = {}) {
12
- const def = sceneDefs[sceneName];
13
- const requireVncPassword = actionOptions.requireVncPassword === true;
14
- const env = {
15
- ...process.env,
16
- PLAYWRIGHT_MCP_DOCKER_TAG: plugin.config.dockerTag,
17
- PLAYWRIGHT_MCP_PORT: String(plugin.scenePort(sceneName)),
18
- PLAYWRIGHT_MCP_CONFIG_PATH: cfgPath,
19
- PLAYWRIGHT_MCP_CONTAINER_NAME: def.containerName,
20
- PLAYWRIGHT_MCP_IMAGE: plugin.config.headedImage,
21
- PLAYWRIGHT_MCP_NOVNC_PORT: String(plugin.config.ports.mcpContHeadedNoVnc)
22
- };
23
-
24
- if (sceneName === 'mcp-cont-headed') {
25
- const envKey = plugin.config.vncPasswordEnvKey;
26
- let password = process.env[envKey];
27
- if (!password) {
28
- password = plugin.randomAlnum(16);
29
- if (requireVncPassword) {
30
- plugin.writeStdout(`[up] mcp-cont-headed ${envKey} not set; generated random 16-char password: ${password}`);
31
- }
32
- }
33
- env.VNC_PASSWORD = password;
34
- }
35
-
36
- return env;
37
- },
38
- containerComposePath(sceneName) {
39
- const def = sceneDefs[sceneName];
40
- return path.join(plugin.config.composeDir, def.composeFile);
41
- },
42
- sceneComposeOverridePath(sceneName) {
43
- return path.join(plugin.config.runDir, `${sceneName}.compose.override.yaml`);
44
- },
45
- ensureContainerComposeOverride(sceneName, volumeMounts = []) {
46
- const overridePath = this.sceneComposeOverridePath(sceneName);
47
- if (!Array.isArray(volumeMounts) || volumeMounts.length === 0) {
48
- fs.rmSync(overridePath, { force: true });
49
- return '';
50
- }
51
-
52
- fs.mkdirSync(plugin.config.runDir, { recursive: true });
53
- const lines = [
54
- 'services:',
55
- ' playwright:',
56
- ' volumes:'
57
- ];
58
- volumeMounts.forEach(item => {
59
- lines.push(` - ${JSON.stringify(String(item))}`);
60
- });
61
- fs.writeFileSync(overridePath, `${lines.join('\n')}\n`, 'utf8');
62
- return overridePath;
63
- },
64
- ensureContainerRuntimeAvailable(action, sceneName) {
65
- const runtime = plugin.config.containerRuntime;
66
- if (!plugin.ensureCommandAvailable(runtime)) {
67
- plugin.writeStderr(`[${action}] ${sceneName} failed: ${runtime} command not found.`);
68
- return '';
69
- }
70
- return runtime;
71
- },
72
- buildContainerComposeCommand(sceneName, composeFiles = [], trailingArgs = []) {
73
- const def = sceneDefs[sceneName];
74
- const files = Array.isArray(composeFiles) && composeFiles.length > 0
75
- ? composeFiles
76
- : [this.containerComposePath(sceneName)];
77
- const args = [
78
- plugin.config.containerRuntime,
79
- 'compose',
80
- '-p',
81
- def.projectName
82
- ];
83
- files.forEach(filePath => {
84
- args.push('-f', filePath);
85
- });
86
- args.push(...trailingArgs);
87
- return args;
88
- }
89
- };
90
- }
91
-
92
- module.exports = {
93
- createPlaywrightContainerRuntimeManager
94
- };
@@ -1,265 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
- const { spawnSync } = require('child_process');
7
-
8
- function sleep(ms) {
9
- return new Promise(resolve => setTimeout(resolve, ms));
10
- }
11
-
12
- function isHostPermission(value) {
13
- if (value === '<all_urls>') {
14
- return true;
15
- }
16
- return /^(?:\*|http|https|file|ftp):\/\//.test(value);
17
- }
18
-
19
- function scriptSourcesFromHtml(htmlFile) {
20
- const content = fs.readFileSync(htmlFile, { encoding: 'utf8' });
21
- const scripts = [...content.matchAll(/<script[^>]+src=["']([^"']+)["']/gi)].map(m => m[1]);
22
- return scripts.filter(src => !/^(?:https?:)?\/\//.test(src));
23
- }
24
-
25
- function convertManifestV2ToV3(extDir) {
26
- const manifestFile = path.join(extDir, 'manifest.json');
27
- const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
28
- if (manifest.manifest_version !== 2) {
29
- return false;
30
- }
31
-
32
- manifest.manifest_version = 3;
33
-
34
- if (manifest.browser_action && !manifest.action) {
35
- manifest.action = manifest.browser_action;
36
- delete manifest.browser_action;
37
- }
38
- if (manifest.page_action && !manifest.action) {
39
- manifest.action = manifest.page_action;
40
- delete manifest.page_action;
41
- }
42
-
43
- const background = manifest.background;
44
- if (background && typeof background === 'object' && !Array.isArray(background)) {
45
- let scripts = [];
46
- if (Array.isArray(background.scripts)) {
47
- scripts = background.scripts.filter(s => typeof s === 'string');
48
- } else if (typeof background.page === 'string') {
49
- const pagePath = path.join(extDir, background.page);
50
- if (fs.existsSync(pagePath)) {
51
- scripts = scriptSourcesFromHtml(pagePath);
52
- }
53
- }
54
-
55
- if (scripts.length > 0) {
56
- const swName = 'generated_background_sw.js';
57
- const swFile = path.join(extDir, swName);
58
- const swLines = [
59
- '// Auto-generated by manyoyo playwright ext-download for MV3.',
60
- `importScripts(${scripts.map(s => JSON.stringify(s)).join(', ')});`,
61
- ''
62
- ];
63
- fs.writeFileSync(swFile, swLines.join('\n'), 'utf8');
64
- manifest.background = { service_worker: swName };
65
- } else {
66
- delete manifest.background;
67
- }
68
- }
69
-
70
- if (typeof manifest.content_security_policy === 'string') {
71
- manifest.content_security_policy = { extension_pages: manifest.content_security_policy };
72
- }
73
-
74
- if (Array.isArray(manifest.permissions)) {
75
- const hostPermissions = Array.isArray(manifest.host_permissions) ? [...manifest.host_permissions] : [];
76
- const keptPermissions = [];
77
-
78
- for (const perm of manifest.permissions) {
79
- if (typeof perm === 'string' && isHostPermission(perm)) {
80
- if (!hostPermissions.includes(perm)) {
81
- hostPermissions.push(perm);
82
- }
83
- } else {
84
- keptPermissions.push(perm);
85
- }
86
- }
87
-
88
- manifest.permissions = keptPermissions;
89
- if (hostPermissions.length > 0) {
90
- manifest.host_permissions = hostPermissions;
91
- }
92
- }
93
-
94
- const war = manifest.web_accessible_resources;
95
- if (Array.isArray(war) && war.length > 0 && war.every(v => typeof v === 'string')) {
96
- manifest.web_accessible_resources = [
97
- {
98
- resources: war,
99
- matches: ['<all_urls>']
100
- }
101
- ];
102
- }
103
-
104
- fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
105
- return true;
106
- }
107
-
108
- function buildCrxUrl(extId, prodversion) {
109
- return (
110
- 'https://clients2.google.com/service/update2/crx' +
111
- `?response=redirect&prodversion=${prodversion}` +
112
- '&acceptformat=crx2,crx3' +
113
- `&x=id%3D${extId}%26installsource%3Dondemand%26uc`
114
- );
115
- }
116
-
117
- function crxZipOffset(data) {
118
- if (data.subarray(0, 4).toString('ascii') !== 'Cr24') {
119
- throw new Error('not a CRX file');
120
- }
121
-
122
- const version = data.readUInt32LE(4);
123
- if (version === 2) {
124
- const pubLen = data.readUInt32LE(8);
125
- const sigLen = data.readUInt32LE(12);
126
- return 16 + pubLen + sigLen;
127
- }
128
- if (version === 3) {
129
- const headerLen = data.readUInt32LE(8);
130
- return 12 + headerLen;
131
- }
132
- throw new Error(`unsupported CRX version: ${version}`);
133
- }
134
-
135
- class PlaywrightExtensionManager {
136
- constructor(options = {}) {
137
- this.extensions = Array.isArray(options.extensions) ? options.extensions : [];
138
- this.ensureCommandAvailable = options.ensureCommandAvailable;
139
- this.runCmd = options.runCmd;
140
- this.writeStdout = options.writeStdout;
141
- this.writeStderr = options.writeStderr;
142
- this.extensionDirPath = options.extensionDirPath;
143
- this.extensionTmpDirPath = options.extensionTmpDirPath;
144
- this.defaultProdversion = String(options.defaultProdversion || '132.0.0.0').trim();
145
- }
146
-
147
- async downloadFile(url, output, retries = 3, timeoutMs = 60_000) {
148
- const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
149
- if (!this.ensureCommandAvailable('curl')) {
150
- throw new Error('curl command not found');
151
- }
152
-
153
- let lastError = null;
154
- for (let i = 1; i <= retries; i += 1) {
155
- try {
156
- const result = this.runCmd([
157
- 'curl',
158
- '--fail',
159
- '--location',
160
- '--silent',
161
- '--show-error',
162
- '--connect-timeout',
163
- String(timeoutSec),
164
- '--max-time',
165
- String(timeoutSec),
166
- '--output',
167
- output,
168
- url
169
- ], { captureOutput: true, check: false });
170
- if (result.returncode !== 0) {
171
- throw new Error(result.stderr || `curl failed with exit code ${result.returncode}`);
172
- }
173
- return;
174
- } catch (error) {
175
- lastError = error;
176
- if (i < retries) {
177
- // eslint-disable-next-line no-await-in-loop
178
- await sleep(1000);
179
- }
180
- }
181
- }
182
-
183
- throw new Error(`download failed after ${retries} attempts: ${url}; ${String(lastError)}`);
184
- }
185
-
186
- extractZipBuffer(zipBuffer, outDir) {
187
- fs.mkdirSync(outDir, { recursive: true });
188
- const tempZip = path.join(os.tmpdir(), `manyoyo-playwright-ext-${process.pid}-${Date.now()}.zip`);
189
- fs.writeFileSync(tempZip, zipBuffer);
190
-
191
- const result = spawnSync('unzip', ['-oq', tempZip, '-d', outDir], { encoding: 'utf8' });
192
- fs.rmSync(tempZip, { force: true });
193
-
194
- if (result.error) {
195
- throw result.error;
196
- }
197
- if (result.status !== 0) {
198
- throw new Error(result.stderr || `unzip failed with exit code ${result.status}`);
199
- }
200
- }
201
-
202
- extractCrx(crxFile, outDir) {
203
- const data = fs.readFileSync(crxFile);
204
- const offset = crxZipOffset(data);
205
- const zipBuffer = data.subarray(offset);
206
-
207
- this.extractZipBuffer(zipBuffer, outDir);
208
-
209
- const manifest = path.join(outDir, 'manifest.json');
210
- if (!fs.existsSync(manifest)) {
211
- throw new Error(`${crxFile} extracted but manifest.json missing`);
212
- }
213
-
214
- if (convertManifestV2ToV3(outDir)) {
215
- this.writeStdout(`[manifest] upgraded to MV3: ${path.basename(outDir)}`);
216
- }
217
- }
218
-
219
- async downloadExtensions(options = {}) {
220
- if (!this.ensureCommandAvailable('unzip')) {
221
- this.writeStderr('[ext-download] failed: unzip command not found.');
222
- return 1;
223
- }
224
-
225
- const prodversion = String(options.prodversion || this.defaultProdversion).trim();
226
- const extDir = path.resolve(this.extensionDirPath());
227
- const tmpDir = path.resolve(this.extensionTmpDirPath());
228
-
229
- fs.rmSync(tmpDir, { recursive: true, force: true });
230
- fs.mkdirSync(extDir, { recursive: true });
231
- fs.mkdirSync(tmpDir, { recursive: true });
232
-
233
- try {
234
- this.writeStdout(`[info] ext dir: ${extDir}`);
235
- this.writeStdout(`[info] tmp dir: ${tmpDir}`);
236
-
237
- for (const [name, extId] of this.extensions) {
238
- const url = buildCrxUrl(extId, prodversion);
239
- const crxFile = path.join(tmpDir, `${name}.crx`);
240
- const outDir = path.join(extDir, name);
241
-
242
- this.writeStdout(`[download] ${name}`);
243
- // eslint-disable-next-line no-await-in-loop
244
- await this.downloadFile(url, crxFile);
245
-
246
- this.writeStdout(`[extract] ${name}`);
247
- fs.rmSync(outDir, { recursive: true, force: true });
248
- this.extractCrx(crxFile, outDir);
249
- }
250
- } finally {
251
- fs.rmSync(tmpDir, { recursive: true, force: true });
252
- this.writeStdout(`[cleanup] removed ${tmpDir}`);
253
- }
254
-
255
- this.writeStdout(`[done] all extensions are ready: ${extDir}`);
256
- return 0;
257
- }
258
- }
259
-
260
- module.exports = {
261
- buildCrxUrl,
262
- convertManifestV2ToV3,
263
- crxZipOffset,
264
- PlaywrightExtensionManager
265
- };
@@ -1,98 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- function createPlaywrightExtensionPathManager(options = {}) {
7
- const plugin = options.plugin;
8
- const asStringArray = options.asStringArray || ((value, fallback) => fallback);
9
- const containerExtensionRoot = options.containerExtensionRoot || '/app/extensions';
10
-
11
- return {
12
- resolveExtensionPaths(extensionArgs = []) {
13
- const inputs = asStringArray(extensionArgs, []);
14
- const uniquePaths = [];
15
- const seen = new Set();
16
-
17
- for (const item of inputs) {
18
- const absPath = path.resolve(item);
19
- if (!fs.existsSync(absPath)) {
20
- throw new Error(`扩展路径不存在: ${absPath}`);
21
- }
22
- const stat = fs.statSync(absPath);
23
- if (!stat.isDirectory()) {
24
- throw new Error(`扩展路径必须是目录: ${absPath}`);
25
- }
26
-
27
- const manifestPath = path.join(absPath, 'manifest.json');
28
- if (fs.existsSync(manifestPath)) {
29
- if (!seen.has(absPath)) {
30
- seen.add(absPath);
31
- uniquePaths.push(absPath);
32
- }
33
- continue;
34
- }
35
-
36
- const children = fs.readdirSync(absPath, { withFileTypes: true })
37
- .filter(dirent => dirent.isDirectory())
38
- .map(dirent => path.join(absPath, dirent.name))
39
- .filter(child => fs.existsSync(path.join(child, 'manifest.json')));
40
-
41
- if (children.length === 0) {
42
- throw new Error(`目录下未找到扩展(manifest.json): ${absPath}`);
43
- }
44
-
45
- for (const childPath of children) {
46
- if (!seen.has(childPath)) {
47
- seen.add(childPath);
48
- uniquePaths.push(childPath);
49
- }
50
- }
51
- }
52
-
53
- return uniquePaths;
54
- },
55
- resolveNamedExtensionPaths(extensionNames = []) {
56
- const names = asStringArray(extensionNames, []);
57
- const extensionRoot = path.resolve(plugin.extensionDirPath());
58
-
59
- return names.map(name => {
60
- if (name.includes('/') || name.includes('\\') || name === '.' || name === '..') {
61
- throw new Error(`扩展名称无效: ${name}`);
62
- }
63
- return path.join(extensionRoot, name);
64
- });
65
- },
66
- resolveExtensionInputs(inputOptions = {}) {
67
- const extensionPaths = asStringArray(inputOptions.extensionPaths, []);
68
- const namedPaths = this.resolveNamedExtensionPaths(inputOptions.extensionNames || []);
69
- return this.resolveExtensionPaths([...extensionPaths, ...namedPaths]);
70
- },
71
- sanitizeExtensionMountName(value) {
72
- const sanitized = String(value || '')
73
- .trim()
74
- .replace(/[^A-Za-z0-9._-]/g, '-')
75
- .replace(/-+/g, '-')
76
- .replace(/^-|-$/g, '');
77
- return sanitized || 'ext';
78
- },
79
- buildContainerExtensionMounts(extensionPaths = []) {
80
- const hostPaths = asStringArray(extensionPaths, []);
81
- const containerPaths = [];
82
- const volumeMounts = [];
83
-
84
- hostPaths.forEach((hostPath, idx) => {
85
- const safeName = this.sanitizeExtensionMountName(path.basename(hostPath));
86
- const containerPath = path.posix.join(containerExtensionRoot, `ext-${idx + 1}-${safeName}`);
87
- containerPaths.push(containerPath);
88
- volumeMounts.push(`${hostPath}:${containerPath}:ro`);
89
- });
90
-
91
- return { containerPaths, volumeMounts };
92
- }
93
- };
94
- }
95
-
96
- module.exports = {
97
- createPlaywrightExtensionPathManager
98
- };