@mcp-b/chrome-devtools-mcp 2.3.0 → 2.3.1-beta.20260528050333

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 (67) hide show
  1. package/package.json +1 -1
  2. package/build/src/DevToolsConnectionAdapter.js +0 -70
  3. package/build/src/DevtoolsUtils.js +0 -290
  4. package/build/src/McpContext.js +0 -687
  5. package/build/src/McpPage.js +0 -95
  6. package/build/src/McpResponse.js +0 -588
  7. package/build/src/Mutex.js +0 -37
  8. package/build/src/PageCollector.js +0 -308
  9. package/build/src/SlimMcpResponse.js +0 -18
  10. package/build/src/WaitForHelper.js +0 -135
  11. package/build/src/bin/chrome-devtools-cli-options.js +0 -651
  12. package/build/src/bin/chrome-devtools-mcp-cli-options.js +0 -317
  13. package/build/src/bin/chrome-devtools-mcp-main.js +0 -35
  14. package/build/src/bin/chrome-devtools-mcp.js +0 -21
  15. package/build/src/bin/chrome-devtools.js +0 -185
  16. package/build/src/bin/cliDefinitions.js +0 -615
  17. package/build/src/browser.js +0 -198
  18. package/build/src/daemon/client.js +0 -152
  19. package/build/src/daemon/daemon.js +0 -206
  20. package/build/src/daemon/types.js +0 -6
  21. package/build/src/daemon/utils.js +0 -108
  22. package/build/src/formatters/ConsoleFormatter.js +0 -234
  23. package/build/src/formatters/IssueFormatter.js +0 -192
  24. package/build/src/formatters/NetworkFormatter.js +0 -215
  25. package/build/src/formatters/SnapshotFormatter.js +0 -131
  26. package/build/src/index.js +0 -202
  27. package/build/src/issue-descriptions.js +0 -39
  28. package/build/src/logger.js +0 -36
  29. package/build/src/polyfill.js +0 -7
  30. package/build/src/telemetry/ClearcutLogger.js +0 -102
  31. package/build/src/telemetry/WatchdogClient.js +0 -60
  32. package/build/src/telemetry/flagUtils.js +0 -45
  33. package/build/src/telemetry/metricUtils.js +0 -14
  34. package/build/src/telemetry/persistence.js +0 -53
  35. package/build/src/telemetry/types.js +0 -33
  36. package/build/src/telemetry/watchdog/ClearcutSender.js +0 -203
  37. package/build/src/telemetry/watchdog/main.js +0 -127
  38. package/build/src/third_party/devtools-formatter-worker.js +0 -7
  39. package/build/src/third_party/index.js +0 -26
  40. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +0 -54183
  41. package/build/src/tools/ToolDefinition.js +0 -72
  42. package/build/src/tools/categories.js +0 -24
  43. package/build/src/tools/console.js +0 -85
  44. package/build/src/tools/emulation.js +0 -55
  45. package/build/src/tools/extensions.js +0 -96
  46. package/build/src/tools/input.js +0 -368
  47. package/build/src/tools/lighthouse.js +0 -123
  48. package/build/src/tools/memory.js +0 -28
  49. package/build/src/tools/network.js +0 -120
  50. package/build/src/tools/pages.js +0 -319
  51. package/build/src/tools/performance.js +0 -190
  52. package/build/src/tools/screencast.js +0 -79
  53. package/build/src/tools/screenshot.js +0 -84
  54. package/build/src/tools/script.js +0 -119
  55. package/build/src/tools/slim/tools.js +0 -81
  56. package/build/src/tools/snapshot.js +0 -56
  57. package/build/src/tools/tools.js +0 -52
  58. package/build/src/tools/webmcp.js +0 -416
  59. package/build/src/trace-processing/parse.js +0 -84
  60. package/build/src/types.js +0 -6
  61. package/build/src/utils/ExtensionRegistry.js +0 -35
  62. package/build/src/utils/files.js +0 -19
  63. package/build/src/utils/keyboard.js +0 -296
  64. package/build/src/utils/pagination.js +0 -49
  65. package/build/src/utils/string.js +0 -36
  66. package/build/src/utils/types.js +0 -6
  67. package/build/src/version.js +0 -9
