claude-coder 1.9.0 → 1.9.2

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.
Files changed (74) hide show
  1. package/README.md +214 -214
  2. package/bin/cli.js +155 -155
  3. package/package.json +55 -55
  4. package/recipes/_shared/roles/developer.md +11 -11
  5. package/recipes/_shared/roles/product.md +12 -12
  6. package/recipes/_shared/roles/tester.md +12 -12
  7. package/recipes/_shared/test/report-format.md +86 -86
  8. package/recipes/backend/base.md +27 -27
  9. package/recipes/backend/components/auth.md +18 -18
  10. package/recipes/backend/components/crud-api.md +18 -18
  11. package/recipes/backend/components/file-service.md +15 -15
  12. package/recipes/backend/manifest.json +20 -20
  13. package/recipes/backend/test/api-test.md +25 -25
  14. package/recipes/console/base.md +37 -37
  15. package/recipes/console/components/modal-form.md +20 -20
  16. package/recipes/console/components/pagination.md +17 -17
  17. package/recipes/console/components/search.md +17 -17
  18. package/recipes/console/components/table-list.md +18 -18
  19. package/recipes/console/components/tabs.md +14 -14
  20. package/recipes/console/components/tree.md +15 -15
  21. package/recipes/console/components/upload.md +15 -15
  22. package/recipes/console/manifest.json +24 -24
  23. package/recipes/console/test/crud-e2e.md +47 -47
  24. package/recipes/h5/base.md +26 -26
  25. package/recipes/h5/components/animation.md +11 -11
  26. package/recipes/h5/components/countdown.md +11 -11
  27. package/recipes/h5/components/share.md +11 -11
  28. package/recipes/h5/components/swiper.md +11 -11
  29. package/recipes/h5/manifest.json +21 -21
  30. package/recipes/h5/test/h5-e2e.md +20 -20
  31. package/src/commands/auth.js +420 -362
  32. package/src/commands/setup-modules/helpers.js +100 -100
  33. package/src/commands/setup-modules/index.js +25 -25
  34. package/src/commands/setup-modules/mcp.js +115 -115
  35. package/src/commands/setup-modules/provider.js +260 -260
  36. package/src/commands/setup-modules/safety.js +47 -47
  37. package/src/commands/setup-modules/simplify.js +52 -52
  38. package/src/commands/setup.js +172 -172
  39. package/src/common/assets.js +245 -245
  40. package/src/common/config.js +125 -125
  41. package/src/common/constants.js +55 -55
  42. package/src/common/indicator.js +260 -260
  43. package/src/common/interaction.js +170 -170
  44. package/src/common/logging.js +77 -77
  45. package/src/common/sdk.js +50 -50
  46. package/src/common/tasks.js +88 -88
  47. package/src/common/utils.js +213 -213
  48. package/src/core/coding.js +33 -33
  49. package/src/core/go.js +264 -264
  50. package/src/core/hooks.js +500 -500
  51. package/src/core/init.js +166 -165
  52. package/src/core/plan.js +188 -187
  53. package/src/core/prompts.js +247 -247
  54. package/src/core/repair.js +36 -36
  55. package/src/core/runner.js +471 -458
  56. package/src/core/scan.js +93 -93
  57. package/src/core/session.js +280 -271
  58. package/src/core/simplify.js +74 -74
  59. package/src/core/state.js +105 -105
  60. package/src/index.js +76 -76
  61. package/templates/bash-process.md +12 -12
  62. package/templates/codingSystem.md +65 -65
  63. package/templates/codingUser.md +17 -17
  64. package/templates/coreProtocol.md +29 -29
  65. package/templates/goSystem.md +130 -130
  66. package/templates/guidance.json +72 -72
  67. package/templates/planSystem.md +78 -78
  68. package/templates/planUser.md +8 -8
  69. package/templates/requirements.example.md +57 -57
  70. package/templates/scanSystem.md +120 -120
  71. package/templates/scanUser.md +10 -10
  72. package/templates/test_rule.md +194 -194
  73. package/templates/web-testing.md +17 -17
  74. package/types/index.d.ts +217 -217
