chrome-devtools-mcp-for-extension 0.14.1 → 0.15.0

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.
@@ -7,6 +7,7 @@ import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import puppeteer from 'puppeteer-core';
10
+ import { resolveUserDataDir } from './profile-resolver.js';
10
11
  let browser;
11
12
  const ignoredPrefixes = new Set([
12
13
  'chrome://',
@@ -418,20 +419,36 @@ export async function launch(options) {
418
419
  const { channel, executablePath, customDevTools, headless, isolated, loadExtension, loadExtensionsDir, loadSystemExtensions, chromeProfile, } = options;
419
420
  // Reset development extension paths
420
421
  developmentExtensionPaths = [];
421
- const profileDirName = channel && channel !== 'stable'
422
- ? `chrome-profile-${channel}`
423
- : 'chrome-profile';
424
- let userDataDir = options.userDataDir;
422
+ // Resolve user data directory using new profile resolver (v0.15.0+)
423
+ const resolved = resolveUserDataDir({
424
+ cliUserDataDir: options.userDataDir,
425
+ env: process.env,
426
+ cwd: process.cwd(),
427
+ channel: channel || 'stable',
428
+ });
429
+ const userDataDir = resolved.path;
430
+ await fs.promises.mkdir(userDataDir, { recursive: true });
431
+ // Legacy profile warning (shown if legacy path exists)
432
+ try {
433
+ const legacy = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', 'chrome-profile');
434
+ if (fs.existsSync(legacy)) {
435
+ console.error(`⚠️ Legacy profile detected: ${legacy}\n` +
436
+ `ℹ️ New profile location: ${path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', 'profiles', 'project-default', 'stable')}\n` +
437
+ `💡 To continue using the legacy profile, set: MCP_USER_DATA_DIR=${legacy}`);
438
+ }
439
+ }
440
+ catch {
441
+ /* ignore */
442
+ }
443
+ // Profile resolution logs
444
+ console.error(`[profiles] Using: ${userDataDir}`);
445
+ console.error(` Reason: ${resolved.reason}`);
446
+ console.error(` Project: ${resolved.projectName} (${resolved.hash})`);
447
+ if (resolved.reason === 'AUTO') {
448
+ console.error(` Root: ${process.cwd()}`);
449
+ }
425
450
  let usingSystemProfile = false;
426
451
  let profileDirectory = 'Default';
427
- if (!userDataDir) {
428
- // Use isolated profile (independent from system Chrome)
429
- userDataDir = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', profileDirName);
430
- await fs.promises.mkdir(userDataDir, {
431
- recursive: true,
432
- });
433
- console.error(`📁 Using isolated profile: ${userDataDir}`);
434
- }
435
452
  const args = [
436
453
  '--hide-crash-restore-bubble',
437
454
  `--profile-directory=${profileDirectory}`,
@@ -0,0 +1,190 @@
1
+ // src/profile-resolver.ts
2
+ // Phase 1 (v0.15.0)
3
+ // - Hybrid priority (CLI > MCP_USER_DATA_DIR > MCP_PROJECT_ID > AUTO > DEFAULT)
4
+ // - Auto-detection (git root -> nearest package.json -> cwd)
5
+ // - Realpath normalization
6
+ // - Tilde (~) expansion
7
+ // - Short SHA-256 hash (8 chars)
8
+ // - CI detection => ephemeral session profile (unless MCP_PERSIST_PROFILES)
9
+ // - Minimal console.error() logging of decision
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+ import crypto from 'node:crypto';
14
+ import { detectProjectName, detectProjectRoot } from './project-detector.js';
15
+ const CACHE_ROOT = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp');
16
+ // --- Public API ---
17
+ export function resolveUserDataDir(opts) {
18
+ const channel = opts.channel || 'stable';
19
+ // 0) CI detection → ephemeral session directory (unless MCP_PERSIST_PROFILES)
20
+ // - This happens before other priorities to keep CI clean by default.
21
+ if (isCI(opts.env) && !opts.env.MCP_PERSIST_PROFILES) {
22
+ const sessionId = `${process.pid}-${Date.now()}`;
23
+ const tempPath = realpathSafe(path.join(CACHE_ROOT, 'sessions', sessionId, channel));
24
+ // best-effort cleanup on exit
25
+ process.on('exit', () => {
26
+ try {
27
+ fs.rmSync(tempPath, { recursive: true, force: true });
28
+ }
29
+ catch {
30
+ /* ignore */
31
+ }
32
+ });
33
+ const result = {
34
+ path: tempPath,
35
+ reason: 'AUTO', // keep enum as specified (no EPHEMERAL type in Phase 1)
36
+ projectKey: `session-${sessionId}`,
37
+ projectName: 'ci-session',
38
+ hash: sessionId,
39
+ channel,
40
+ };
41
+ // concise decision log (resolver-side)
42
+ console.error(`[profiles] resolved(AUTO, ci-ephemeral): ${result.path} (session=${sessionId})`);
43
+ return result;
44
+ }
45
+ // 1) CLI explicit userDataDir
46
+ if (opts.cliUserDataDir && opts.cliUserDataDir.trim().length > 0) {
47
+ const p = realpathOrExpand(opts.cliUserDataDir);
48
+ const result = {
49
+ path: p,
50
+ reason: 'CLI',
51
+ projectKey: stripHomeForKey(p),
52
+ projectName: 'cli',
53
+ hash: shortHash(p),
54
+ channel,
55
+ };
56
+ console.error(`[profiles] resolved(CLI): ${result.path}`);
57
+ return result;
58
+ }
59
+ // 2) ENV: MCP_USER_DATA_DIR (full path)
60
+ const envUserData = opts.env.MCP_USER_DATA_DIR?.trim();
61
+ if (envUserData) {
62
+ const p = realpathOrExpand(envUserData);
63
+ const result = {
64
+ path: p,
65
+ reason: 'MCP_USER_DATA_DIR',
66
+ projectKey: stripHomeForKey(p),
67
+ projectName: 'env',
68
+ hash: shortHash(p),
69
+ channel,
70
+ };
71
+ console.error(`[profiles] resolved(MCP_USER_DATA_DIR): ${result.path}`);
72
+ return result;
73
+ }
74
+ // 3) ENV: MCP_PROJECT_ID (project-scoped persistent profile)
75
+ const projectId = sanitize(opts.env.MCP_PROJECT_ID || '');
76
+ if (projectId) {
77
+ const p = projectProfilePath(projectId, channel);
78
+ const result = {
79
+ path: p,
80
+ reason: 'MCP_PROJECT_ID',
81
+ projectKey: projectId,
82
+ projectName: projectId,
83
+ hash: shortHash(projectId),
84
+ channel,
85
+ };
86
+ console.error(`[profiles] resolved(MCP_PROJECT_ID): ${result.path} (projectId=${projectId})`);
87
+ return result;
88
+ }
89
+ // 4) AUTO: detect by root -> name -> hash
90
+ try {
91
+ const root = detectProjectRoot(opts.cwd);
92
+ const name = detectProjectName(root);
93
+ const realRoot = realpathSafe(root);
94
+ const hash = shortHash(realRoot);
95
+ const key = `${sanitize(name)}_${hash}`;
96
+ const p = projectProfilePath(key, channel);
97
+ const result = {
98
+ path: p,
99
+ reason: 'AUTO',
100
+ projectKey: key,
101
+ projectName: sanitize(name),
102
+ hash,
103
+ channel,
104
+ };
105
+ console.error(`[profiles] resolved(AUTO): ${result.path} (root=${root}, name=${name}, hash=${hash})`);
106
+ return result;
107
+ }
108
+ catch (e) {
109
+ // 5) DEFAULT fallback
110
+ const key = 'project-default';
111
+ const p = projectProfilePath(key, channel);
112
+ const result = {
113
+ path: p,
114
+ reason: 'DEFAULT',
115
+ projectKey: key,
116
+ projectName: key,
117
+ hash: '00000000',
118
+ channel,
119
+ };
120
+ console.error(`[profiles] resolved(DEFAULT): ${result.path} (reason=${e?.message || 'fallback'})`);
121
+ return result;
122
+ }
123
+ }
124
+ // --- Helpers ---
125
+ function isCI(env) {
126
+ // Common CI signals
127
+ return env.CI === 'true' || env.GITHUB_ACTIONS === 'true';
128
+ }
129
+ function projectProfilePath(projectKey, channel) {
130
+ const base = path.join(CACHE_ROOT, 'profiles', projectKey, channel);
131
+ return pathNormalize(base);
132
+ }
133
+ function shortHash(s) {
134
+ try {
135
+ return crypto.createHash('sha256').update(s).digest('hex').slice(0, 8);
136
+ }
137
+ catch {
138
+ // Extremely unlikely; fallback for robustness
139
+ return '00000000';
140
+ }
141
+ }
142
+ function sanitize(s) {
143
+ const base = (s || 'project').toLowerCase().replace(/[^a-z0-9-_]/g, '-');
144
+ // Avoid empty string after sanitize
145
+ return base.length ? base : 'project';
146
+ }
147
+ function expandTilde(p) {
148
+ if (!p)
149
+ return p;
150
+ if (p.startsWith('~')) {
151
+ return path.join(os.homedir(), p.slice(1));
152
+ }
153
+ return p;
154
+ }
155
+ function realpathSafe(p) {
156
+ try {
157
+ // Use native realpath if available for speed/behavior
158
+ // (Node 18+ has fs.realpathSync.native)
159
+ const rp = fs.realpathSync.native
160
+ ? fs.realpathSync.native(p)
161
+ : fs.realpathSync(p);
162
+ return rp;
163
+ }
164
+ catch {
165
+ // The path may not exist yet; normalize current form
166
+ return pathNormalize(p);
167
+ }
168
+ }
169
+ function realpathOrExpand(p) {
170
+ const expanded = expandTilde(p);
171
+ return realpathSafe(expanded);
172
+ }
173
+ function pathNormalize(p) {
174
+ // Normalize but do not resolve symlinks here
175
+ // (realpathSafe is used where resolution is desired)
176
+ return path.normalize(p);
177
+ }
178
+ /**
179
+ * Best-effort readable key when user supplied an absolute path.
180
+ * We do not want slashes in the key, so hash + tail dir name.
181
+ */
182
+ function stripHomeForKey(absPath) {
183
+ try {
184
+ const name = path.basename(absPath);
185
+ return `${sanitize(name)}_${shortHash(absPath)}`;
186
+ }
187
+ catch {
188
+ return `abs_${shortHash(absPath)}`;
189
+ }
190
+ }
@@ -0,0 +1,66 @@
1
+ // src/project-detector.ts
2
+ // Phase 1 (v0.15.0)
3
+ // - detectProjectRoot: git root → nearest package.json → cwd
4
+ // - detectProjectName: package.json "name" → dirname
5
+ // - Use spawnSync with 500ms timeout to avoid blocking long
6
+ // - Realpath normalization
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
10
+ export function detectProjectRoot(cwd) {
11
+ const realCwd = realpathSafe(cwd);
12
+ // 1) Try 'git rev-parse --show-toplevel' with 500ms timeout
13
+ const git = spawnSync('git', ['rev-parse', '--show-toplevel'], {
14
+ cwd: realCwd,
15
+ timeout: 500,
16
+ encoding: 'utf8',
17
+ stdio: ['ignore', 'pipe', 'ignore'],
18
+ windowsHide: true,
19
+ });
20
+ if (git.status === 0 && git.stdout) {
21
+ const out = git.stdout.toString().trim();
22
+ if (out)
23
+ return realpathSafe(out);
24
+ }
25
+ // If git is not available or command failed/timed out → fall through
26
+ // 2) Walk up to nearest package.json
27
+ let cur = realCwd;
28
+ while (true) {
29
+ if (fs.existsSync(path.join(cur, 'package.json')))
30
+ return cur;
31
+ const next = path.dirname(cur);
32
+ if (next === cur)
33
+ break; // reached filesystem root
34
+ cur = next;
35
+ }
36
+ // 3) Fallback to cwd
37
+ return realCwd;
38
+ }
39
+ export function detectProjectName(root) {
40
+ const realRoot = realpathSafe(root);
41
+ const pj = path.join(realRoot, 'package.json');
42
+ if (fs.existsSync(pj)) {
43
+ try {
44
+ const pkg = JSON.parse(fs.readFileSync(pj, 'utf8'));
45
+ if (pkg && typeof pkg.name === 'string' && pkg.name.trim()) {
46
+ return String(pkg.name).trim();
47
+ }
48
+ }
49
+ catch {
50
+ // ignore parse errors and fall back to directory name
51
+ }
52
+ }
53
+ return path.basename(realRoot);
54
+ }
55
+ // --- helpers ---
56
+ function realpathSafe(p) {
57
+ try {
58
+ const rp = fs.realpathSync.native
59
+ ? fs.realpathSync.native(p)
60
+ : fs.realpathSync(p);
61
+ return rp;
62
+ }
63
+ catch {
64
+ return path.normalize(p);
65
+ }
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp-for-extension",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
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",