surf-cli 2.7.2 → 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/README.md +110 -9
- package/native/browser-lock.cjs +169 -0
- package/native/chatgpt-client.cjs +18 -1
- package/native/cli.cjs +670 -279
- package/native/config.cjs +2 -2
- package/native/do-executor.cjs +2 -9
- package/native/do-parser.cjs +12 -0
- package/native/doctor.cjs +583 -0
- package/native/gemini-client.cjs +91 -20
- package/native/grok-client.cjs +270 -170
- package/native/host-helpers.cjs +51 -4
- package/native/host.cjs +22 -7
- package/native/mcp-server.cjs +10 -7
- package/native/socket-path.cjs +46 -0
- package/package.json +5 -5
- package/scripts/install-native-host.cjs +155 -53
- package/scripts/uninstall-native-host.cjs +93 -15
- package/skills/surf/SKILL.md +46 -18
package/native/host-helpers.cjs
CHANGED
|
@@ -2,6 +2,14 @@ const fs = require("fs");
|
|
|
2
2
|
const networkFormatters = require("./formatters/network.cjs");
|
|
3
3
|
const networkStore = require("./network-store.cjs");
|
|
4
4
|
|
|
5
|
+
function buildProviderUploadMessage(provider, tabId, filePaths, id) {
|
|
6
|
+
const normalizedProvider = String(provider || "").toLowerCase();
|
|
7
|
+
if (!["chatgpt", "gemini"].includes(normalizedProvider)) {
|
|
8
|
+
throw new Error(`Unsupported upload provider: ${provider}`);
|
|
9
|
+
}
|
|
10
|
+
return { type: "AI_UPLOAD_FILE_TO_TAB", provider: normalizedProvider, tabId, filePaths, id };
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
function normalizeModelString(model) {
|
|
6
14
|
return String(model || "").trim().toLowerCase();
|
|
7
15
|
}
|
|
@@ -342,6 +350,19 @@ function formatToolContent(result, log = () => {}) {
|
|
|
342
350
|
}
|
|
343
351
|
return text(output);
|
|
344
352
|
}
|
|
353
|
+
|
|
354
|
+
if (
|
|
355
|
+
result.scrollTop !== undefined &&
|
|
356
|
+
result.scrollHeight !== undefined &&
|
|
357
|
+
result.scrollPercentage === undefined
|
|
358
|
+
) {
|
|
359
|
+
return text(`Scrolled to Y:${result.scrollTop} (page height: ${result.scrollHeight})`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (result.scrollY !== undefined) {
|
|
363
|
+
const pageHeight = result.pageHeight !== undefined ? ` (page height: ${result.pageHeight})` : "";
|
|
364
|
+
return text(`Scrolled to Y:${result.scrollY}${pageHeight}`);
|
|
365
|
+
}
|
|
345
366
|
|
|
346
367
|
if (result.success && result.name && result.tabId !== undefined) {
|
|
347
368
|
return text(`Registered tab ${result.tabId} as "${result.name}"`);
|
|
@@ -478,14 +499,15 @@ function mapComputerAction(args, tabId) {
|
|
|
478
499
|
return { type: "FIND_AND_TYPE", text, submit: a.submit ?? false, submitKey: a.submitKey || "Enter", ...baseMsg };
|
|
479
500
|
|
|
480
501
|
case "scroll": {
|
|
481
|
-
const
|
|
502
|
+
const direction = a.direction || scroll_direction;
|
|
503
|
+
const amount = a.scroll_pixels ?? ((a.amount ?? scroll_amount ?? 3) * 100);
|
|
482
504
|
const deltas = {
|
|
483
505
|
up: { deltaX: 0, deltaY: -amount },
|
|
484
506
|
down: { deltaX: 0, deltaY: amount },
|
|
485
507
|
left: { deltaX: -amount, deltaY: 0 },
|
|
486
508
|
right: { deltaX: amount, deltaY: 0 },
|
|
487
509
|
};
|
|
488
|
-
const { deltaX, deltaY } = deltas[
|
|
510
|
+
const { deltaX, deltaY } = deltas[direction] || { deltaX: 0, deltaY: 0 };
|
|
489
511
|
return { type: "EXECUTE_SCROLL", deltaX, deltaY, x: coordinate?.[0], y: coordinate?.[1], ...baseMsg };
|
|
490
512
|
}
|
|
491
513
|
|
|
@@ -575,7 +597,7 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
575
597
|
type: "EXECUTE_SCREENSHOT",
|
|
576
598
|
savePath: a.savePath || a.output, // Accept both savePath (CLI) and output (MCP)
|
|
577
599
|
annotate: a.annotate || false,
|
|
578
|
-
fullpage: a.fullpage || false,
|
|
600
|
+
fullpage: a.fullpage || a["full-page"] || false,
|
|
579
601
|
maxHeight: a["max-height"] || 4000,
|
|
580
602
|
fullRes: a.full || false,
|
|
581
603
|
maxSize: a["max-size"] || 1200,
|
|
@@ -583,6 +605,31 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
583
605
|
};
|
|
584
606
|
case "javascript_tool":
|
|
585
607
|
return { type: "EXECUTE_JAVASCRIPT", code: a.code, ...baseMsg };
|
|
608
|
+
case "animate-audit": {
|
|
609
|
+
if (!a.selector || typeof a.selector !== "string") throw new Error("selector required");
|
|
610
|
+
if (typeof a.duration === "boolean") throw new Error("duration must be a number");
|
|
611
|
+
if (typeof a.fps === "boolean") throw new Error("fps must be a number");
|
|
612
|
+
const durationMs = a.duration !== undefined ? Number(a.duration) : 2000;
|
|
613
|
+
const fps = a.fps !== undefined ? Number(a.fps) : 10;
|
|
614
|
+
if (!Number.isFinite(durationMs) || durationMs < 100 || durationMs > 10000) {
|
|
615
|
+
throw new Error("duration must be between 100 and 10000 ms");
|
|
616
|
+
}
|
|
617
|
+
if (!Number.isFinite(fps) || fps < 1 || fps > 30) {
|
|
618
|
+
throw new Error("fps must be between 1 and 30");
|
|
619
|
+
}
|
|
620
|
+
return { type: "ANIMATE_AUDIT", selector: a.selector, durationMs, fps, ...baseMsg };
|
|
621
|
+
}
|
|
622
|
+
case "perf-audit": {
|
|
623
|
+
if (typeof a.duration === "boolean") throw new Error("duration must be a number");
|
|
624
|
+
if (a.trigger !== undefined && typeof a.trigger !== "string") {
|
|
625
|
+
throw new Error("trigger must be action:target");
|
|
626
|
+
}
|
|
627
|
+
const durationMs = a.duration !== undefined ? Number(a.duration) : 3000;
|
|
628
|
+
if (!Number.isFinite(durationMs) || durationMs < 100 || durationMs > 10000) {
|
|
629
|
+
throw new Error("duration must be between 100 and 10000 ms");
|
|
630
|
+
}
|
|
631
|
+
return { type: "PERF_AUDIT", durationMs, trigger: a.trigger, ...baseMsg };
|
|
632
|
+
}
|
|
586
633
|
case "wait_for_element":
|
|
587
634
|
return {
|
|
588
635
|
type: "WAIT_FOR_ELEMENT",
|
|
@@ -1132,4 +1179,4 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
1132
1179
|
}
|
|
1133
1180
|
}
|
|
1134
1181
|
|
|
1135
|
-
module.exports = { mapToolToMessage, mapComputerAction, formatToolContent };
|
|
1182
|
+
module.exports = { mapToolToMessage, mapComputerAction, formatToolContent, buildProviderUploadMessage };
|
package/native/host.cjs
CHANGED
|
@@ -12,11 +12,10 @@ const perplexityClient = require("./perplexity-client.cjs");
|
|
|
12
12
|
const grokClient = require("./grok-client.cjs");
|
|
13
13
|
const aistudioClient = require("./aistudio-client.cjs");
|
|
14
14
|
const aistudioBuild = require("./aistudio-build.cjs");
|
|
15
|
-
const { mapToolToMessage, mapComputerAction, formatToolContent } = require("./host-helpers.cjs");
|
|
15
|
+
const { mapToolToMessage, mapComputerAction, formatToolContent, buildProviderUploadMessage } = require("./host-helpers.cjs");
|
|
16
16
|
|
|
17
17
|
const IS_WIN = process.platform === "win32";
|
|
18
|
-
const SURF_TMP =
|
|
19
|
-
const SOCKET_PATH = IS_WIN ? "//./pipe/surf" : "/tmp/surf.sock";
|
|
18
|
+
const { SOCKET_PATH, SURF_TMP } = require("./socket-path.cjs");
|
|
20
19
|
if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
|
|
21
20
|
|
|
22
21
|
// Cross-platform image resize (macOS: sips, Linux: ImageMagick)
|
|
@@ -534,6 +533,16 @@ function handleToolRequest(msg, socket) {
|
|
|
534
533
|
});
|
|
535
534
|
writeMessage({ type: "CHATGPT_CDP_COMMAND", tabId, method, params, id: cmdId });
|
|
536
535
|
}),
|
|
536
|
+
uploadFile: (tabId, filePaths) => new Promise((resolve) => {
|
|
537
|
+
const uploadId = ++requestCounter;
|
|
538
|
+
pendingToolRequests.set(uploadId, {
|
|
539
|
+
socket: null,
|
|
540
|
+
originalId: null,
|
|
541
|
+
tool: "upload_file",
|
|
542
|
+
onComplete: (r) => resolve(r)
|
|
543
|
+
});
|
|
544
|
+
writeMessage(buildProviderUploadMessage("chatgpt", tabId, filePaths, uploadId));
|
|
545
|
+
}),
|
|
537
546
|
log: (msg) => log(`[chatgpt] ${msg}`)
|
|
538
547
|
});
|
|
539
548
|
|
|
@@ -736,7 +745,7 @@ function handleToolRequest(msg, socket) {
|
|
|
736
745
|
tool: "upload_file",
|
|
737
746
|
onComplete: (r) => resolve(r)
|
|
738
747
|
});
|
|
739
|
-
writeMessage(
|
|
748
|
+
writeMessage(buildProviderUploadMessage("gemini", tabId, filePaths, uploadId));
|
|
740
749
|
}),
|
|
741
750
|
fetchUrl: (url) => new Promise((resolve) => {
|
|
742
751
|
const fetchId = ++requestCounter;
|
|
@@ -941,19 +950,25 @@ function handleToolRequest(msg, socket) {
|
|
|
941
950
|
}).then((result) => {
|
|
942
951
|
// If --save-models flag was passed and we found models, save them
|
|
943
952
|
if (saveModels && result.models && result.models.length > 0) {
|
|
944
|
-
// Convert scraped model names to
|
|
953
|
+
// Convert scraped model names to selectable IDs.
|
|
945
954
|
const modelMap = {};
|
|
955
|
+
const defaultModels = Object.values(grokClient.DEFAULT_GROK_MODELS || {});
|
|
946
956
|
result.models.forEach(name => {
|
|
947
957
|
const nameLower = name.toLowerCase();
|
|
958
|
+
const normalizedName = grokClient.normalizeGrokModelLabel(name);
|
|
959
|
+
const knownModel = defaultModels.find(model => {
|
|
960
|
+
const normalizedDefaultName = grokClient.normalizeGrokModelLabel(model.name);
|
|
961
|
+
return normalizedName.includes(normalizedDefaultName) || normalizedDefaultName.includes(normalizedName);
|
|
962
|
+
});
|
|
948
963
|
// Match known model keywords to generate consistent short IDs
|
|
949
964
|
let shortId;
|
|
950
|
-
if (
|
|
965
|
+
if (knownModel) shortId = knownModel.id;
|
|
951
966
|
else if (nameLower.includes('expert')) shortId = 'expert';
|
|
952
967
|
else if (nameLower.includes('fast')) shortId = 'fast';
|
|
953
968
|
else if (nameLower.includes('auto')) shortId = 'auto';
|
|
954
969
|
else shortId = nameLower.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
955
970
|
|
|
956
|
-
modelMap[shortId] = { id: shortId, name: name, desc: "" };
|
|
971
|
+
modelMap[shortId] = { id: shortId, name: name, desc: knownModel?.desc || "" };
|
|
957
972
|
});
|
|
958
973
|
const saveResult = grokClient.saveModels(modelMap);
|
|
959
974
|
result.savedModels = saveResult;
|
package/native/mcp-server.cjs
CHANGED
|
@@ -3,8 +3,8 @@ const net = require("net");
|
|
|
3
3
|
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
4
4
|
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
5
5
|
const { z } = require("zod");
|
|
6
|
+
const { SOCKET_PATH, formatSocketError } = require("./socket-path.cjs");
|
|
6
7
|
|
|
7
|
-
const SOCKET_PATH = process.platform === "win32" ? "//./pipe/surf" : "/tmp/surf.sock";
|
|
8
8
|
const REQUEST_TIMEOUT = 30000;
|
|
9
9
|
|
|
10
10
|
const TOOL_SCHEMAS = {
|
|
@@ -279,7 +279,9 @@ function sendSocketRequest(tool, args = {}) {
|
|
|
279
279
|
});
|
|
280
280
|
|
|
281
281
|
let buf = "";
|
|
282
|
+
let settled = false;
|
|
282
283
|
const timeout = setTimeout(() => {
|
|
284
|
+
settled = true;
|
|
283
285
|
sock.destroy();
|
|
284
286
|
reject(new Error("Request timeout"));
|
|
285
287
|
}, REQUEST_TIMEOUT);
|
|
@@ -291,11 +293,13 @@ function sendSocketRequest(tool, args = {}) {
|
|
|
291
293
|
for (const line of lines) {
|
|
292
294
|
if (!line.trim()) continue;
|
|
293
295
|
try {
|
|
296
|
+
settled = true;
|
|
294
297
|
clearTimeout(timeout);
|
|
295
298
|
const resp = JSON.parse(line);
|
|
296
299
|
sock.end();
|
|
297
300
|
resolve(resp);
|
|
298
301
|
} catch {
|
|
302
|
+
settled = true;
|
|
299
303
|
clearTimeout(timeout);
|
|
300
304
|
sock.end();
|
|
301
305
|
reject(new Error("Invalid JSON response"));
|
|
@@ -304,17 +308,16 @@ function sendSocketRequest(tool, args = {}) {
|
|
|
304
308
|
});
|
|
305
309
|
|
|
306
310
|
sock.on("error", (e) => {
|
|
311
|
+
settled = true;
|
|
307
312
|
clearTimeout(timeout);
|
|
308
|
-
|
|
309
|
-
reject(new Error("Socket not found. Is Chrome running with the surf extension?"));
|
|
310
|
-
} else {
|
|
311
|
-
reject(e);
|
|
312
|
-
}
|
|
313
|
+
reject(new Error(formatSocketError(e)));
|
|
313
314
|
});
|
|
314
315
|
|
|
315
316
|
sock.on("close", () => {
|
|
316
317
|
clearTimeout(timeout);
|
|
317
|
-
|
|
318
|
+
if (!settled) {
|
|
319
|
+
reject(new Error(`Socket closed unexpectedly\nAttempted socket: ${SOCKET_PATH}`));
|
|
320
|
+
}
|
|
318
321
|
});
|
|
319
322
|
});
|
|
320
323
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
|
|
4
|
+
const IS_WIN = process.platform === "win32";
|
|
5
|
+
const DEFAULT_SOCKET_PATH = IS_WIN ? "//./pipe/surf" : "/tmp/surf.sock";
|
|
6
|
+
const SOCKET_PATH = process.env.SURF_SOCKET || DEFAULT_SOCKET_PATH;
|
|
7
|
+
const SURF_TMP = IS_WIN ? path.join(os.tmpdir(), "surf") : "/tmp";
|
|
8
|
+
|
|
9
|
+
function getSocketTroubleshootingHint() {
|
|
10
|
+
const lines = [
|
|
11
|
+
`Attempted socket: ${SOCKET_PATH}`,
|
|
12
|
+
"Make sure the browser is running with the Surf extension enabled, then restart the browser after native host install changes.",
|
|
13
|
+
"Run `surf doctor --browser all` for detailed native host diagnostics.",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
if (process.env.SURF_SOCKET) {
|
|
17
|
+
lines.push("SURF_SOCKET is set; make sure the native host and CLI use the same value.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (process.platform === "linux") {
|
|
21
|
+
lines.push("On WSL2 with Windows Chrome, run `surf install <extension-id>` from WSL and restart Windows Chrome.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatSocketError(error, context = "connect") {
|
|
28
|
+
let message;
|
|
29
|
+
if (error && error.code === "ENOENT") {
|
|
30
|
+
message = "Socket not found.";
|
|
31
|
+
} else if (error && error.code === "ECONNREFUSED") {
|
|
32
|
+
message = "Connection refused. Native host is not accepting connections.";
|
|
33
|
+
} else {
|
|
34
|
+
message = error && error.message ? error.message : String(error);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return `Socket ${context} failed: ${message}\n${getSocketTroubleshootingHint()}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
DEFAULT_SOCKET_PATH,
|
|
42
|
+
SOCKET_PATH,
|
|
43
|
+
SURF_TMP,
|
|
44
|
+
formatSocketError,
|
|
45
|
+
getSocketTroubleshootingHint,
|
|
46
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "surf-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chrome",
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@biomejs/biome": "^2.4.4",
|
|
64
64
|
"@types/chrome": "^0.1.37",
|
|
65
|
-
"@vitest/coverage-v8": "^4.
|
|
66
|
-
"@vitest/ui": "^4.
|
|
67
|
-
"typescript": "^
|
|
65
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
66
|
+
"@vitest/ui": "^4.1.9",
|
|
67
|
+
"typescript": "^6.0.3",
|
|
68
68
|
"vite": "^7.3.1",
|
|
69
|
-
"vitest": "^4.
|
|
69
|
+
"vitest": "^4.1.9"
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const os = require("os");
|
|
5
|
-
const {
|
|
5
|
+
const { execFileSync, execSync } = require("child_process");
|
|
6
6
|
|
|
7
7
|
const HOST_NAME = "surf.browser.host";
|
|
8
8
|
|
|
@@ -12,12 +12,14 @@ const BROWSERS = {
|
|
|
12
12
|
darwin: "Library/Application Support/Google/Chrome/NativeMessagingHosts",
|
|
13
13
|
linux: ".config/google-chrome/NativeMessagingHosts",
|
|
14
14
|
win32: "Google\\Chrome",
|
|
15
|
+
wsl: "Google/Chrome/User Data/NativeMessagingHosts",
|
|
15
16
|
},
|
|
16
17
|
chromium: {
|
|
17
18
|
name: "Chromium",
|
|
18
19
|
darwin: "Library/Application Support/Chromium/NativeMessagingHosts",
|
|
19
20
|
linux: ".config/chromium/NativeMessagingHosts",
|
|
20
21
|
win32: "Chromium",
|
|
22
|
+
wsl: "Chromium/User Data/NativeMessagingHosts",
|
|
21
23
|
},
|
|
22
24
|
brave: {
|
|
23
25
|
name: "Brave",
|
|
@@ -25,12 +27,14 @@ const BROWSERS = {
|
|
|
25
27
|
"Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts",
|
|
26
28
|
linux: ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts",
|
|
27
29
|
win32: "BraveSoftware\\Brave-Browser",
|
|
30
|
+
wsl: "BraveSoftware/Brave-Browser/User Data/NativeMessagingHosts",
|
|
28
31
|
},
|
|
29
32
|
edge: {
|
|
30
33
|
name: "Microsoft Edge",
|
|
31
34
|
darwin: "Library/Application Support/Microsoft Edge/NativeMessagingHosts",
|
|
32
35
|
linux: ".config/microsoft-edge/NativeMessagingHosts",
|
|
33
36
|
win32: "Microsoft\\Edge",
|
|
37
|
+
wsl: "Microsoft/Edge/User Data/NativeMessagingHosts",
|
|
34
38
|
},
|
|
35
39
|
arc: {
|
|
36
40
|
name: "Arc",
|
|
@@ -38,12 +42,14 @@ const BROWSERS = {
|
|
|
38
42
|
"Library/Application Support/Arc/User Data/NativeMessagingHosts",
|
|
39
43
|
linux: null,
|
|
40
44
|
win32: null,
|
|
45
|
+
wsl: null,
|
|
41
46
|
},
|
|
42
47
|
helium: {
|
|
43
48
|
name: "Helium",
|
|
44
49
|
darwin: "Library/Application Support/net.imput.helium/NativeMessagingHosts",
|
|
45
50
|
linux: null,
|
|
46
51
|
win32: null,
|
|
52
|
+
wsl: null,
|
|
47
53
|
},
|
|
48
54
|
};
|
|
49
55
|
|
|
@@ -63,6 +69,16 @@ const NODE_PATHS = {
|
|
|
63
69
|
],
|
|
64
70
|
};
|
|
65
71
|
|
|
72
|
+
function isWsl() {
|
|
73
|
+
if (process.platform !== "linux") return false;
|
|
74
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
|
|
75
|
+
try {
|
|
76
|
+
return /microsoft|wsl/i.test(fs.readFileSync("/proc/version", "utf8"));
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
function findNode() {
|
|
67
83
|
if (process.env.SURF_NODE_PATH && fs.existsSync(process.env.SURF_NODE_PATH)) {
|
|
68
84
|
return process.env.SURF_NODE_PATH;
|
|
@@ -88,10 +104,14 @@ function findNpmGlobalRoot() {
|
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
|
|
91
|
-
function getWrapperDir() {
|
|
92
|
-
const platform = process.platform;
|
|
107
|
+
function getWrapperDir(target = process.platform) {
|
|
93
108
|
const home = os.homedir();
|
|
94
|
-
|
|
109
|
+
if (target === "wsl-windows") {
|
|
110
|
+
const localAppData = getWindowsEnv("LOCALAPPDATA");
|
|
111
|
+
if (!localAppData) return null;
|
|
112
|
+
return path.join(windowsPathToWslPath(localAppData), "surf-cli");
|
|
113
|
+
}
|
|
114
|
+
switch (process.platform) {
|
|
95
115
|
case "darwin":
|
|
96
116
|
return path.join(home, "Library/Application Support/surf-cli");
|
|
97
117
|
case "linux":
|
|
@@ -117,72 +137,123 @@ function getHostPath() {
|
|
|
117
137
|
return null;
|
|
118
138
|
}
|
|
119
139
|
|
|
120
|
-
function
|
|
121
|
-
|
|
140
|
+
function getWindowsEnv(name) {
|
|
141
|
+
try {
|
|
142
|
+
return execFileSync("cmd.exe", ["/c", "echo", `%${name}%`], { encoding: "utf8" })
|
|
143
|
+
.trim()
|
|
144
|
+
.replace(/\r/g, "");
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function windowsPathToWslPath(winPath) {
|
|
151
|
+
const normalized = winPath.replace(/\\/g, "/");
|
|
152
|
+
const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
|
153
|
+
if (!match) return normalized;
|
|
154
|
+
return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function wslPathToWindowsPath(wslPath) {
|
|
158
|
+
try {
|
|
159
|
+
return execFileSync("wslpath", ["-w", wslPath], { encoding: "utf8" }).trim().replace(/\r/g, "");
|
|
160
|
+
} catch {
|
|
161
|
+
const match = wslPath.match(/^\/mnt\/([a-zA-Z])\/(.*)$/);
|
|
162
|
+
if (match) return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
163
|
+
return wslPath;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function createWrapper(wrapperDir, nodePath, hostPath, target = process.platform) {
|
|
122
168
|
fs.mkdirSync(wrapperDir, { recursive: true });
|
|
123
169
|
|
|
124
|
-
if (
|
|
170
|
+
if (target === "wsl-windows") {
|
|
171
|
+
const cmdPath = path.join(wrapperDir, "host-wrapper-wsl.cmd");
|
|
172
|
+
const distroArg = process.env.WSL_DISTRO_NAME ? ` -d "${process.env.WSL_DISTRO_NAME}"` : "";
|
|
173
|
+
const content = `@echo off\r\nwsl.exe${distroArg} --cd "${path.dirname(hostPath)}" --exec "${nodePath}" "${hostPath}" %*\r\n`;
|
|
174
|
+
fs.writeFileSync(cmdPath, content);
|
|
175
|
+
return wslPathToWindowsPath(cmdPath);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (process.platform === "win32") {
|
|
125
179
|
const batPath = path.join(wrapperDir, "host-wrapper.bat");
|
|
126
|
-
const content = `@echo off\r\n"${nodePath}" "${hostPath}"
|
|
180
|
+
const content = `@echo off\r\n"${nodePath}" "${hostPath}" %*\r\n`;
|
|
127
181
|
fs.writeFileSync(batPath, content);
|
|
128
182
|
return batPath;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const shPath = path.join(wrapperDir, "host-wrapper.sh");
|
|
186
|
+
const hostDir = path.dirname(hostPath);
|
|
187
|
+
const content = `#!/usr/bin/env bash
|
|
133
188
|
cd "${hostDir}"
|
|
134
|
-
exec "${nodePath}" "${hostPath}"
|
|
189
|
+
exec "${nodePath}" "${hostPath}" "$@"
|
|
135
190
|
`;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
191
|
+
fs.writeFileSync(shPath, content);
|
|
192
|
+
fs.chmodSync(shPath, "755");
|
|
193
|
+
return shPath;
|
|
140
194
|
}
|
|
141
195
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!browserConfig || !browserConfig[platform]) {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (platform === "win32") {
|
|
151
|
-
return installWindowsRegistry(browser, extensionId, wrapperPath);
|
|
152
|
-
}
|
|
196
|
+
function readExistingManifest(manifestPath) {
|
|
197
|
+
if (!fs.existsSync(manifestPath)) return {};
|
|
198
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
199
|
+
}
|
|
153
200
|
|
|
154
|
-
|
|
155
|
-
|
|
201
|
+
function writeManifest(manifestPath, extensionId, wrapperPath) {
|
|
202
|
+
const origin = `chrome-extension://${extensionId}/`;
|
|
203
|
+
const existing = readExistingManifest(manifestPath);
|
|
204
|
+
const allowedOrigins = Array.isArray(existing.allowed_origins) ? existing.allowed_origins : [];
|
|
156
205
|
|
|
157
206
|
const manifest = {
|
|
207
|
+
...existing,
|
|
158
208
|
name: HOST_NAME,
|
|
159
|
-
description: "Surf CLI Native Host",
|
|
209
|
+
description: existing.description || "Surf CLI Native Host",
|
|
160
210
|
path: wrapperPath,
|
|
161
211
|
type: "stdio",
|
|
162
|
-
allowed_origins: [
|
|
212
|
+
allowed_origins: Array.from(new Set([...allowedOrigins, origin])),
|
|
163
213
|
};
|
|
164
214
|
|
|
165
|
-
|
|
215
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
166
216
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
167
217
|
return manifestPath;
|
|
168
218
|
}
|
|
169
219
|
|
|
220
|
+
function getWslWindowsManifestDir(browserConfig) {
|
|
221
|
+
const localAppData = getWindowsEnv("LOCALAPPDATA");
|
|
222
|
+
if (!localAppData || !browserConfig.wsl) return null;
|
|
223
|
+
return path.join(windowsPathToWslPath(localAppData), browserConfig.wsl);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function installManifest(browser, extensionId, wrapperPath, target) {
|
|
227
|
+
const browserConfig = BROWSERS[browser];
|
|
228
|
+
|
|
229
|
+
if (!browserConfig) return null;
|
|
230
|
+
|
|
231
|
+
if (target === "wsl-windows") {
|
|
232
|
+
const manifestDir = getWslWindowsManifestDir(browserConfig);
|
|
233
|
+
if (!manifestDir) return null;
|
|
234
|
+
const manifestPath = path.join(manifestDir, `${HOST_NAME}.json`);
|
|
235
|
+
return writeManifest(manifestPath, extensionId, wrapperPath);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const platform = process.platform;
|
|
239
|
+
if (!browserConfig[platform]) return null;
|
|
240
|
+
|
|
241
|
+
if (platform === "win32") {
|
|
242
|
+
return installWindowsRegistry(browser, extensionId, wrapperPath);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const manifestDir = path.join(os.homedir(), browserConfig[platform]);
|
|
246
|
+
const manifestPath = path.join(manifestDir, `${HOST_NAME}.json`);
|
|
247
|
+
return writeManifest(manifestPath, extensionId, wrapperPath);
|
|
248
|
+
}
|
|
249
|
+
|
|
170
250
|
function installWindowsRegistry(browser, extensionId, wrapperPath) {
|
|
171
251
|
const browserConfig = BROWSERS[browser];
|
|
172
252
|
const regPath = `HKCU\\Software\\${browserConfig.win32}\\NativeMessagingHosts\\${HOST_NAME}`;
|
|
173
253
|
|
|
174
254
|
const manifestDir = getWrapperDir();
|
|
175
255
|
const manifestPath = path.join(manifestDir, `${HOST_NAME}.json`);
|
|
176
|
-
|
|
177
|
-
const manifest = {
|
|
178
|
-
name: HOST_NAME,
|
|
179
|
-
description: "Surf CLI Native Host",
|
|
180
|
-
path: wrapperPath,
|
|
181
|
-
type: "stdio",
|
|
182
|
-
allowed_origins: [`chrome-extension://${extensionId}/`],
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
256
|
+
writeManifest(manifestPath, extensionId, wrapperPath);
|
|
186
257
|
|
|
187
258
|
try {
|
|
188
259
|
execSync(`reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath}" /f`, {
|
|
@@ -197,7 +268,7 @@ function installWindowsRegistry(browser, extensionId, wrapperPath) {
|
|
|
197
268
|
|
|
198
269
|
function parseArgs() {
|
|
199
270
|
const args = process.argv.slice(2);
|
|
200
|
-
const result = { extensionId: null, browsers: ["chrome"] };
|
|
271
|
+
const result = { extensionId: null, browsers: ["chrome"], target: "auto" };
|
|
201
272
|
|
|
202
273
|
for (let i = 0; i < args.length; i++) {
|
|
203
274
|
const arg = args[i];
|
|
@@ -208,6 +279,8 @@ function parseArgs() {
|
|
|
208
279
|
} else {
|
|
209
280
|
result.browsers = browserArg.split(",").map((b) => b.trim().toLowerCase());
|
|
210
281
|
}
|
|
282
|
+
} else if (arg === "--target") {
|
|
283
|
+
result.target = args[++i];
|
|
211
284
|
} else if (arg === "--help" || arg === "-h") {
|
|
212
285
|
printHelp();
|
|
213
286
|
process.exit(0);
|
|
@@ -232,20 +305,23 @@ Options:
|
|
|
232
305
|
-b, --browser Browser(s) to install for (default: chrome)
|
|
233
306
|
Values: chrome, chromium, brave, edge, arc, helium, all
|
|
234
307
|
Multiple: --browser chrome,brave
|
|
308
|
+
--target Install target: auto, linux, windows
|
|
309
|
+
On WSL2, auto installs for Windows Chrome. Use linux for WSLg/Linux browsers.
|
|
235
310
|
|
|
236
311
|
Examples:
|
|
237
312
|
node install-native-host.cjs abcdefghijklmnopabcdefghijklmnop
|
|
238
313
|
node install-native-host.cjs abcdefghijklmnop --browser brave
|
|
239
314
|
node install-native-host.cjs abcdefghijklmnop --browser all
|
|
315
|
+
node install-native-host.cjs abcdefghijklmnop --target linux
|
|
240
316
|
`);
|
|
241
317
|
}
|
|
242
318
|
|
|
243
319
|
function main() {
|
|
244
|
-
const { extensionId, browsers } = parseArgs();
|
|
320
|
+
const { extensionId, browsers, target } = parseArgs();
|
|
245
321
|
|
|
246
322
|
if (!extensionId) {
|
|
247
323
|
console.error("Error: Extension ID required");
|
|
248
|
-
console.error("Usage: install-native-host.cjs <extension-id> [--browser chrome|chromium|brave|edge|arc|helium|all]");
|
|
324
|
+
console.error("Usage: install-native-host.cjs <extension-id> [--browser chrome|chromium|brave|edge|arc|helium|all] [--target auto|linux|windows]");
|
|
249
325
|
console.error("\nFind your extension ID at chrome://extensions (enable Developer Mode)");
|
|
250
326
|
process.exit(1);
|
|
251
327
|
}
|
|
@@ -256,6 +332,24 @@ function main() {
|
|
|
256
332
|
process.exit(1);
|
|
257
333
|
}
|
|
258
334
|
|
|
335
|
+
if (!["auto", "linux", "windows"].includes(target)) {
|
|
336
|
+
console.error("Error: Invalid --target value. Expected auto, linux, or windows");
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const runningInWsl = isWsl();
|
|
341
|
+
if (target === "windows" && !runningInWsl && process.platform !== "win32") {
|
|
342
|
+
console.error("Error: --target windows is only supported on Windows or WSL2");
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (target === "linux" && process.platform !== "linux") {
|
|
347
|
+
console.error("Error: --target linux is only supported on Linux or WSL2");
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const effectiveTarget = runningInWsl && target !== "linux" ? "wsl-windows" : process.platform;
|
|
352
|
+
|
|
259
353
|
const nodePath = findNode();
|
|
260
354
|
if (!nodePath) {
|
|
261
355
|
console.error("Error: Could not find Node.js");
|
|
@@ -270,19 +364,20 @@ function main() {
|
|
|
270
364
|
process.exit(1);
|
|
271
365
|
}
|
|
272
366
|
|
|
273
|
-
const wrapperDir = getWrapperDir();
|
|
367
|
+
const wrapperDir = getWrapperDir(effectiveTarget);
|
|
274
368
|
if (!wrapperDir) {
|
|
275
|
-
console.error("Error: Unsupported platform");
|
|
369
|
+
console.error("Error: Unsupported platform or Windows interop unavailable");
|
|
276
370
|
process.exit(1);
|
|
277
371
|
}
|
|
278
372
|
|
|
279
|
-
console.log(`Platform: ${process.platform}`);
|
|
373
|
+
console.log(`Platform: ${process.platform}${runningInWsl ? " (WSL2 detected)" : ""}`);
|
|
374
|
+
console.log(`Target: ${effectiveTarget === "wsl-windows" ? "Windows browser from WSL2" : effectiveTarget}`);
|
|
280
375
|
console.log(`Node: ${nodePath}`);
|
|
281
376
|
console.log(`Host: ${hostPath}`);
|
|
282
377
|
console.log(`Wrapper dir: ${wrapperDir}`);
|
|
283
378
|
console.log("");
|
|
284
379
|
|
|
285
|
-
const wrapperPath = createWrapper(wrapperDir, nodePath, hostPath);
|
|
380
|
+
const wrapperPath = createWrapper(wrapperDir, nodePath, hostPath, effectiveTarget);
|
|
286
381
|
console.log(`Created wrapper: ${wrapperPath}`);
|
|
287
382
|
console.log("");
|
|
288
383
|
|
|
@@ -295,7 +390,7 @@ function main() {
|
|
|
295
390
|
continue;
|
|
296
391
|
}
|
|
297
392
|
|
|
298
|
-
const result = installManifest(browser, extensionId, wrapperPath);
|
|
393
|
+
const result = installManifest(browser, extensionId, wrapperPath, effectiveTarget);
|
|
299
394
|
if (result) {
|
|
300
395
|
installed.push({ browser: BROWSERS[browser].name, path: result });
|
|
301
396
|
} else {
|
|
@@ -311,10 +406,17 @@ function main() {
|
|
|
311
406
|
}
|
|
312
407
|
|
|
313
408
|
if (skipped.length > 0) {
|
|
314
|
-
console.log(`\nSkipped (not supported
|
|
409
|
+
console.log(`\nSkipped (not supported for ${effectiveTarget}): ${skipped.join(", ")}`);
|
|
315
410
|
}
|
|
316
411
|
|
|
317
412
|
console.log("\nDone! Restart your browser for changes to take effect.");
|
|
318
413
|
}
|
|
319
414
|
|
|
320
|
-
main
|
|
415
|
+
if (require.main === module) {
|
|
416
|
+
main();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
module.exports = {
|
|
420
|
+
createWrapper,
|
|
421
|
+
writeManifest,
|
|
422
|
+
};
|