@@ -1,362 +1,420 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
- const { execSync } = require('child_process');
7
- const { loadConfig, log } = require('../common/config');
8
- const { assets } = require('../common/assets');
9
- const { appendGitignore } = require('../common/utils');
10
-
11
- function resolvePlaywright() {
12
- const { createRequire } = require('module');
13
- const pkg = 'playwright';
14
-
15
- try {
16
- return path.dirname(require.resolve(`${pkg}/package.json`));
17
- } catch {}
18
-
19
- try {
20
- const r = createRequire(path.join(process.cwd(), 'noop.js'));
21
- return path.dirname(r.resolve(`${pkg}/package.json`));
22
- } catch {}
23
-
24
- try {
25
- const globalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
26
- const pkgJsonPath = path.join(globalRoot, pkg, 'package.json');
27
- if (fs.existsSync(pkgJsonPath)) return path.join(globalRoot, pkg);
28
- } catch {}
29
-
30
- return null;
31
- }
32
-
33
- function normalizeUrl(url) {
34
- if (!url) return null;
35
- return /^https?:\/\//.test(url) ? url : `http://${url}`;
36
- }
37
-
38
- function updateGitignore(entry) {
39
- if (appendGitignore(assets.projectRoot, entry)) {
40
- log('ok', `.gitignore 已添加: ${entry}`);
41
- }
42
- }
43
-
44
- // ─────────────────────────────────────────────────────────────
45
- // .mcp.json 配置(Playwright / Chrome DevTools 共用)
46
- // ─────────────────────────────────────────────────────────────
47
-
48
- function updateMcpConfig(mcpPath, tool, mode) {
49
- let mcpConfig = {};
50
- if (fs.existsSync(mcpPath)) {
51
- try { mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8')); } catch {}
52
- }
53
-
54
- if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
55
-
56
- if (tool === 'chrome-devtools') {
57
- delete mcpConfig.mcpServers.playwright;
58
- mcpConfig.mcpServers['chrome-devtools'] = {
59
- command: 'npx',
60
- args: ['-y', 'chrome-devtools-mcp@latest', '--autoConnect'],
61
- };
62
- fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
63
- log('ok', '.mcp.json 已配置 Chrome DevTools MCP (autoConnect)');
64
- return;
65
- }
66
-
67
- // Playwright MCP
68
- delete mcpConfig.mcpServers['chrome-devtools'];
69
- const args = ['@playwright/mcp@latest'];
70
- const projectRoot = assets.projectRoot;
71
-
72
- switch (mode) {
73
- case 'persistent': {
74
- const browserProfilePath = assets.path('browserProfile');
75
- const relProfile = path.relative(projectRoot, browserProfilePath).split(path.sep).join('/');
76
- args.push(`--user-data-dir=${relProfile}`);
77
- break;
78
- }
79
- case 'isolated': {
80
- const playwrightAuthPath = assets.path('playwrightAuth');
81
- const relAuth = path.relative(projectRoot, playwrightAuthPath).split(path.sep).join('/');
82
- args.push('--isolated', `--storage-state=${relAuth}`);
83
- break;
84
- }
85
- case 'extension':
86
- args.push('--extension');
87
- break;
88
- }
89
-
90
- mcpConfig.mcpServers.playwright = { command: 'npx', args };
91
- fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
92
- log('ok', `.mcp.json 已配置 Playwright MCP (${mode} 模式)`);
93
- }
94
-
95
- function enableWebTestEnv(tool) {
96
- const envPath = assets.path('env');
97
- if (!envPath || !fs.existsSync(envPath)) return;
98
-
99
- let content = fs.readFileSync(envPath, 'utf8');
100
- if (/^WEB_TEST_TOOL=/m.test(content)) {
101
- content = content.replace(/^WEB_TEST_TOOL=.*/m, `WEB_TEST_TOOL=${tool}`);
102
- } else {
103
- const suffix = content.endsWith('\n') ? '' : '\n';
104
- content += `${suffix}WEB_TEST_TOOL=${tool}\n`;
105
- }
106
- fs.writeFileSync(envPath, content, 'utf8');
107
- log('ok', `.claude-coder/.env 已设置 WEB_TEST_TOOL=${tool}`);
108
- }
109
-
110
- // ─────────────────────────────────────────────────────────────
111
- // 浏览器脚本(Playwright persistent 模式用)
112
- // ─────────────────────────────────────────────────────────────
113
-
114
- function buildBrowserScript(playwrightDir, profileDir, url) {
115
- const thirtyDays = Math.floor(Date.now() / 1000) + 86400 * 30;
116
- return [
117
- `const { chromium } = require(${JSON.stringify(playwrightDir)});`,
118
- `(async () => {`,
119
- ` const ctx = await chromium.launchPersistentContext(${JSON.stringify(profileDir)}, { headless: false });`,
120
- ` const page = ctx.pages()[0] || await ctx.newPage();`,
121
- ` try { await page.goto(${JSON.stringify(url)}); } catch {}`,
122
- ` console.log('请在浏览器中完成操作后关闭窗口...');`,
123
- ` const persistSessionCookies = async () => {`,
124
- ` try {`,
125
- ` const cookies = await ctx.cookies();`,
126
- ` const session = cookies.filter(c => c.expires === -1);`,
127
- ` if (session.length > 0) {`,
128
- ` await ctx.addCookies(session.map(c => ({ ...c, expires: ${thirtyDays} })));`,
129
- ` console.log('已将 ' + session.length + ' session cookie 转为持久化');`,
130
- ` }`,
131
- ` } catch {}`,
132
- ` };`,
133
- ` ctx.on('page', p => p.on('close', () => persistSessionCookies()));`,
134
- ` for (const p of ctx.pages()) p.on('close', () => persistSessionCookies());`,
135
- ` await new Promise(r => {`,
136
- ` ctx.on('close', r);`,
137
- ` const t = setInterval(async () => {`,
138
- ` try {`,
139
- ` if (!ctx.pages().length) { clearInterval(t); await persistSessionCookies(); r(); }`,
140
- ` } catch { clearInterval(t); r(); }`,
141
- ` }, 2000);`,
142
- ` });`,
143
- ` try { await ctx.close(); } catch {}`,
144
- `})().then(() => process.exit(0)).catch(() => process.exit(0));`,
145
- ].join('\n');
146
- }
147
-
148
- function runBrowserScript(script, cwd) {
149
- const tmpScript = path.join(os.tmpdir(), `pw-auth-${Date.now()}.js`);
150
- fs.writeFileSync(tmpScript, script);
151
- try {
152
- execSync(`node "${tmpScript}"`, { stdio: 'inherit', cwd });
153
- return true;
154
- } catch {
155
- return false;
156
- } finally {
157
- try { fs.unlinkSync(tmpScript); } catch {}
158
- }
159
- }
160
-
161
- // ─────────────────────────────────────────────────────────────
162
- // auth 模式实现
163
- // ─────────────────────────────────────────────────────────────
164
-
165
- async function authPersistent(url) {
166
- const playwrightDir = resolvePlaywright();
167
- if (!playwrightDir) {
168
- log('error', '未找到 playwright 模块');
169
- log('info', '请安装: npm install -g playwright && npx playwright install chromium');
170
- return;
171
- }
172
-
173
- const profileDir = assets.path('browserProfile');
174
- if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
175
-
176
- const lockFile = path.join(profileDir, 'SingletonLock');
177
- if (fs.existsSync(lockFile)) {
178
- fs.unlinkSync(lockFile);
179
- log('warn', '已清理残留的 SingletonLock(上次浏览器未正常关闭)');
180
- }
181
-
182
- console.log('操作步骤:');
183
- console.log(' 1. 浏览器将自动打开,请手动完成登录');
184
- console.log(' 2. 登录成功后关闭浏览器窗口');
185
- console.log(' 3. 登录状态将保存在持久化配置中(session cookie 自动转持久化)');
186
- console.log(' 4. MCP 后续会话自动复用此登录状态');
187
- console.log('');
188
-
189
- const script = buildBrowserScript(playwrightDir, profileDir, url);
190
- const projectRoot = assets.projectRoot;
191
-
192
- const ok = runBrowserScript(script, projectRoot);
193
- if (!ok) {
194
- const profileFiles = fs.readdirSync(profileDir);
195
- if (profileFiles.length <= 2) {
196
- log('error', 'Playwright 启动失败,且未检测到有效的浏览器配置');
197
- log('info', '请确保已安装 Chromium: npx playwright install chromium');
198
- return;
199
- }
200
- log('warn', '浏览器退出码非零,但已检测到有效配置,继续...');
201
- }
202
-
203
- const mcpPath = assets.path('mcpConfig');
204
- log('ok', '登录状态已保存到持久化配置');
205
- updateMcpConfig(mcpPath, 'playwright', 'persistent');
206
- updateGitignore('.claude-coder/.runtime/browser-profile');
207
- enableWebTestEnv('playwright');
208
-
209
- console.log('');
210
- log('ok', '配置完成!');
211
- const relProfile = path.relative(projectRoot, profileDir);
212
- log('info', `MCP 使用 persistent 模式 (user-data-dir: ${relProfile})`);
213
- log('info', '验证: 再次运行 claude-coder auth <URL>,浏览器应直接进入已登录状态');
214
- }
215
-
216
- async function authIsolated(url) {
217
- const playwrightAuthPath = assets.path('playwrightAuth');
218
- const projectRoot = assets.projectRoot;
219
-
220
- console.log('操作步骤:');
221
- console.log(' 1. 浏览器将自动打开,请手动完成登录');
222
- console.log(' 2. 登录成功后关闭浏览器窗口');
223
- console.log(' 3. 登录状态(cookies + localStorage)将保存到 playwright-auth.json');
224
- console.log(' 4. MCP 每次会话自动从此文件加载初始状态');
225
- console.log('');
226
-
227
- try {
228
- execSync(
229
- `npx playwright codegen --save-storage="${playwrightAuthPath}" "${url}"`,
230
- { stdio: 'inherit', cwd: projectRoot }
231
- );
232
- } catch (err) {
233
- if (!fs.existsSync(playwrightAuthPath)) {
234
- log('error', `Playwright 登录状态导出失败: ${err.message}`);
235
- log('info', '请确保已安装: npx playwright install chromium');
236
- return;
237
- }
238
- }
239
-
240
- if (!fs.existsSync(playwrightAuthPath)) {
241
- log('error', '未检测到导出的登录状态文件');
242
- return;
243
- }
244
-
245
- const mcpPath = assets.path('mcpConfig');
246
- log('ok', '登录状态已保存到 playwright-auth.json');
247
- updateMcpConfig(mcpPath, 'playwright', 'isolated');
248
- updateGitignore('.claude-coder/playwright-auth.json');
249
- enableWebTestEnv('playwright');
250
-
251
- console.log('');
252
- log('ok', '配置完成!');
253
- log('info', 'MCP 使用 isolated 模式 (storage-state)');
254
- log('info', 'cookies localStorage 每次会话自动从 playwright-auth.json 加载');
255
- }
256
-
257
- function authExtension() {
258
- console.log('Extension 模式说明:');
259
- console.log('');
260
- console.log(' 此模式通过 Chrome 扩展连接到您正在运行的浏览器。');
261
- console.log(' MCP 将直接使用浏览器中已有的登录态和扩展。');
262
- console.log('');
263
- console.log(' 前置条件:');
264
- console.log(' 1. 安装 "Playwright MCP Bridge" Chrome/Edge 扩展');
265
- console.log(' https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm');
266
- console.log(' 2. 确保浏览器已启动且扩展已启用');
267
- console.log(' 3. 无需额外认证操作,您的浏览器登录态将自动可用');
268
- console.log('');
269
-
270
- const mcpPath = assets.path('mcpConfig');
271
- updateMcpConfig(mcpPath, 'playwright', 'extension');
272
- enableWebTestEnv('playwright');
273
-
274
- console.log('');
275
- log('ok', '配置完成!');
276
- log('info', 'MCP 使用 extension 模式(连接真实浏览器)');
277
- log('info', '确保 Chrome/Edge 已运行且 Playwright MCP Bridge 扩展已启用');
278
- }
279
-
280
- function authChromeDevTools() {
281
- console.log('Chrome DevTools MCP 配置:');
282
- console.log('');
283
- console.log(' 此模式通过 Chrome DevTools Protocol 连接到已打开的 Chrome 浏览器。');
284
- console.log(' 直接复用浏览器中已有的登录态、扩展和 DevTools 调试能力。');
285
- console.log('');
286
- console.log(' 前置条件:');
287
- console.log(' 1. Node.js v20.19+(npx 自动下载 chrome-devtools-mcp 包)');
288
- console.log(' 2. Chrome 144+ 版本');
289
- console.log(' 3. 打开 chrome://inspect/#remote-debugging 启用远程调试');
290
- console.log(' 4. 允许传入调试连接');
291
- console.log('');
292
- console.log(' 功能:');
293
- console.log(' - 输入自动化: 点击、输入、表单填写');
294
- console.log(' - 页面导航: 多页面管理、截图');
295
- console.log(' - 性能分析: Trace 录制、Core Web Vitals、Lighthouse 审计');
296
- console.log(' - 调试工具: Console 消息、网络请求检查、内存快照');
297
- console.log('');
298
- console.log(` 注意: 单实例限制,同一时间只能连接一个 Chrome 调试会话。`);
299
- console.log(` 如需多实例并行,请配置 Playwright MCP(claude-coder setup)。`);
300
- console.log('');
301
-
302
- const mcpPath = assets.path('mcpConfig');
303
- updateMcpConfig(mcpPath, 'chrome-devtools');
304
- enableWebTestEnv('chrome-devtools');
305
-
306
- console.log('');
307
- log('ok', '配置完成!');
308
- log('info', 'Chrome DevTools MCP 使用 autoConnect 模式');
309
- log('info', '确保 Chrome 已启动且已在 chrome://inspect 中启用远程调试');
310
- }
311
-
312
- async function auth(url) {
313
- assets.ensureDirs();
314
- const config = loadConfig();
315
- const tool = config.webTestTool;
316
-
317
- if (!tool) {
318
- log('error', '未配置浏览器测试工具');
319
- log('info', '请先运行 claude-coder setup 选择 Playwright MCP 或 Chrome DevTools MCP');
320
- return;
321
- }
322
-
323
- if (tool === 'chrome-devtools') {
324
- const [major, minor] = process.versions.node.split('.').map(Number);
325
- if (major < 20 || (major === 20 && minor < 19)) {
326
- log('warn', `当前 Node.js 版本 v${process.versions.node},Chrome DevTools MCP 要求 v20.19+`);
327
- log('info', 'nvm 用户请执行: nvm alias default 22 && nvm use 22');
328
- log('info', '升级后重新运行此命令');
329
- return;
330
- }
331
- log('info', '浏览器工具: Chrome DevTools MCP');
332
- console.log('');
333
- authChromeDevTools();
334
- return;
335
- }
336
-
337
- // Playwright MCP
338
- const mode = config.webTestMode;
339
- const targetUrl = normalizeUrl(url) || 'http://localhost:3000';
340
-
341
- log('info', `浏览器工具: Playwright MCP (${mode} 模式)`);
342
- log('info', `目标 URL: ${targetUrl}`);
343
- console.log('');
344
-
345
- switch (mode) {
346
- case 'persistent':
347
- await authPersistent(targetUrl);
348
- break;
349
- case 'isolated':
350
- await authIsolated(targetUrl);
351
- break;
352
- case 'extension':
353
- authExtension();
354
- break;
355
- default:
356
- log('error', `未知的 Playwright 模式: ${mode}`);
357
- log('info', '请运行 claude-coder setup 重新配置');
358
- return;
359
- }
360
- }
361
-
362
- module.exports = { auth, updateMcpConfig };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const http = require('http');
7
+ const { execSync } = require('child_process');
8
+ const { loadConfig, log } = require('../common/config');
9
+ const { assets } = require('../common/assets');
10
+ const { appendGitignore } = require('../common/utils');
11
+
12
+ function resolvePlaywright() {
13
+ const { createRequire } = require('module');
14
+ const pkg = 'playwright';
15
+
16
+ try {
17
+ return path.dirname(require.resolve(`${pkg}/package.json`));
18
+ } catch {}
19
+
20
+ try {
21
+ const r = createRequire(path.join(process.cwd(), 'noop.js'));
22
+ return path.dirname(r.resolve(`${pkg}/package.json`));
23
+ } catch {}
24
+
25
+ try {
26
+ const globalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
27
+ const pkgJsonPath = path.join(globalRoot, pkg, 'package.json');
28
+ if (fs.existsSync(pkgJsonPath)) return path.join(globalRoot, pkg);
29
+ } catch {}
30
+
31
+ return null;
32
+ }
33
+
34
+ function normalizeUrl(url) {
35
+ if (!url) return null;
36
+ return /^https?:\/\//.test(url) ? url : `http://${url}`;
37
+ }
38
+
39
+ function updateGitignore(entry) {
40
+ if (appendGitignore(assets.projectRoot, entry)) {
41
+ log('ok', `.gitignore 已添加: ${entry}`);
42
+ }
43
+ }
44
+
45
+ // ─────────────────────────────────────────────────────────────
46
+ // .mcp.json 配置(Playwright / Chrome DevTools 共用)
47
+ // ─────────────────────────────────────────────────────────────
48
+
49
+ function updateMcpConfig(mcpPath, tool, mode) {
50
+ let mcpConfig = {};
51
+ if (fs.existsSync(mcpPath)) {
52
+ try { mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8')); } catch {}
53
+ }
54
+
55
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
56
+
57
+ if (tool === 'chrome-devtools') {
58
+ delete mcpConfig.mcpServers.playwright;
59
+ mcpConfig.mcpServers['chrome-devtools'] = {
60
+ command: 'npx',
61
+ args: ['-y', 'chrome-devtools-mcp@latest', '--autoConnect'],
62
+ };
63
+ fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
64
+ log('ok', '.mcp.json 已配置 Chrome DevTools MCP (autoConnect)');
65
+ return;
66
+ }
67
+
68
+ // Playwright MCP
69
+ delete mcpConfig.mcpServers['chrome-devtools'];
70
+ const args = ['@playwright/mcp@latest'];
71
+ const projectRoot = assets.projectRoot;
72
+
73
+ switch (mode) {
74
+ case 'persistent': {
75
+ const browserProfilePath = assets.path('browserProfile');
76
+ const relProfile = path.relative(projectRoot, browserProfilePath).split(path.sep).join('/');
77
+ args.push(`--user-data-dir=${relProfile}`);
78
+ break;
79
+ }
80
+ case 'isolated': {
81
+ const playwrightAuthPath = assets.path('playwrightAuth');
82
+ const relAuth = path.relative(projectRoot, playwrightAuthPath).split(path.sep).join('/');
83
+ args.push('--isolated', `--storage-state=${relAuth}`);
84
+ break;
85
+ }
86
+ case 'extension':
87
+ args.push('--extension');
88
+ break;
89
+ }
90
+
91
+ mcpConfig.mcpServers.playwright = { command: 'npx', args };
92
+ fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
93
+ log('ok', `.mcp.json 已配置 Playwright MCP (${mode} 模式)`);
94
+ }
95
+
96
+ function enableWebTestEnv(tool) {
97
+ const envPath = assets.path('env');
98
+ if (!envPath || !fs.existsSync(envPath)) return;
99
+
100
+ let content = fs.readFileSync(envPath, 'utf8');
101
+ if (/^WEB_TEST_TOOL=/m.test(content)) {
102
+ content = content.replace(/^WEB_TEST_TOOL=.*/m, `WEB_TEST_TOOL=${tool}`);
103
+ } else {
104
+ const suffix = content.endsWith('\n') ? '' : '\n';
105
+ content += `${suffix}WEB_TEST_TOOL=${tool}\n`;
106
+ }
107
+ fs.writeFileSync(envPath, content, 'utf8');
108
+ log('ok', `.claude-coder/.env 已设置 WEB_TEST_TOOL=${tool}`);
109
+ }
110
+
111
+ // ─────────────────────────────────────────────────────────────
112
+ // 浏览器脚本(Playwright persistent 模式用)
113
+ // ─────────────────────────────────────────────────────────────
114
+
115
+ function buildBrowserScript(playwrightDir, profileDir, url) {
116
+ const thirtyDays = Math.floor(Date.now() / 1000) + 86400 * 30;
117
+ return [
118
+ `const { chromium } = require(${JSON.stringify(playwrightDir)});`,
119
+ `(async () => {`,
120
+ ` const ctx = await chromium.launchPersistentContext(${JSON.stringify(profileDir)}, { headless: false });`,
121
+ ` const page = ctx.pages()[0] || await ctx.newPage();`,
122
+ ` try { await page.goto(${JSON.stringify(url)}); } catch {}`,
123
+ ` console.log('请在浏览器中完成操作后关闭窗口...');`,
124
+ ` const persistSessionCookies = async () => {`,
125
+ ` try {`,
126
+ ` const cookies = await ctx.cookies();`,
127
+ ` const session = cookies.filter(c => c.expires === -1);`,
128
+ ` if (session.length > 0) {`,
129
+ ` await ctx.addCookies(session.map(c => ({ ...c, expires: ${thirtyDays} })));`,
130
+ ` console.log('已将 ' + session.length + ' 个 session cookie 转为持久化');`,
131
+ ` }`,
132
+ ` } catch {}`,
133
+ ` };`,
134
+ ` ctx.on('page', p => p.on('close', () => persistSessionCookies()));`,
135
+ ` for (const p of ctx.pages()) p.on('close', () => persistSessionCookies());`,
136
+ ` await new Promise(r => {`,
137
+ ` ctx.on('close', r);`,
138
+ ` const t = setInterval(async () => {`,
139
+ ` try {`,
140
+ ` if (!ctx.pages().length) { clearInterval(t); await persistSessionCookies(); r(); }`,
141
+ ` } catch { clearInterval(t); r(); }`,
142
+ ` }, 2000);`,
143
+ ` });`,
144
+ ` try { await ctx.close(); } catch {}`,
145
+ `})().then(() => process.exit(0)).catch(() => process.exit(0));`,
146
+ ].join('\n');
147
+ }
148
+
149
+ function runBrowserScript(script, cwd) {
150
+ const tmpScript = path.join(os.tmpdir(), `pw-auth-${Date.now()}.js`);
151
+ fs.writeFileSync(tmpScript, script);
152
+ try {
153
+ execSync(`node "${tmpScript}"`, { stdio: 'inherit', cwd });
154
+ return true;
155
+ } catch {
156
+ return false;
157
+ } finally {
158
+ try { fs.unlinkSync(tmpScript); } catch {}
159
+ }
160
+ }
161
+
162
+ // ─────────────────────────────────────────────────────────────
163
+ // auth 模式实现
164
+ // ─────────────────────────────────────────────────────────────
165
+
166
+ async function authPersistent(url) {
167
+ const playwrightDir = resolvePlaywright();
168
+ if (!playwrightDir) {
169
+ log('error', '未找到 playwright 模块');
170
+ log('info', '请安装: npm install -g playwright && npx playwright install chromium');
171
+ return;
172
+ }
173
+
174
+ const profileDir = assets.path('browserProfile');
175
+ if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
176
+
177
+ const lockFile = path.join(profileDir, 'SingletonLock');
178
+ if (fs.existsSync(lockFile)) {
179
+ fs.unlinkSync(lockFile);
180
+ log('warn', '已清理残留的 SingletonLock(上次浏览器未正常关闭)');
181
+ }
182
+
183
+ console.log('操作步骤:');
184
+ console.log(' 1. 浏览器将自动打开,请手动完成登录');
185
+ console.log(' 2. 登录成功后关闭浏览器窗口');
186
+ console.log(' 3. 登录状态将保存在持久化配置中(session cookie 自动转持久化)');
187
+ console.log(' 4. MCP 后续会话自动复用此登录状态');
188
+ console.log('');
189
+
190
+ const script = buildBrowserScript(playwrightDir, profileDir, url);
191
+ const projectRoot = assets.projectRoot;
192
+
193
+ const ok = runBrowserScript(script, projectRoot);
194
+ if (!ok) {
195
+ const profileFiles = fs.readdirSync(profileDir);
196
+ if (profileFiles.length <= 2) {
197
+ log('error', 'Playwright 启动失败,且未检测到有效的浏览器配置');
198
+ log('info', '请确保已安装 Chromium: npx playwright install chromium');
199
+ return;
200
+ }
201
+ log('warn', '浏览器退出码非零,但已检测到有效配置,继续...');
202
+ }
203
+
204
+ const mcpPath = assets.path('mcpConfig');
205
+ log('ok', '登录状态已保存到持久化配置');
206
+ updateMcpConfig(mcpPath, 'playwright', 'persistent');
207
+ updateGitignore('.claude-coder/.runtime/browser-profile');
208
+ enableWebTestEnv('playwright');
209
+
210
+ console.log('');
211
+ log('ok', '配置完成!');
212
+ const relProfile = path.relative(projectRoot, profileDir);
213
+ log('info', `MCP 使用 persistent 模式 (user-data-dir: ${relProfile})`);
214
+ log('info', '验证: 再次运行 claude-coder auth <URL>,浏览器应直接进入已登录状态');
215
+ }
216
+
217
+ async function authIsolated(url) {
218
+ const playwrightAuthPath = assets.path('playwrightAuth');
219
+ const projectRoot = assets.projectRoot;
220
+
221
+ console.log('操作步骤:');
222
+ console.log(' 1. 浏览器将自动打开,请手动完成登录');
223
+ console.log(' 2. 登录成功后关闭浏览器窗口');
224
+ console.log(' 3. 登录状态(cookies + localStorage)将保存到 playwright-auth.json');
225
+ console.log(' 4. MCP 每次会话自动从此文件加载初始状态');
226
+ console.log('');
227
+
228
+ try {
229
+ execSync(
230
+ `npx playwright codegen --save-storage="${playwrightAuthPath}" "${url}"`,
231
+ { stdio: 'inherit', cwd: projectRoot }
232
+ );
233
+ } catch (err) {
234
+ if (!fs.existsSync(playwrightAuthPath)) {
235
+ log('error', `Playwright 登录状态导出失败: ${err.message}`);
236
+ log('info', '请确保已安装: npx playwright install chromium');
237
+ return;
238
+ }
239
+ }
240
+
241
+ if (!fs.existsSync(playwrightAuthPath)) {
242
+ log('error', '未检测到导出的登录状态文件');
243
+ return;
244
+ }
245
+
246
+ const mcpPath = assets.path('mcpConfig');
247
+ log('ok', '登录状态已保存到 playwright-auth.json');
248
+ updateMcpConfig(mcpPath, 'playwright', 'isolated');
249
+ updateGitignore('.claude-coder/playwright-auth.json');
250
+ enableWebTestEnv('playwright');
251
+
252
+ console.log('');
253
+ log('ok', '配置完成!');
254
+ log('info', 'MCP 使用 isolated 模式 (storage-state)');
255
+ log('info', 'cookies 和 localStorage 每次会话自动从 playwright-auth.json 加载');
256
+ }
257
+
258
+ function authExtension() {
259
+ console.log('Extension 模式说明:');
260
+ console.log('');
261
+ console.log(' 此模式通过 Chrome 扩展连接到您正在运行的浏览器。');
262
+ console.log(' MCP 将直接使用浏览器中已有的登录态和扩展。');
263
+ console.log('');
264
+ console.log(' 前置条件:');
265
+ console.log(' 1. 安装 "Playwright MCP Bridge" Chrome/Edge 扩展');
266
+ console.log(' https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm');
267
+ console.log(' 2. 确保浏览器已启动且扩展已启用');
268
+ console.log(' 3. 无需额外认证操作,您的浏览器登录态将自动可用');
269
+ console.log('');
270
+
271
+ const mcpPath = assets.path('mcpConfig');
272
+ updateMcpConfig(mcpPath, 'playwright', 'extension');
273
+ enableWebTestEnv('playwright');
274
+
275
+ console.log('');
276
+ log('ok', '配置完成!');
277
+ log('info', 'MCP 使用 extension 模式(连接真实浏览器)');
278
+ log('info', '确保 Chrome/Edge 已运行且 Playwright MCP Bridge 扩展已启用');
279
+ }
280
+
281
+ function getChromeCommand() {
282
+ if (process.platform === 'win32') {
283
+ const prefixes = [
284
+ process.env['PROGRAMFILES(X86)'],
285
+ process.env.PROGRAMFILES,
286
+ process.env.LOCALAPPDATA,
287
+ ].filter(Boolean);
288
+ for (const prefix of prefixes) {
289
+ const p = path.join(prefix, 'Google', 'Chrome', 'Application', 'chrome.exe');
290
+ if (fs.existsSync(p)) return `"${p}"`;
291
+ }
292
+ return 'start chrome';
293
+ }
294
+ if (process.platform === 'darwin') return 'open -a "Google Chrome" --args';
295
+ return 'google-chrome';
296
+ }
297
+
298
+ function checkCdpConnection(port = 9222, timeoutMs = 5000) {
299
+ return new Promise(resolve => {
300
+ const req = http.get(`http://127.0.0.1:${port}/json/version`, res => {
301
+ let data = '';
302
+ res.on('data', chunk => { data += chunk; });
303
+ res.on('end', () => {
304
+ try {
305
+ const info = JSON.parse(data);
306
+ resolve({ ok: true, browser: info.Browser || 'Chrome', wsUrl: info.webSocketDebuggerUrl || '' });
307
+ } catch {
308
+ resolve({ ok: false });
309
+ }
310
+ });
311
+ });
312
+ req.on('error', () => resolve({ ok: false }));
313
+ req.setTimeout(timeoutMs, () => { req.destroy(); resolve({ ok: false }); });
314
+ });
315
+ }
316
+
317
+ async function authChromeDevTools(url) {
318
+ const mcpPath = assets.path('mcpConfig');
319
+ updateMcpConfig(mcpPath, 'chrome-devtools');
320
+ enableWebTestEnv('chrome-devtools');
321
+
322
+ log('ok', '.mcp.json 已配置完成');
323
+ console.log('');
324
+
325
+ log('info', '正在检测 Chrome DevTools 连接...');
326
+ let conn = await checkCdpConnection();
327
+
328
+ if (!conn.ok && url) {
329
+ log('info', '未检测到 Chrome 远程调试实例,尝试启动 Chrome...');
330
+ const chromeCmd = getChromeCommand();
331
+ const launchCmd = `${chromeCmd} --remote-debugging-port=9222 "${url}"`;
332
+ try {
333
+ const { spawn } = require('child_process');
334
+ const child = spawn(launchCmd, { shell: true, detached: true, stdio: 'ignore' });
335
+ child.unref();
336
+ } catch (err) {
337
+ log('warn', `Chrome 启动失败: ${err.message}`);
338
+ }
339
+
340
+ for (let i = 0; i < 6; i++) {
341
+ await new Promise(r => setTimeout(r, 2000));
342
+ conn = await checkCdpConnection();
343
+ if (conn.ok) break;
344
+ }
345
+ }
346
+
347
+ console.log('');
348
+ if (conn.ok) {
349
+ log('ok', `Chrome DevTools 连接成功: ${conn.browser}`);
350
+ if (conn.wsUrl) log('info', `WebSocket: ${conn.wsUrl}`);
351
+ log('ok', '配置验证通过!MCP 可以正常连接 Chrome。');
352
+ } else {
353
+ log('warn', '未检测到 Chrome 远程调试实例');
354
+ console.log('');
355
+ console.log(' 请确保:');
356
+ console.log(' 1. Chrome 144+ 已安装');
357
+ console.log(' 2. 打开 chrome://inspect/#remote-debugging 启用远程调试');
358
+ console.log(' 3. 允许传入调试连接');
359
+ console.log('');
360
+ console.log(' 或手动启动带远程调试的 Chrome:');
361
+ const chromeCmd = getChromeCommand();
362
+ console.log(` ${chromeCmd} --remote-debugging-port=9222`);
363
+ console.log('');
364
+ log('info', '.mcp.json 已配置,Chrome 就绪后 MCP 会自动连接 (autoConnect)');
365
+ }
366
+ }
367
+
368
+ async function auth(url) {
369
+ assets.ensureDirs();
370
+ const config = loadConfig();
371
+ const tool = config.webTestTool;
372
+
373
+ if (!tool) {
374
+ log('error', '未配置浏览器测试工具');
375
+ log('info', '请先运行 claude-coder setup 选择 Playwright MCP 或 Chrome DevTools MCP');
376
+ return;
377
+ }
378
+
379
+ if (tool === 'chrome-devtools') {
380
+ const [major, minor] = process.versions.node.split('.').map(Number);
381
+ if (major < 20 || (major === 20 && minor < 19)) {
382
+ log('warn', `当前 Node.js 版本 v${process.versions.node},Chrome DevTools MCP 要求 v20.19+`);
383
+ log('info', 'nvm 用户请执行: nvm alias default 22 && nvm use 22');
384
+ log('info', '升级后重新运行此命令');
385
+ return;
386
+ }
387
+ const targetUrl = normalizeUrl(url) || null;
388
+ log('info', '浏览器工具: Chrome DevTools MCP');
389
+ if (targetUrl) log('info', `目标 URL: ${targetUrl}`);
390
+ console.log('');
391
+ await authChromeDevTools(targetUrl);
392
+ return;
393
+ }
394
+
395
+ // Playwright MCP
396
+ const mode = config.webTestMode;
397
+ const targetUrl = normalizeUrl(url) || 'http://localhost:3000';
398
+
399
+ log('info', `浏览器工具: Playwright MCP (${mode} 模式)`);
400
+ log('info', `目标 URL: ${targetUrl}`);
401
+ console.log('');
402
+
403
+ switch (mode) {
404
+ case 'persistent':
405
+ await authPersistent(targetUrl);
406
+ break;
407
+ case 'isolated':
408
+ await authIsolated(targetUrl);
409
+ break;
410
+ case 'extension':
411
+ authExtension();
412
+ break;
413
+ default:
414
+ log('error', `未知的 Playwright 模式: ${mode}`);
415
+ log('info', '请运行 claude-coder setup 重新配置');
416
+ return;
417
+ }
418
+ }
419
+
420
+ module.exports = { auth, updateMcpConfig };