surf-cli 2.7.1 → 2.8.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.
package/native/config.cjs CHANGED
@@ -25,10 +25,10 @@ const STARTER_CONFIG = {
25
25
  // {
26
26
  // "grok": {
27
27
  // "models": {
28
- // "thinking": { "id": "thinking", "name": "Grok 4.1 Thinking" },
29
28
  // "auto": { "id": "auto", "name": "Auto" },
30
29
  // "fast": { "id": "fast", "name": "Fast" },
31
- // "expert": { "id": "expert", "name": "Expert" }
30
+ // "expert": { "id": "expert", "name": "Expert" },
31
+ // "grok-4.20-beta": { "id": "grok-4.20-beta", "name": "Grok 4.20 Beta" }
32
32
  // }
33
33
  // }
34
34
  // }
@@ -11,8 +11,7 @@
11
11
  */
12
12
 
13
13
  const net = require("net");
14
-
15
- const SOCKET_PATH = process.platform === "win32" ? "//./pipe/surf" : "/tmp/surf.sock";
14
+ const { SOCKET_PATH, formatSocketError } = require("./socket-path.cjs");
16
15
 
17
16
  // Maximum iterations for loops (safety cap)
18
17
  const MAX_LOOP_ITERATIONS = 100;
@@ -104,13 +103,7 @@ function sendDoRequest(toolName, toolArgs, context = {}) {
104
103
  });
105
104
 
106
105
  sock.on("error", (e) => {
107
- if (e.code === "ENOENT") {
108
- reject(new Error("Socket not found. Is Chrome running with the extension?"));
109
- } else if (e.code === "ECONNREFUSED") {
110
- reject(new Error("Connection refused. Native host not running."));
111
- } else {
112
- reject(e);
113
- }
106
+ reject(new Error(formatSocketError(e)));
114
107
  });
115
108
 