@@ -1,198 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- import { execSync } from 'node:child_process';
7
- import fs from 'node:fs';
8
- import os from 'node:os';
9
- import path from 'node:path';
10
- import { logger } from './logger.js';
11
- import { puppeteer } from './third_party/index.js';
12
- let browser;
13
- function makeTargetFilter(enableExtensions = false) {
14
- const ignoredPrefixes = new Set(['chrome://', 'chrome-untrusted://']);
15
- if (!enableExtensions) {
16
- ignoredPrefixes.add('chrome-extension://');
17
- }
18
- return function targetFilter(target) {
19
- if (target.url() === 'chrome://newtab/') {
20
- return true;
21
- }
22
- // Could be the only page opened in the browser.
23
- if (target.url().startsWith('chrome://inspect')) {
24
- return true;
25
- }
26
- for (const prefix of ignoredPrefixes) {
27
- if (target.url().startsWith(prefix)) {
28
- return false;
29
- }
30
- }
31
- return true;
32
- };
33
- }
34
- export async function ensureBrowserConnected(options) {
35
- const { channel, enableExtensions } = options;
36
- if (browser?.connected) {
37
- return browser;
38
- }
39
- const connectOptions = {
40
- targetFilter: makeTargetFilter(enableExtensions),
41
- defaultViewport: null,
42
- handleDevToolsAsPage: true,
43
- };
44
- let autoConnect = false;
45
- if (options.wsEndpoint) {
46
- connectOptions.browserWSEndpoint = options.wsEndpoint;
47
- if (options.wsHeaders) {
48
- connectOptions.headers = options.wsHeaders;
49
- }
50
- }
51
- else if (options.browserURL) {
52
- connectOptions.browserURL = options.browserURL;
53
- }
54
- else if (channel || options.userDataDir) {
55
- const userDataDir = options.userDataDir;
56
- if (userDataDir) {
57
- autoConnect = true;
58
- // TODO: re-expose this logic via Puppeteer.
59
- const portPath = path.join(userDataDir, 'DevToolsActivePort');
60
- try {
61
- const fileContent = await fs.promises.readFile(portPath, 'utf8');
62
- const [rawPort, rawPath] = fileContent
63
- .split('\n')
64
- .map((line) => {
65
- return line.trim();
66
- })
67
- .filter((line) => {
68
- return !!line;
69
- });
70
- if (!rawPort || !rawPath) {
71
- throw new Error(`Invalid DevToolsActivePort '${fileContent}' found`);
72
- }
73
- const port = parseInt(rawPort, 10);
74
- if (isNaN(port) || port <= 0 || port > 65535) {
75
- throw new Error(`Invalid port '${rawPort}' found`);
76
- }
77
- const browserWSEndpoint = `ws://127.0.0.1:${port}${rawPath}`;
78
- connectOptions.browserWSEndpoint = browserWSEndpoint;
79
- }
80
- catch (error) {
81
- throw new Error(`Could not connect to Chrome in ${userDataDir}. Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.`, {
82
- cause: error,
83
- });
84
- }
85
- }
86
- else {
87
- if (!channel) {
88
- throw new Error('Channel must be provided if userDataDir is missing');
89
- }
90
- connectOptions.channel = (channel === 'stable' ? 'chrome' : `chrome-${channel}`);
91
- }
92
- }
93
- else {
94
- throw new Error('Either browserURL, wsEndpoint, channel or userDataDir must be provided');
95
- }
96
- logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
97
- try {
98
- browser = await puppeteer.connect(connectOptions);
99
- }
100
- catch (err) {
101
- throw new Error(`Could not connect to Chrome. ${autoConnect ? `Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.` : `Check if Chrome is running.`}`, {
102
- cause: err,
103
- });
104
- }
105
- logger('Connected Puppeteer');
106
- return browser;
107
- }
108
- export function detectDisplay() {
109
- // Only detect display on Linux/UNIX.
110
- if (os.platform() === 'win32' || os.platform() === 'darwin') {
111
- return;
112
- }
113
- if (!process.env['DISPLAY']) {
114
- try {
115
- const result = execSync(`ps -u $(id -u) -o pid= | xargs -I{} cat /proc/{}/environ 2>/dev/null | tr '\\0' '\\n' | grep -m1 '^DISPLAY=' | cut -d= -f2`);
116
- const display = result.toString('utf8').trim();
117
- process.env['DISPLAY'] = display;
118
- }
119
- catch {
120
- // no-op
121
- }
122
- }
123
- }
124
- export async function launch(options) {
125
- const { channel, executablePath, headless, isolated } = options;
126
- const profileDirName = channel && channel !== 'stable' ? `chrome-profile-${channel}` : 'chrome-profile';
127
- let userDataDir = options.userDataDir;
128
- if (!isolated && !userDataDir) {
129
- userDataDir = path.join(os.homedir(), '.cache', options.viaCli ? 'chrome-devtools-mcp-cli' : 'chrome-devtools-mcp', profileDirName);
130
- await fs.promises.mkdir(userDataDir, {
131
- recursive: true,
132
- });
133
- }
134
- const args = [
135
- ...(options.chromeArgs ?? []),
136
- '--hide-crash-restore-bubble',
137
- ];
138
- const ignoreDefaultArgs = options.ignoreDefaultChromeArgs ?? false;
139
- if (headless) {
140
- args.push('--screen-info={3840x2160}');
141
- }
142
- let puppeteerChannel;
143
- if (options.devtools) {
144
- args.push('--auto-open-devtools-for-tabs');
145
- }
146
- if (!executablePath) {
147
- puppeteerChannel =
148
- channel && channel !== 'stable' ? `chrome-${channel}` : 'chrome';
149
- }
150
- if (!headless) {
151
- detectDisplay();
152
- }
153
- try {
154
- const browser = await puppeteer.launch({
155
- channel: puppeteerChannel,
156
- targetFilter: makeTargetFilter(options.enableExtensions),
157
- executablePath,
158
- defaultViewport: null,
159
- userDataDir,
160
- pipe: true,
161
- headless,
162
- args,
163
- ignoreDefaultArgs: ignoreDefaultArgs,
164
- acceptInsecureCerts: options.acceptInsecureCerts,
165
- handleDevToolsAsPage: true,
166
- enableExtensions: options.enableExtensions,
167
- });
168
- if (options.logFile) {
169
- // FIXME: we are probably subscribing too late to catch startup logs. We
170
- // should expose the process earlier or expose the getRecentLogs() getter.
171
- browser.process()?.stderr?.pipe(options.logFile);
172
- browser.process()?.stdout?.pipe(options.logFile);
173
- }
174
- if (options.viewport) {
175
- const [page] = await browser.pages();
176
- await page?.resize({
177
- contentWidth: options.viewport.width,
178
- contentHeight: options.viewport.height,
179
- });
180
- }
181
- return browser;
182
- }
183
- catch (error) {
184
- if (userDataDir && error.message.includes('The browser is already running')) {
185
- throw new Error(`The browser is already running for ${userDataDir}. Use --isolated to run multiple browser instances.`, {
186
- cause: error,
187
- });
188
- }
189
- throw error;
190
- }
191
- }
192
- export async function ensureBrowserLaunched(options) {
193
- if (browser?.connected) {
194
- return browser;
195
- }
196
- browser = await launch(options);
197
- return browser;
198
- }
@@ -1,152 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2026 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- import { spawn } from 'node:child_process';
7
- import fs from 'node:fs';
8
- import net from 'node:net';
9
- import { logger } from '../logger.js';
10
- import { PipeTransport } from '../third_party/index.js';
11
- import { saveTemporaryFile } from '../utils/files.js';
12
- import { DAEMON_SCRIPT_PATH, getSocketPath, getPidFilePath, isDaemonRunning, } from './utils.js';
13
- const FILE_TIMEOUT = 10_000;
14
- /**
15
- * Waits for a file to be created and populated (removed = false) or removed (removed = true).
16
- */
17
- function waitForFile(filePath, removed = false) {
18
- return new Promise((resolve, reject) => {
19
- const check = () => {
20
- const exists = fs.existsSync(filePath);
21
- if (removed) {
22
- return !exists;
23
- }
24
- if (!exists) {
25
- return false;
26
- }
27
- try {
28
- return fs.statSync(filePath).size > 0;
29
- }
30
- catch {
31
- return false;
32
- }
33
- };
34
- if (check()) {
35
- resolve();
36
- return;
37
- }
38
- const timer = setTimeout(() => {
39
- fs.unwatchFile(filePath);
40
- reject(new Error(`Timeout: file ${filePath} ${removed ? 'not removed' : 'not found'} within ${FILE_TIMEOUT}ms`));
41
- }, FILE_TIMEOUT);
42
- fs.watchFile(filePath, { interval: 500 }, () => {
43
- if (check()) {
44
- clearTimeout(timer);
45
- fs.unwatchFile(filePath);
46
- resolve();
47
- }
48
- });
49
- });
50
- }
51
- export async function startDaemon(mcpArgs = []) {
52
- if (isDaemonRunning()) {
53
- logger('Daemon is already running');
54
- return;
55
- }
56
- const pidFilePath = getPidFilePath();
57
- if (fs.existsSync(pidFilePath)) {
58
- fs.unlinkSync(pidFilePath);
59
- }
60
- logger('Starting daemon...', ...mcpArgs);
61
- const child = spawn(process.execPath, [DAEMON_SCRIPT_PATH, ...mcpArgs], {
62
- detached: true,
63
- stdio: 'ignore',
64
- env: process.env,
65
- cwd: process.cwd(),
66
- windowsHide: true,
67
- });
68
- child.unref();
69
- await waitForFile(pidFilePath);
70
- }
71
- const SEND_COMMAND_TIMEOUT = 60_000; // ms
72
- /**
73
- * `sendCommand` opens a socket connection sends a single command and disconnects.
74
- */
75
- export async function sendCommand(command) {
76
- const socketPath = getSocketPath();
77
- const socket = net.createConnection({
78
- path: socketPath,
79
- });
80
- return new Promise((resolve, reject) => {
81
- const timer = setTimeout(() => {
82
- socket.destroy();
83
- reject(new Error('Timeout waiting for daemon response'));
84
- }, SEND_COMMAND_TIMEOUT);
85
- const transport = new PipeTransport(socket, socket);
86
- transport.onmessage = async (message) => {
87
- clearTimeout(timer);
88
- logger('onmessage', message);
89
- resolve(JSON.parse(message));
90
- };
91
- socket.on('error', error => {
92
- clearTimeout(timer);
93
- logger('Socket error:', error);
94
- reject(error);
95
- });
96
- socket.on('close', () => {
97
- clearTimeout(timer);
98
- logger('Socket closed:');
99
- reject(new Error('Socket closed'));
100
- });
101
- logger('Sending message', command);
102
- transport.send(JSON.stringify(command));
103
- });
104
- }
105
- export async function stopDaemon() {
106
- if (!isDaemonRunning()) {
107
- logger('Daemon is not running');
108
- return;
109
- }
110
- const pidFilePath = getPidFilePath();
111
- await sendCommand({ method: 'stop' });
112
- await waitForFile(pidFilePath, /*removed=*/ true);
113
- }
114
- export async function handleResponse(response, format) {
115
- if (response.isError) {
116
- return JSON.stringify(response.content);
117
- }
118
- if (format === 'json') {
119
- if (response.structuredContent) {
120
- return JSON.stringify(response.structuredContent);
121
- }
122
- // Fall-through to text for backward compatibility.
123
- }
124
- const chunks = [];
125
- for (const content of response.content) {
126
- if (content.type === 'text') {
127
- chunks.push(content.text);
128
- }
129
- else if (content.type === 'image') {
130
- const imageData = content.data;
131
- const mimeType = content.mimeType;
132
- let extension = '.png';
133
- switch (mimeType) {
134
- case 'image/jpg':
135
- case 'image/jpeg':
136
- extension = '.jpeg';
137
- break;
138
- case 'webp':
139
- extension = '.webp';
140
- break;
141
- }
142
- const data = Buffer.from(imageData, 'base64');
143
- const name = crypto.randomUUID();
144
- const { filepath } = await saveTemporaryFile(data, `${name}${extension}`);
145
- chunks.push(`Saved to ${filepath}.`);
146
- }
147
- else {
148
- throw new Error('Not supported response content type');
149
- }
150
- }
151
- return format === 'md' ? chunks.join(' ') : JSON.stringify(chunks);
152
- }
@@ -1,206 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * @license
4
- * Copyright 2026 Google LLC
5
- * SPDX-License-Identifier: Apache-2.0
6
- */
7
- import fs from 'node:fs';
8
- import { createServer } from 'node:net';
9
- import path from 'node:path';
10
- import process from 'node:process';
11
- import { logger } from '../logger.js';
12
- import { Client, PipeTransport, StdioClientTransport, } from '../third_party/index.js';
13
- import { VERSION } from '../version.js';
14
- import { getDaemonPid, getPidFilePath, getSocketPath, INDEX_SCRIPT_PATH, IS_WINDOWS, isDaemonRunning, } from './utils.js';
15
- const pid = getDaemonPid();
16
- if (isDaemonRunning(pid)) {
17
- logger('Another daemon process is running.');
18
- process.exit(1);
19
- }
20
- const pidFilePath = getPidFilePath();
21
- fs.mkdirSync(path.dirname(pidFilePath), {
22
- recursive: true,
23
- });
24
- fs.writeFileSync(pidFilePath, process.pid.toString());
25
- logger(`Writing ${process.pid.toString()} to ${pidFilePath}`);
26
- const socketPath = getSocketPath();
27
- const startDate = new Date();
28
- const mcpServerArgs = process.argv.slice(2);
29
- let mcpClient = null;
30
- let mcpTransport = null;
31
- let server = null;
32
- async function setupMCPClient() {
33
- console.log('Setting up MCP client connection...');
34
- // Create stdio transport for chrome-devtools-mcp
35
- // Workaround for https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/client/stdio.ts#L128
36
- // which causes the console window to show on Windows.
37
- // @ts-expect-error no types for type.
38
- process.type = 'mcp-client';
39
- mcpTransport = new StdioClientTransport({
40
- command: process.execPath,
41
- args: [INDEX_SCRIPT_PATH, ...mcpServerArgs],
42
- env: process.env,
43
- });
44
- mcpClient = new Client({
45
- name: 'chrome-devtools-cli-daemon',
46
- version: VERSION,
47
- }, {
48
- capabilities: {},
49
- });
50
- await mcpClient.connect(mcpTransport);
51
- console.log('MCP client connected');
52
- }
53
- async function handleRequest(msg) {
54
- try {
55
- if (msg.method === 'invoke_tool') {
56
- if (!mcpClient) {
57
- throw new Error('MCP client not initialized');
58
- }
59
- const { tool, args } = msg;
60
- const result = (await mcpClient.callTool({
61
- name: tool,
62
- arguments: args || {},
63
- }));
64
- return {
65
- success: true,
66
- result: JSON.stringify(result),
67
- };
68
- }
69
- else if (msg.method === 'stop') {
70
- // Ensure we are not interrupting in-progress starting.
71
- await started;
72
- // Trigger cleanup asynchronously.
73
- setImmediate(() => {
74
- void cleanup();
75
- });
76
- return {
77
- success: true,
78
- message: 'stopping',
79
- };
80
- }
81
- else if (msg.method === 'status') {
82
- return {
83
- success: true,
84
- result: JSON.stringify({
85
- pid: process.pid,
86
- socketPath,
87
- startDate: startDate.toISOString(),
88
- version: VERSION,
89
- args: mcpServerArgs,
90
- }),
91
- };
92
- }
93
- {
94
- return {
95
- success: false,
96
- error: `Unknown method: ${JSON.stringify(msg, null, 2)}`,
97
- };
98
- }
99
- }
100
- catch (error) {
101
- const errorMessage = error instanceof Error ? error.message : String(error);
102
- return {
103
- success: false,
104
- error: errorMessage,
105
- };
106
- }
107
- }
108
- async function startSocketServer() {
109
- // Remove existing socket file if it exists (only on non-Windows)
110
- if (!IS_WINDOWS) {
111
- try {
112
- fs.unlinkSync(socketPath);
113
- }
114
- catch {
115
- // ignore errors.
116
- }
117
- }
118
- return await new Promise((resolve, reject) => {
119
- server = createServer(socket => {
120
- const transport = new PipeTransport(socket, socket);
121
- transport.onmessage = async (message) => {
122
- logger('onmessage', message);
123
- const response = await handleRequest(JSON.parse(message));
124
- transport.send(JSON.stringify(response));
125
- socket.end();
126
- };
127
- socket.on('error', error => {
128
- logger('Socket error:', error);
129
- });
130
- });
131
- server.listen({
132
- path: socketPath,
133
- readableAll: false,
134
- writableAll: false,
135
- }, async () => {
136
- console.log(`Daemon server listening on ${socketPath}`);
137
- try {
138
- // Setup MCP client
139
- await setupMCPClient();
140
- resolve();
141
- }
142
- catch (err) {
143
- reject(err);
144
- }
145
- });
146
- server.on('error', error => {
147
- logger('Server error:', error);
148
- reject(error);
149
- });
150
- });
151
- }
152
- async function cleanup() {
153
- console.log('Cleaning up daemon...');
154
- try {
155
- await mcpClient?.close();
156
- }
157
- catch (error) {
158
- logger('Error closing MCP client:', error);
159
- }
160
- try {
161
- await mcpTransport?.close();
162
- }
163
- catch (error) {
164
- logger('Error closing MCP transport:', error);
165
- }
166
- if (server) {
167
- await new Promise(resolve => {
168
- server.close(() => resolve());
169
- });
170
- }
171
- if (!IS_WINDOWS) {
172
- try {
173
- fs.unlinkSync(socketPath);
174
- }
175
- catch {
176
- // ignore errors
177
- }
178
- }
179
- logger(`unlinking ${pidFilePath}`);
180
- if (fs.existsSync(pidFilePath)) {
181
- fs.unlinkSync(pidFilePath);
182
- }
183
- process.exit(0);
184
- }
185
- // Handle shutdown signals
186
- process.on('SIGTERM', () => {
187
- void cleanup();
188
- });
189
- process.on('SIGINT', () => {
190
- void cleanup();
191
- });
192
- process.on('SIGHUP', () => {
193
- void cleanup();
194
- });
195
- // Handle uncaught errors
196
- process.on('uncaughtException', error => {
197
- logger('Uncaught exception:', error);
198
- });
199
- process.on('unhandledRejection', error => {
200
- logger('Unhandled rejection:', error);
201
- });
202
- // Start the server
203
- const started = startSocketServer().catch(error => {
204
- logger('Failed to start daemon server:', error);
205
- process.exit(1);
206
- });
@@ -1,6 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2026 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- export {};
@@ -1,108 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2026 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- import fs from 'node:fs';
7
- import os from 'node:os';
8
- import path from 'node:path';
9
- import process from 'node:process';
10
- import { logger } from '../logger.js';
11
- export const DAEMON_SCRIPT_PATH = path.join(import.meta.dirname, 'daemon.js');
12
- export const INDEX_SCRIPT_PATH = path.join(import.meta.dirname, '..', 'bin', 'chrome-devtools-mcp.js');
13
- const APP_NAME = 'chrome-devtools-mcp';
14
- // Using these paths due to strict limits on the POSIX socket path length.
15
- export function getSocketPath() {
16
- const uid = os.userInfo().uid;
17
- if (IS_WINDOWS) {
18
- // Windows uses Named Pipes, not file paths.
19
- // This format is required for server.listen()
20
- return path.join('\\\\.\\pipe', APP_NAME, 'server.sock');
21
- }
22
- // 1. Try XDG_RUNTIME_DIR (Linux standard, sometimes macOS)
23
- if (process.env.XDG_RUNTIME_DIR) {
24
- return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME, 'server.sock');
25
- }
26
- // 2. macOS/Unix Fallback: Use /tmp/
27
- // We use /tmp/ because it is much shorter than ~/Library/Application Support/
28
- // and keeps us well under the 104-character limit.
29
- return path.join('/tmp', `${APP_NAME}-${uid}.sock`);
30
- }
31
- export function getRuntimeHome() {
32
- const platform = os.platform();
33
- const uid = os.userInfo().uid;
34
- // 1. Check for the modern Unix standard
35
- if (process.env.XDG_RUNTIME_DIR) {
36
- return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME);
37
- }
38
- // 2. Fallback for macOS and older Linux
39
- if (platform === 'darwin' || platform === 'linux') {
40
- // /tmp is cleared on boot, making it perfect for PIDs
41
- return path.join('/tmp', `${APP_NAME}-${uid}`);
42
- }
43
- // 3. Windows Fallback
44
- return path.join(os.tmpdir(), APP_NAME);
45
- }
46
- export const IS_WINDOWS = os.platform() === 'win32';
47
- export function getPidFilePath() {
48
- const runtimeDir = getRuntimeHome();
49
- return path.join(runtimeDir, 'daemon.pid');
50
- }
51
- export function getDaemonPid() {
52
- try {
53
- const pidFile = getPidFilePath();
54
- logger(`Daemon pid file ${pidFile}`);
55
- if (!fs.existsSync(pidFile)) {
56
- return null;
57
- }
58
- const pidContent = fs.readFileSync(pidFile, 'utf-8');
59
- const pid = parseInt(pidContent.trim(), 10);
60
- logger(`Daemon pid: ${pid}`);
61
- if (isNaN(pid)) {
62
- return null;
63
- }
64
- return pid;
65
- }
66
- catch {
67
- return null;
68
- }
69
- }
70
- export function isDaemonRunning(pid = getDaemonPid()) {
71
- if (pid) {
72
- try {
73
- process.kill(pid, 0); // Throws if process doesn't exist
74
- return true;
75
- }
76
- catch {
77
- // Process is dead, stale PID file. Proceed with startup.
78
- }
79
- }
80
- return false;
81
- }
82
- export function serializeArgs(options, argv) {
83
- const args = [];
84
- for (const key of Object.keys(options)) {
85
- if (argv[key] === undefined || argv[key] === null) {
86
- continue;
87
- }
88
- const value = argv[key];
89
- const kebabKey = key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
90
- if (typeof value === 'boolean') {
91
- if (value) {
92
- args.push(`--${kebabKey}`);
93
- }
94
- else {
95
- args.push(`--no-${kebabKey}`);
96
- }
97
- }
98
- else if (Array.isArray(value)) {
99
- for (const item of value) {
100
- args.push(`--${kebabKey}`, String(item));
101
- }
102
- }
103
- else {
104
- args.push(`--${kebabKey}`, String(value));
105
- }
106
- }
107
- return args;
108
- }