116
109
  const timeoutId = setTimeout(() => {
@@ -165,6 +165,18 @@ function parseCommandLine(line) {
165
165
  } else if (values.length > 1) {
166
166
  args.values = values; // Multiple values as array
167
167
  }
168
+ } else if (cmd === 'scroll') {
169
+ if (firstArg === 'top' || firstArg === 'bottom') {
170
+ cmd = `scroll.${firstArg}`;
171
+ i++;
172
+ } else if (['up', 'down', 'left', 'right'].includes(firstArg)) {
173
+ args.direction = firstArg;
174
+ i++;
175
+ if (i < tokens.length && /^-?\d+$/.test(tokens[i])) {
176
+ args.scroll_pixels = parseInt(tokens[i], 10);
177
+ i++;
178
+ }
179
+ }
168
180
  } else {
169
181
  // Use PRIMARY_ARG_MAP for other commands
170
182
  const primaryKey = PRIMARY_ARG_MAP[cmd];
@@ -0,0 +1,583 @@
1
+ const fs = require("fs");
2
+ const net = require("net");
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const { execFileSync } = require("child_process");
6
+
7
+ const HOST_NAME = "surf.browser.host";
8
+
9
+ const BROWSERS = {
10
+ chrome: {
11
+ name: "Google Chrome",
12
+ darwin: "Library/Application Support/Google/Chrome/NativeMessagingHosts",
13
+ linux: ".config/google-chrome/NativeMessagingHosts",
14
+ win32: "Google\\Chrome",
15
+ wsl: "Google/Chrome/User Data/NativeMessagingHosts",
16
+ },
17
+ chromium: {
18
+ name: "Chromium",
19
+ darwin: "Library/Application Support/Chromium/NativeMessagingHosts",
20
+ linux: ".config/chromium/NativeMessagingHosts",
21
+ win32: "Chromium",
22
+ wsl: "Chromium/User Data/NativeMessagingHosts",
23
+ },
24
+ brave: {
25
+ name: "Brave",
26
+ darwin: "Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts",
27
+ linux: ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts",
28
+ win32: "BraveSoftware\\Brave-Browser",
29
+ wsl: "BraveSoftware/Brave-Browser/User Data/NativeMessagingHosts",
30
+ },
31
+ edge: {
32
+ name: "Microsoft Edge",
33
+ darwin: "Library/Application Support/Microsoft Edge/NativeMessagingHosts",
34
+ linux: ".config/microsoft-edge/NativeMessagingHosts",
35
+ win32: "Microsoft\\Edge",
36
+ wsl: "Microsoft/Edge/User Data/NativeMessagingHosts",
37
+ },
38
+ arc: {
39
+ name: "Arc",
40
+ darwin: "Library/Application Support/Arc/User Data/NativeMessagingHosts",
41
+ linux: null,
42
+ win32: null,
43
+ wsl: null,
44
+ },
45
+ helium: {
46
+ name: "Helium",
47
+ darwin: "Library/Application Support/net.imput.helium/NativeMessagingHosts",
48
+ linux: null,
49
+ win32: null,
50
+ wsl: null,
51
+ },
52
+ };
53
+
54
+ function isWsl({ platform = process.platform, env = process.env, readFileSync = fs.readFileSync } = {}) {
55
+ if (platform !== "linux") return false;
56
+ if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
57
+ try {
58
+ return /microsoft|wsl/i.test(readFileSync("/proc/version", "utf8"));
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ function defaultSocketPath(platform = process.platform) {
65
+ return platform === "win32" ? "//./pipe/surf" : "/tmp/surf.sock";
66
+ }
67
+
68
+ function parseDoctorArgs(rawArgs) {
69
+ const options = {
70
+ browser: "chrome",
71
+ target: "auto",
72
+ json: false,
73
+ socket: undefined,
74
+ connectTimeoutMs: 750,
75
+ };
76
+
77
+ for (let i = 0; i < rawArgs.length; i++) {
78
+ const arg = rawArgs[i];
79
+ if (arg === "--json") {
80
+ options.json = true;
81
+ } else if (arg === "--browser" || arg === "-b") {
82
+ options.browser = rawArgs[++i] || "";
83
+ } else if (arg === "--target") {
84
+ options.target = rawArgs[++i] || "";
85
+ } else if (arg === "--socket") {
86
+ options.socket = rawArgs[++i] || "";
87
+ } else if (arg === "--connect-timeout") {
88
+ const value = Number(rawArgs[++i]);
89
+ if (!Number.isFinite(value) || value < 0) throw new Error("--connect-timeout must be a non-negative number");
90
+ options.connectTimeoutMs = value;
91
+ } else if (arg === "--help" || arg === "-h") {
92
+ options.help = true;
93
+ } else {
94
+ throw new Error(`Unknown doctor option: ${arg}`);
95
+ }
96
+ }
97
+
98
+ if (!options.browser) throw new Error("--browser requires a value");
99
+ if (!options.target) throw new Error("--target requires a value");
100
+ if (!["auto", "linux", "windows"].includes(options.target)) {
101
+ throw new Error("--target must be auto, linux, or windows");
102
+ }
103
+ if (options.socket === "") throw new Error("--socket requires a value");
104
+
105
+ return options;
106
+ }
107
+
108
+ function resolveBrowsers(browserArg) {
109
+ if (browserArg === "all") return Object.keys(BROWSERS);
110
+ const browsers = browserArg.split(",").map((browser) => browser.trim().toLowerCase()).filter(Boolean);
111
+ if (browsers.length === 0) throw new Error("--browser requires a browser name or all");
112
+ const unknown = browsers.filter((browser) => !BROWSERS[browser]);
113
+ if (unknown.length > 0) throw new Error(`Unknown browser: ${unknown.join(", ")}`);
114
+ return browsers;
115
+ }
116
+
117
+ function getWindowsEnv(name, { env = process.env, execFileSync: execFile = execFileSync } = {}) {
118
+ if (env[name]) return env[name];
119
+ try {
120
+ return execFile("cmd.exe", ["/c", "echo", `%${name}%`], { encoding: "utf8" })
121
+ .trim()
122
+ .replace(/\r/g, "");
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ function windowsPathToWslPath(winPath) {
129
+ const normalized = winPath.replace(/\\/g, "/");
130
+ const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
131
+ if (!match) return normalized;
132
+ return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
133
+ }
134
+
135
+ function manifestPathForBrowser(browserKey, context) {
136
+ const browser = BROWSERS[browserKey];
137
+ if (!browser) return null;
138
+
139
+ if (context.effectiveTarget === "wsl-windows") {
140
+ const localAppData = getWindowsEnv("LOCALAPPDATA", context);
141
+ if (!localAppData || !browser.wsl) return null;
142
+ return path.join(windowsPathToWslPath(localAppData), browser.wsl, `${HOST_NAME}.json`);
143
+ }
144
+
145
+ if (context.platform === "win32") {
146
+ if (!browser.win32) return null;
147
+ const localAppData = context.env.LOCALAPPDATA || path.join(context.homeDir, "AppData/Local");
148
+ return path.join(localAppData, "surf-cli", `${HOST_NAME}.json`);
149
+ }
150
+
151
+ const manifestDir = browser[context.platform];
152
+ if (!manifestDir) return null;
153
+ return path.join(context.homeDir, manifestDir, `${HOST_NAME}.json`);
154
+ }
155
+
156
+ function fsPathFromManifestPath(manifestPath, context) {
157
+ if (context.platform === "linux" && /^[A-Za-z]:[\\/]/.test(manifestPath)) {
158
+ return windowsPathToWslPath(manifestPath);
159
+ }
160
+ return manifestPath;
161
+ }
162
+
163
+ function windowsRegistryPathForBrowser(browserKey) {
164
+ const browser = BROWSERS[browserKey];
165
+ if (!browser?.win32) return null;
166
+ return `HKCU\\Software\\${browser.win32}\\NativeMessagingHosts\\${HOST_NAME}`;
167
+ }
168
+
169
+ function readWindowsRegistryManifestPath(registryPath, context) {
170
+ try {
171
+ const output = context.execFileSync("reg", ["query", registryPath, "/ve"], { encoding: "utf8" });
172
+ const line = output.split(/\r?\n/).find((item) => item.includes("REG_SZ"));
173
+ if (!line) return null;
174
+ return line.replace(/^.*REG_SZ\s+/, "").trim() || null;
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+
180
+ function checkWindowsRegistry(browserKey, context) {
181
+ const registryPath = windowsRegistryPathForBrowser(browserKey);
182
+ if (!registryPath) {
183
+ return {
184
+ check: {
185
+ id: "windows-registry-supported",
186
+ status: "fail",
187
+ browser: browserKey,
188
+ message: `${BROWSERS[browserKey].name} native messaging registry is not supported on Windows`,
189
+ },
190
+ manifestPath: null,
191
+ };
192
+ }
193
+
194
+ const manifestPath = readWindowsRegistryManifestPath(registryPath, context);
195
+ return {
196
+ check: {
197
+ id: "windows-registry",
198
+ status: manifestPath ? "pass" : "fail",
199
+ browser: browserKey,
200
+ message: manifestPath
201
+ ? `Windows native messaging registry points to ${manifestPath}`
202
+ : `Windows native messaging registry entry not found: ${registryPath}`,
203
+ registryPath,
204
+ path: manifestPath,
205
+ },
206
+ manifestPath,
207
+ };
208
+ }
209
+
210
+ function checkManifest(manifestPath, context) {
211
+ const checks = [];
212
+ const exists = manifestPath ? context.fs.existsSync(manifestPath) : false;
213
+ checks.push({
214
+ id: "manifest-file",
215
+ status: exists ? "pass" : "fail",
216
+ message: exists ? `Manifest found: ${manifestPath}` : "Native messaging manifest not found",
217
+ path: manifestPath,
218
+ });
219
+
220
+ if (!exists) return { checks, manifest: null };
221
+
222
+ let manifest;
223
+ try {
224
+ manifest = JSON.parse(context.fs.readFileSync(manifestPath, "utf8"));
225
+ checks.push({ id: "manifest-json", status: "pass", message: "Manifest JSON is valid" });
226
+ } catch (error) {
227
+ checks.push({ id: "manifest-json", status: "fail", message: `Manifest JSON is invalid: ${error.message}` });
228
+ return { checks, manifest: null };
229
+ }
230
+
231
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
232
+ checks.push({
233
+ id: "manifest-shape",
234
+ status: "fail",
235
+ message: "Manifest JSON must be an object",
236
+ });
237
+ return { checks, manifest: null };
238
+ }
239
+
240
+ checks.push({
241
+ id: "manifest-name",
242
+ status: manifest.name === HOST_NAME ? "pass" : "fail",
243
+ message: manifest.name === HOST_NAME ? `Manifest name is ${HOST_NAME}` : `Manifest name is ${JSON.stringify(manifest.name)}; expected ${HOST_NAME}`,
244
+ });
245
+ checks.push({
246
+ id: "manifest-type",
247
+ status: manifest.type === "stdio" ? "pass" : "fail",
248
+ message: manifest.type === "stdio" ? "Manifest type is stdio" : `Manifest type is ${JSON.stringify(manifest.type)}; expected stdio`,
249
+ });
250
+
251
+ const origins = Array.isArray(manifest.allowed_origins) ? manifest.allowed_origins : [];
252
+ const chromeOrigins = origins.filter((origin) => /^chrome-extension:\/\/[a-p]{32}\/$/.test(origin));
253
+ checks.push({
254
+ id: "manifest-origins",
255
+ status: chromeOrigins.length > 0 ? "pass" : "fail",
256
+ message: chromeOrigins.length > 0
257
+ ? `Manifest has ${chromeOrigins.length} Chrome extension origin${chromeOrigins.length === 1 ? "" : "s"}`
258
+ : "Manifest has no valid chrome-extension://<extension-id>/ allowed_origins entry",
259
+ origins,
260
+ });
261
+
262
+ if (typeof manifest.path === "string" && manifest.path.length > 0) {
263
+ const manifestFsPath = fsPathFromManifestPath(manifest.path, context);
264
+ const wrapperExists = context.fs.existsSync(manifestFsPath);
265
+ checks.push({
266
+ id: "manifest-path",
267
+ status: wrapperExists ? "pass" : "fail",
268
+ message: wrapperExists ? `Manifest path exists: ${manifest.path}` : `Manifest path does not exist: ${manifest.path}`,
269
+ path: manifest.path,
270
+ fsPath: manifestFsPath,
271
+ });
272
+
273
+ if (wrapperExists && context.platform !== "win32" && !manifest.path.endsWith(".cmd") && !manifest.path.endsWith(".bat")) {
274
+ try {
275
+ const mode = context.fs.statSync(manifestFsPath).mode;
276
+ checks.push({
277
+ id: "manifest-path-executable",
278
+ status: mode & 0o111 ? "pass" : "fail",
279
+ message: mode & 0o111 ? "Manifest wrapper is executable" : "Manifest wrapper exists but is not executable",
280
+ });
281
+ } catch {}
282
+ }
283
+ } else {
284
+ checks.push({ id: "manifest-path", status: "fail", message: "Manifest path is missing" });
285
+ }
286
+
287
+ return { checks, manifest };
288
+ }
289
+
290
+ async function checkSocket(socketPath, context) {
291
+ const checks = [];
292
+ if (context.platform !== "win32") {
293
+ const exists = context.fs.existsSync(socketPath);
294
+ checks.push({
295
+ id: "socket-file",
296
+ status: exists ? "pass" : "fail",
297
+ message: exists ? `Socket path exists: ${socketPath}` : `Socket path does not exist: ${socketPath}`,
298
+ path: socketPath,
299
+ });
300
+
301
+ if (exists) {
302
+ try {
303
+ const stat = context.fs.statSync(socketPath);
304
+ checks.push({
305
+ id: "socket-type",
306
+ status: stat.isSocket() ? "pass" : "warn",
307
+ message: stat.isSocket() ? "Socket path is a Unix socket" : "Socket path exists but is not a Unix socket",
308
+ });
309
+ } catch (error) {
310
+ checks.push({ id: "socket-type", status: "warn", message: `Could not stat socket path: ${error.message}` });
311
+ }
312
+ }
313
+ }
314
+
315
+ const connection = await context.connectSocket(socketPath, context.connectTimeoutMs);
316
+ checks.push({
317
+ id: "socket-connect",
318
+ status: connection.ok ? "pass" : "fail",
319
+ message: connection.ok ? "Connected to native host socket" : `Could not connect to socket: ${connection.message}`,
320
+ code: connection.code,
321
+ });
322
+ return checks;
323
+ }
324
+
325
+ function connectSocket(socketPath, timeoutMs) {
326
+ return new Promise((resolve) => {
327
+ let settled = false;
328
+ const socket = net.createConnection(socketPath);
329
+ const finish = (result) => {
330
+ if (settled) return;
331
+ settled = true;
332
+ clearTimeout(timeout);
333
+ socket.destroy();
334
+ resolve(result);
335
+ };
336
+ const timeout = setTimeout(() => {
337
+ finish({ ok: false, code: "ETIMEDOUT", message: `timed out after ${timeoutMs}ms` });
338
+ }, timeoutMs);
339
+ socket.once("connect", () => finish({ ok: true, message: "connected" }));
340
+ socket.once("error", (error) => finish({
341
+ ok: false,
342
+ code: error.code,
343
+ message: error && error.message ? error.message : String(error),
344
+ }));
345
+ });
346
+ }
347
+
348
+ function resolveEffectiveTarget(options, context) {
349
+ if (options.target === "linux" && context.platform !== "linux") {
350
+ throw new Error("--target linux is only supported on Linux or WSL2");
351
+ }
352
+ if (options.target === "windows" && !context.runningInWsl && context.platform !== "win32") {
353
+ throw new Error("--target windows is only supported on Windows or WSL2");
354
+ }
355
+ return context.runningInWsl && options.target !== "linux" ? "wsl-windows" : context.platform;
356
+ }
357
+
358
+ function statusRank(status) {
359
+ return { fail: 3, warn: 2, pass: 1, info: 0 }[status] || 0;
360
+ }
361
+
362
+ function summarize(checks) {
363
+ return {
364
+ pass: checks.filter((check) => check.status === "pass").length,
365
+ warn: checks.filter((check) => check.status === "warn").length,
366
+ fail: checks.filter((check) => check.status === "fail").length,
367
+ };
368
+ }
369
+
370
+ function buildRecommendations(report) {
371
+ const recommendations = [];
372
+ const failedIds = new Set(report.checks.filter((check) => check.status === "fail").map((check) => check.id));
373
+
374
+ if (failedIds.has("windows-registry")) {
375
+ recommendations.push("Run `surf install <extension-id> --browser <browser>` so Windows registers the native messaging host, then restart the browser.");
376
+ }
377
+ if (failedIds.has("manifest-file") || failedIds.has("manifest-shape")) {
378
+ recommendations.push("Run `surf install <extension-id>` with the extension ID from chrome://extensions, then restart the browser.");
379
+ }
380
+ if (failedIds.has("manifest-origins")) {
381
+ recommendations.push("Rerun `surf install <extension-id>` after copying the current Surf extension ID from chrome://extensions.");
382
+ }
383
+ if (failedIds.has("manifest-path") || failedIds.has("manifest-path-executable")) {
384
+ recommendations.push("Reinstall the native host so the manifest path points at the current Surf wrapper.");
385
+ }
386
+ if (failedIds.has("manifest-supported")) {
387
+ recommendations.push("Choose a browser supported for this target, or rerun with `--browser all` to inspect every supported setup.");
388
+ }
389
+ if (failedIds.has("socket-file") || failedIds.has("socket-connect")) {
390
+ recommendations.push("Make sure the browser is running with the Surf extension enabled, then restart the browser after install changes.");
391
+ }
392
+ if (report.environment.surfSocketSet) {
393
+ recommendations.push("SURF_SOCKET is set; make sure Chrome launches the native host with the same socket value.");
394
+ }
395
+ if (report.environment.runningInWsl && report.environment.effectiveTarget === "wsl-windows") {
396
+ recommendations.push("For WSL2 with Windows Chrome, run `surf install <extension-id>` from the same WSL distro and restart Windows Chrome. Use `--target linux` only for a Linux browser in WSLg.");
397
+ }
398
+ if (report.environment.platform === "darwin") {
399
+ recommendations.push("On macOS, confirm the extension ID in the manifest matches chrome://extensions and reopen the extension service worker console for native messaging errors.");
400
+ }
401
+
402
+ return Array.from(new Set(recommendations));
403
+ }
404
+
405
+ async function runDoctor(rawOptions = {}, deps = {}) {
406
+ const env = deps.env || process.env;
407
+ const platform = deps.platform || process.platform;
408
+ const homeDir = deps.homeDir || os.homedir();
409
+ const runningInWsl = isWsl({ platform, env, readFileSync: deps.readFileSync || fs.readFileSync });
410
+ const options = {
411
+ browser: rawOptions.browser || "chrome",
412
+ target: rawOptions.target || "auto",
413
+ socket: rawOptions.socket || env.SURF_SOCKET || defaultSocketPath(platform),
414
+ connectTimeoutMs: rawOptions.connectTimeoutMs ?? 750,
415
+ };
416
+ const effectiveTarget = resolveEffectiveTarget(options, { platform, runningInWsl });
417
+ const browsers = resolveBrowsers(options.browser);
418
+ const context = {
419
+ platform,
420
+ homeDir,
421
+ env,
422
+ runningInWsl,
423
+ effectiveTarget,
424
+ fs: deps.fs || fs,
425
+ execFileSync: deps.execFileSync || execFileSync,
426
+ connectSocket: deps.connectSocket || connectSocket,
427
+ connectTimeoutMs: options.connectTimeoutMs,
428
+ };
429
+
430
+ const checks = [];
431
+ checks.push({ id: "platform", status: "info", message: `Platform: ${platform}${runningInWsl ? " (WSL2 detected)" : ""}` });
432
+ checks.push({ id: "target", status: "info", message: `Install target: ${effectiveTarget === "wsl-windows" ? "Windows browser from WSL2" : effectiveTarget}` });
433
+ checks.push({ id: "socket-path", status: "info", message: `Socket path: ${options.socket}`, path: options.socket });
434
+
435
+ checks.push(...await checkSocket(options.socket, context));
436
+
437
+ const manifests = [];
438
+ for (const browserKey of browsers) {
439
+ const browser = BROWSERS[browserKey];
440
+ const browserChecks = [];
441
+ let manifestPath = manifestPathForBrowser(browserKey, context);
442
+
443
+ if (context.platform === "win32" && browser.win32) {
444
+ const registry = checkWindowsRegistry(browserKey, context);
445
+ browserChecks.push(registry.check);
446
+ if (registry.manifestPath) manifestPath = registry.manifestPath;
447
+ }
448
+
449
+ if (!manifestPath) {
450
+ const check = {
451
+ id: "manifest-supported",
452
+ status: options.browser === "all" ? "warn" : "fail",
453
+ browser: browserKey,
454
+ message: `${browser.name} native messaging is not supported for ${effectiveTarget}`,
455
+ };
456
+ browserChecks.push(check);
457
+ checks.push(...browserChecks);
458
+ manifests.push({ browser: browserKey, name: browser.name, path: null, supported: false, checks: browserChecks });
459
+ continue;
460
+ }
461
+
462
+ const result = checkManifest(manifestPath, context);
463
+ browserChecks.push(...result.checks.map((check) => ({ ...check, browser: browserKey })));
464
+ checks.push(...browserChecks);
465
+ manifests.push({
466
+ browser: browserKey,
467
+ name: browser.name,
468
+ path: manifestPath,
469
+ supported: true,
470
+ manifest: result.manifest,
471
+ checks: browserChecks,
472
+ });
473
+ }
474
+
475
+ checks.sort((a, b) => statusRank(b.status) - statusRank(a.status));
476
+ const summary = summarize(checks);
477
+ const report = {
478
+ ok: summary.fail === 0,
479
+ summary,
480
+ environment: {
481
+ platform,
482
+ runningInWsl,
483
+ effectiveTarget,
484
+ socketPath: options.socket,
485
+ surfSocketSet: Boolean(env.SURF_SOCKET),
486
+ socketOverrideSet: Boolean(rawOptions.socket),
487
+ browsers,
488
+ },
489
+ manifests,
490
+ checks,
491
+ };
492
+ report.recommendations = buildRecommendations(report);
493
+ return report;
494
+ }
495
+
496
+ function formatCheck(check) {
497
+ const labels = { pass: "PASS", warn: "WARN", fail: "FAIL", info: "INFO" };
498
+ const browser = check.browser ? ` [${check.browser}]` : "";
499
+ return `[${labels[check.status] || check.status.toUpperCase()}]${browser} ${check.message}`;
500
+ }
501
+
502
+ function formatDoctorReport(report) {
503
+ const lines = ["Surf doctor", ""];
504
+ lines.push(`Platform: ${report.environment.platform}${report.environment.runningInWsl ? " (WSL2 detected)" : ""}`);
505
+ lines.push(`Target: ${report.environment.effectiveTarget === "wsl-windows" ? "Windows browser from WSL2" : report.environment.effectiveTarget}`);
506
+ lines.push(`Socket: ${report.environment.socketPath}`);
507
+ lines.push(`Browsers: ${report.environment.browsers.join(", ")}`);
508
+ lines.push("");
509
+
510
+ for (const check of report.checks.filter((item) => item.status !== "info")) {
511
+ lines.push(formatCheck(check));
512
+ }
513
+
514
+ if (report.recommendations.length > 0) {
515
+ lines.push("", "Next steps:");
516
+ for (const recommendation of report.recommendations) {
517
+ lines.push(`- ${recommendation}`);
518
+ }
519
+ }
520
+
521
+ lines.push("", report.ok ? "Doctor result: OK" : "Doctor result: issues found");
522
+ return lines.join("\n");
523
+ }
524
+
525
+ function doctorHelp() {
526
+ return `Usage: surf doctor [options]
527
+
528
+ Diagnose native host and socket setup without requiring a working browser connection.
529
+
530
+ Options:
531
+ -b, --browser <name> Browser to inspect (default: chrome; supports chrome, chromium, brave, edge, arc, helium, all)
532
+ --target <target> Install target to inspect: auto, linux, windows (default: auto)
533
+ --socket <path> Socket path or named pipe to check (default: SURF_SOCKET or platform default)
534
+ --connect-timeout <ms> Socket connection timeout (default: 750)
535
+ --json Print machine-readable diagnostics
536
+
537
+ Examples:
538
+ surf doctor
539
+ surf doctor --browser brave
540
+ surf doctor --browser all --json
541
+ surf doctor --target linux
542
+ `;
543
+ }
544
+
545
+ async function runDoctorCli(rawArgs) {
546
+ let options;
547
+ try {
548
+ options = parseDoctorArgs(rawArgs);
549
+ } catch (error) {
550
+ console.error(`Error: ${error.message}`);
551
+ console.error("Run `surf doctor --help` for usage.");
552
+ return 1;
553
+ }
554
+
555
+ if (options.help) {
556
+ console.log(doctorHelp());
557
+ return 0;
558
+ }
559
+
560
+ try {
561
+ const report = await runDoctor(options);
562
+ if (options.json) {
563
+ console.log(JSON.stringify(report, null, 2));
564
+ } else {
565
+ console.log(formatDoctorReport(report));
566
+ }
567
+ return report.ok ? 0 : 1;
568
+ } catch (error) {
569
+ console.error(`Error: ${error.message}`);
570
+ return 1;
571
+ }
572
+ }
573
+
574
+ module.exports = {
575
+ BROWSERS,
576
+ HOST_NAME,
577
+ doctorHelp,
578
+ formatDoctorReport,
579
+ parseDoctorArgs,
580
+ runDoctor,
581
+ runDoctorCli,
582
+ windowsPathToWslPath,
583
+ };