surf-cli 2.7.2 → 2.9.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 +208 -13
- package/dist/content/index.js +116 -0
- package/dist/content/index.js.map +1 -0
- package/dist/manifest.json +2 -11
- package/dist/options/options.js +3 -3
- package/dist/options/options.js.map +1 -1
- package/dist/service-worker/index.js +261 -61
- package/dist/service-worker/index.js.map +1 -1
- package/native/abort.cjs +65 -0
- package/native/ai-queue.cjs +64 -0
- package/native/aistudio-build.cjs +21 -13
- package/native/aistudio-client.cjs +40 -20
- package/native/browser-lock.cjs +169 -0
- package/native/chatgpt-client.cjs +63 -30
- package/native/cli.cjs +947 -460
- package/native/client-transport.cjs +168 -0
- package/native/config.cjs +2 -2
- package/native/do-executor.cjs +25 -51
- package/native/do-parser.cjs +12 -0
- package/native/doctor.cjs +633 -0
- package/native/endpoint.cjs +174 -0
- package/native/file-transfer.cjs +734 -0
- package/native/gemini-client.cjs +244 -88
- package/native/grok-client.cjs +321 -212
- package/native/host-helpers.cjs +88 -16
- package/native/host-sessions.cjs +283 -0
- package/native/host.cjs +811 -616
- package/native/listener.cjs +20 -0
- package/native/mcp-server.cjs +60 -62
- package/native/network-export.cjs +113 -0
- package/native/perplexity-client.cjs +46 -17
- package/native/remote-auth.cjs +279 -0
- package/native/remote-transport.cjs +337 -0
- package/native/request-pending.cjs +148 -0
- package/native/socket-path.cjs +46 -0
- package/package.json +11 -9
- package/scripts/install-native-host.cjs +184 -51
- package/scripts/uninstall-native-host.cjs +93 -15
- package/skills/README.md +11 -5
- package/skills/deep-x-research/SKILL.md +106 -0
- package/skills/surf/SKILL.md +77 -22
- package/dist/content/accessibility-tree.js +0 -11
- package/dist/content/accessibility-tree.js.map +0 -1
- package/dist/content/visual-indicator.js +0 -111
- package/dist/content/visual-indicator.js.map +0 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const { abortError } = require("./abort.cjs");
|
|
2
|
+
|
|
3
|
+
class RequestPendingMap extends Map {
|
|
4
|
+
constructor({ getRequest = () => undefined } = {}) {
|
|
5
|
+
super();
|
|
6
|
+
this.getRequest = getRequest;
|
|
7
|
+
this.drainWaiters = new Map();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
set(id, data) {
|
|
11
|
+
const request = data.request || this.getRequest();
|
|
12
|
+
const cleanup = Boolean(data.cleanup || data.tool === "close_tab");
|
|
13
|
+
const entry = { ...data, id, request, cleanup, aborted: false, settled: false };
|
|
14
|
+
if (request) {
|
|
15
|
+
if (!request.pendingEntries) request.pendingEntries = new Set();
|
|
16
|
+
request.pendingEntries.add(entry);
|
|
17
|
+
if (cleanup) entry.abortCleanup = null;
|
|
18
|
+
else {
|
|
19
|
+
const onAbort = () => {
|
|
20
|
+
if (entry.abortNotified || entry.settled) return;
|
|
21
|
+
entry.abortNotified = true;
|
|
22
|
+
entry.aborted = true;
|
|
23
|
+
const error = abortError(request.signal);
|
|
24
|
+
if (entry.reject) entry.reject(error);
|
|
25
|
+
else entry.onAbort?.(error);
|
|
26
|
+
if (!entry.reject && !entry.onAbort) entry.onComplete?.({ error: error.message, cancelled: true });
|
|
27
|
+
};
|
|
28
|
+
entry.abortCleanup = () => request.signal.removeEventListener("abort", onAbort);
|
|
29
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
30
|
+
if (request.signal.aborted) onAbort();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
super.set(id, entry);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get(id) {
|
|
38
|
+
return super.get(id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
delete(id) {
|
|
42
|
+
const entry = super.get(id);
|
|
43
|
+
if (!entry) return false;
|
|
44
|
+
this.#removeEntry(entry);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#removeEntry(entry, notify = true) {
|
|
49
|
+
super.delete(entry.id);
|
|
50
|
+
entry.abortCleanup?.();
|
|
51
|
+
if (entry.tombstoneTimer) clearTimeout(entry.tombstoneTimer);
|
|
52
|
+
entry.request?.pendingEntries?.delete(entry);
|
|
53
|
+
if (notify) this.#notifyDrain(entry.request);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#notifyDrain(request) {
|
|
57
|
+
if (!request || request.pendingEntries?.size) return;
|
|
58
|
+
const waiters = this.drainWaiters.get(request);
|
|
59
|
+
if (!waiters) return;
|
|
60
|
+
this.drainWaiters.delete(request);
|
|
61
|
+
for (const waiter of waiters) waiter();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onDrain(request, callback) {
|
|
65
|
+
if (!request?.pendingEntries?.size) {
|
|
66
|
+
callback();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const waiters = this.drainWaiters.get(request) || [];
|
|
70
|
+
waiters.push(callback);
|
|
71
|
+
this.drainWaiters.set(request, waiters);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
resolve(id, value) {
|
|
75
|
+
const entry = this.get(id);
|
|
76
|
+
if (!entry) return false;
|
|
77
|
+
this.#removeEntry(entry, false);
|
|
78
|
+
try {
|
|
79
|
+
if (!entry.aborted && !entry.hardBoundary && !entry.settled) {
|
|
80
|
+
entry.settled = true;
|
|
81
|
+
if (entry.resolve) entry.resolve(value);
|
|
82
|
+
else if (entry.onComplete) entry.onComplete(value);
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
this.#notifyDrain(entry.request);
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
expire(id, error) {
|
|
91
|
+
const entry = this.get(id);
|
|
92
|
+
if (!entry) return false;
|
|
93
|
+
if (!entry.settled) {
|
|
94
|
+
entry.settled = true;
|
|
95
|
+
entry.aborted = true;
|
|
96
|
+
entry.reject?.(error);
|
|
97
|
+
const request = entry.request;
|
|
98
|
+
const remaining = request ? Math.max(0, request.startedAt + request.deadlineMs - Date.now()) : 0;
|
|
99
|
+
entry.tombstoneTimer = setTimeout(() => this.delete(entry.id), remaining);
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
reject(id, error) {
|
|
105
|
+
const entry = this.get(id);
|
|
106
|
+
if (!entry) return false;
|
|
107
|
+
this.#removeEntry(entry, false);
|
|
108
|
+
try {
|
|
109
|
+
if (!entry.settled) {
|
|
110
|
+
entry.settled = true;
|
|
111
|
+
entry.reject?.(error);
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
this.#notifyDrain(entry.request);
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
tombstoneAfterAbort(request) {
|
|
120
|
+
if (!request?.pendingEntries) return;
|
|
121
|
+
for (const entry of request.pendingEntries) {
|
|
122
|
+
if (entry.cleanup || entry.tombstoneTimer) continue;
|
|
123
|
+
const remaining = Math.max(0, request.startedAt + request.deadlineMs - Date.now());
|
|
124
|
+
entry.tombstoneTimer = setTimeout(() => this.delete(entry.id), remaining);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
hardDeadline(request) {
|
|
129
|
+
if (!request) return;
|
|
130
|
+
request.hardBoundary = true;
|
|
131
|
+
for (const entry of [...(request.pendingEntries || [])]) {
|
|
132
|
+
entry.hardBoundary = true;
|
|
133
|
+
this.#removeEntry(entry);
|
|
134
|
+
if (!entry.settled) {
|
|
135
|
+
entry.settled = true;
|
|
136
|
+
entry.reject?.(abortError(request.signal, "Request timed out"));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
clear() {
|
|
142
|
+
for (const entry of this.values()) this.#removeEntry(entry);
|
|
143
|
+
this.drainWaiters.clear();
|
|
144
|
+
super.clear();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { RequestPendingMap };
|
|
@@ -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 = process.env.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.9.0",
|
|
4
4
|
"description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chrome",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"lint:test": "biome check test/",
|
|
44
44
|
"format": "biome format --write .",
|
|
45
45
|
"test": "vitest run",
|
|
46
|
+
"test:e2e:chrome": "node test/e2e/real-chrome.mjs",
|
|
46
47
|
"test:watch": "vitest",
|
|
47
48
|
"test:coverage": "vitest run --coverage",
|
|
48
49
|
"test:ui": "vitest --ui",
|
|
@@ -56,16 +57,17 @@
|
|
|
56
57
|
"crypto-browserify": "^3.12.1",
|
|
57
58
|
"events": "^3.3.0",
|
|
58
59
|
"stream-browserify": "^3.0.0",
|
|
59
|
-
"vite-plugin-node-polyfills": "^0.
|
|
60
|
+
"vite-plugin-node-polyfills": "^0.28.0",
|
|
60
61
|
"zod": "^4.3.6"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
|
-
"@biomejs/biome": "^2.
|
|
64
|
-
"@types/chrome": "^0.
|
|
65
|
-
"@vitest/coverage-v8": "^4.
|
|
66
|
-
"@vitest/ui": "^4.
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
64
|
+
"@biomejs/biome": "^2.5.4",
|
|
65
|
+
"@types/chrome": "^0.2.2",
|
|
66
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
67
|
+
"@vitest/ui": "^4.1.9",
|
|
68
|
+
"puppeteer": "25.3.0",
|
|
69
|
+
"typescript": "^7.0.2",
|
|
70
|
+
"vite": "^8.1.4",
|
|
71
|
+
"vitest": "^4.1.9"
|
|
70
72
|
}
|
|
71
73
|
}
|
|
@@ -2,7 +2,9 @@
|
|
|
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
|
+
const { parseListenEndpoint } = require("../native/listener.cjs");
|
|
7
|
+
const { getStateDir, loadHostIdentity, loadRegistry } = require("../native/remote-auth.cjs");
|
|
6
8
|
|
|
7
9
|
const HOST_NAME = "surf.browser.host";
|
|
8
10
|
|
|
@@ -12,12 +14,14 @@ const BROWSERS = {
|
|
|
12
14
|
darwin: "Library/Application Support/Google/Chrome/NativeMessagingHosts",
|
|
13
15
|
linux: ".config/google-chrome/NativeMessagingHosts",
|
|
14
16
|
win32: "Google\\Chrome",
|
|
17
|
+
wsl: "Google/Chrome/User Data/NativeMessagingHosts",
|
|
15
18
|
},
|
|
16
19
|
chromium: {
|
|
17
20
|
name: "Chromium",
|
|
18
21
|
darwin: "Library/Application Support/Chromium/NativeMessagingHosts",
|
|
19
22
|
linux: ".config/chromium/NativeMessagingHosts",
|
|
20
23
|
win32: "Chromium",
|
|
24
|
+
wsl: "Chromium/User Data/NativeMessagingHosts",
|
|
21
25
|
},
|
|
22
26
|
brave: {
|
|
23
27
|
name: "Brave",
|
|
@@ -25,12 +29,14 @@ const BROWSERS = {
|
|
|
25
29
|
"Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts",
|
|
26
30
|
linux: ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts",
|
|
27
31
|
win32: "BraveSoftware\\Brave-Browser",
|
|
32
|
+
wsl: "BraveSoftware/Brave-Browser/User Data/NativeMessagingHosts",
|
|
28
33
|
},
|
|
29
34
|
edge: {
|
|
30
35
|
name: "Microsoft Edge",
|
|
31
36
|
darwin: "Library/Application Support/Microsoft Edge/NativeMessagingHosts",
|
|
32
37
|
linux: ".config/microsoft-edge/NativeMessagingHosts",
|
|
33
38
|
win32: "Microsoft\\Edge",
|
|
39
|
+
wsl: "Microsoft/Edge/User Data/NativeMessagingHosts",
|
|
34
40
|
},
|
|
35
41
|
arc: {
|
|
36
42
|
name: "Arc",
|
|
@@ -38,12 +44,14 @@ const BROWSERS = {
|
|
|
38
44
|
"Library/Application Support/Arc/User Data/NativeMessagingHosts",
|
|
39
45
|
linux: null,
|
|
40
46
|
win32: null,
|
|
47
|
+
wsl: null,
|
|
41
48
|
},
|
|
42
49
|
helium: {
|
|
43
50
|
name: "Helium",
|
|
44
51
|
darwin: "Library/Application Support/net.imput.helium/NativeMessagingHosts",
|
|
45
52
|
linux: null,
|
|
46
53
|
win32: null,
|
|
54
|
+
wsl: null,
|
|
47
55
|
},
|
|
48
56
|
};
|
|
49
57
|
|
|
@@ -63,6 +71,16 @@ const NODE_PATHS = {
|
|
|
63
71
|
],
|
|
64
72
|
};
|
|
65
73
|
|
|
74
|
+
function isWsl() {
|
|
75
|
+
if (process.platform !== "linux") return false;
|
|
76
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
|
|
77
|
+
try {
|
|
78
|
+
return /microsoft|wsl/i.test(fs.readFileSync("/proc/version", "utf8"));
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
function findNode() {
|
|
67
85
|
if (process.env.SURF_NODE_PATH && fs.existsSync(process.env.SURF_NODE_PATH)) {
|
|
68
86
|
return process.env.SURF_NODE_PATH;
|
|
@@ -88,10 +106,14 @@ function findNpmGlobalRoot() {
|
|
|
88
106
|
}
|
|
89
107
|
}
|
|
90
108
|
|
|
91
|
-
function getWrapperDir() {
|
|
92
|
-
const platform = process.platform;
|
|
109
|
+
function getWrapperDir(target = process.platform) {
|
|
93
110
|
const home = os.homedir();
|
|
94
|
-
|
|
111
|
+
if (target === "wsl-windows") {
|
|
112
|
+
const localAppData = getWindowsEnv("LOCALAPPDATA");
|
|
113
|
+
if (!localAppData) return null;
|
|
114
|
+
return path.join(windowsPathToWslPath(localAppData), "surf-cli");
|
|
115
|
+
}
|
|
116
|
+
switch (process.platform) {
|
|
95
117
|
case "darwin":
|
|
96
118
|
return path.join(home, "Library/Application Support/surf-cli");
|
|
97
119
|
case "linux":
|
|
@@ -117,72 +139,129 @@ function getHostPath() {
|
|
|
117
139
|
return null;
|
|
118
140
|
}
|
|
119
141
|
|
|
120
|
-
function
|
|
121
|
-
|
|
142
|
+
function getWindowsEnv(name) {
|
|
143
|
+
try {
|
|
144
|
+
return execFileSync("cmd.exe", ["/c", "echo", `%${name}%`], { encoding: "utf8" })
|
|
145
|
+
.trim()
|
|
146
|
+
.replace(/\r/g, "");
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function windowsPathToWslPath(winPath) {
|
|
153
|
+
const normalized = winPath.replace(/\\/g, "/");
|
|
154
|
+
const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
|
155
|
+
if (!match) return normalized;
|
|
156
|
+
return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function wslPathToWindowsPath(wslPath) {
|
|
160
|
+
try {
|
|
161
|
+
return execFileSync("wslpath", ["-w", wslPath], { encoding: "utf8" }).trim().replace(/\r/g, "");
|
|
162
|
+
} catch {
|
|
163
|
+
const match = wslPath.match(/^\/mnt\/([a-zA-Z])\/(.*)$/);
|
|
164
|
+
if (match) return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
165
|
+
return wslPath;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createWrapper(wrapperDir, nodePath, hostPath, target = process.platform, listen) {
|
|
122
170
|
fs.mkdirSync(wrapperDir, { recursive: true });
|
|
123
171
|
|
|
124
|
-
if (
|
|
172
|
+
if (target === "wsl-windows") {
|
|
173
|
+
const cmdPath = path.join(wrapperDir, "host-wrapper-wsl.cmd");
|
|
174
|
+
const distroArg = process.env.WSL_DISTRO_NAME ? ` -d "${process.env.WSL_DISTRO_NAME}"` : "";
|
|
175
|
+
const content = `@echo off\r\nwsl.exe${distroArg} --cd "${path.dirname(hostPath)}" --exec "${nodePath}" "${hostPath}" %*\r\n`;
|
|
176
|
+
fs.writeFileSync(cmdPath, content);
|
|
177
|
+
return wslPathToWindowsPath(cmdPath);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (process.platform === "win32") {
|
|
125
181
|
const batPath = path.join(wrapperDir, "host-wrapper.bat");
|
|
126
|
-
const content = `@echo off\r\n"${nodePath}" "${hostPath}"
|
|
182
|
+
const content = `@echo off\r\n"${nodePath}" "${hostPath}" %*\r\n`;
|
|
127
183
|
fs.writeFileSync(batPath, content);
|
|
128
184
|
return batPath;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const shPath = path.join(wrapperDir, "host-wrapper.sh");
|
|
188
|
+
const hostDir = path.dirname(hostPath);
|
|
189
|
+
const content = `#!/usr/bin/env bash
|
|
133
190
|
cd "${hostDir}"
|
|
134
|
-
exec "${nodePath}" "${hostPath}"
|
|
191
|
+
${listen ? `: "\${SURF_LISTEN:=${listen}}"\nexport SURF_LISTEN\n` : ""}exec "${nodePath}" "${hostPath}" "$@"
|
|
135
192
|
`;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
193
|
+
fs.writeFileSync(shPath, content);
|
|
194
|
+
fs.chmodSync(shPath, "755");
|
|
195
|
+
return shPath;
|
|
140
196
|
}
|
|
141
197
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!browserConfig || !browserConfig[platform]) {
|
|
147
|
-
return null;
|
|
198
|
+
function assertListenTargetSupported(listen, target) {
|
|
199
|
+
if (listen && (target === "win32" || target === "wsl-windows")) {
|
|
200
|
+
throw new Error("--listen is not supported for Windows native-host wrappers");
|
|
148
201
|
}
|
|
202
|
+
}
|
|
149
203
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
204
|
+
function readExistingManifest(manifestPath) {
|
|
205
|
+
if (!fs.existsSync(manifestPath)) return {};
|
|
206
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
207
|
+
}
|
|
153
208
|
|
|
154
|
-
|
|
155
|
-
|
|
209
|
+
function writeManifest(manifestPath, extensionId, wrapperPath) {
|
|
210
|
+
const origin = `chrome-extension://${extensionId}/`;
|
|
211
|
+
const existing = readExistingManifest(manifestPath);
|
|
212
|
+
const allowedOrigins = Array.isArray(existing.allowed_origins) ? existing.allowed_origins : [];
|
|
156
213
|
|
|
157
214
|
const manifest = {
|
|
215
|
+
...existing,
|
|
158
216
|
name: HOST_NAME,
|
|
159
|
-
description: "Surf CLI Native Host",
|
|
217
|
+
description: existing.description || "Surf CLI Native Host",
|
|
160
218
|
path: wrapperPath,
|
|
161
219
|
type: "stdio",
|
|
162
|
-
allowed_origins: [
|
|
220
|
+
allowed_origins: Array.from(new Set([...allowedOrigins, origin])),
|
|
163
221
|
};
|
|
164
222
|
|
|
165
|
-
|
|
223
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
166
224
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
167
225
|
return manifestPath;
|
|
168
226
|
}
|
|
169
227
|
|
|
228
|
+
function getWslWindowsManifestDir(browserConfig) {
|
|
229
|
+
const localAppData = getWindowsEnv("LOCALAPPDATA");
|
|
230
|
+
if (!localAppData || !browserConfig.wsl) return null;
|
|
231
|
+
return path.join(windowsPathToWslPath(localAppData), browserConfig.wsl);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function installManifest(browser, extensionId, wrapperPath, target) {
|
|
235
|
+
const browserConfig = BROWSERS[browser];
|
|
236
|
+
|
|
237
|
+
if (!browserConfig) return null;
|
|
238
|
+
|
|
239
|
+
if (target === "wsl-windows") {
|
|
240
|
+
const manifestDir = getWslWindowsManifestDir(browserConfig);
|
|
241
|
+
if (!manifestDir) return null;
|
|
242
|
+
const manifestPath = path.join(manifestDir, `${HOST_NAME}.json`);
|
|
243
|
+
return writeManifest(manifestPath, extensionId, wrapperPath);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const platform = process.platform;
|
|
247
|
+
if (!browserConfig[platform]) return null;
|
|
248
|
+
|
|
249
|
+
if (platform === "win32") {
|
|
250
|
+
return installWindowsRegistry(browser, extensionId, wrapperPath);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const manifestDir = path.join(os.homedir(), browserConfig[platform]);
|
|
254
|
+
const manifestPath = path.join(manifestDir, `${HOST_NAME}.json`);
|
|
255
|
+
return writeManifest(manifestPath, extensionId, wrapperPath);
|
|
256
|
+
}
|
|
257
|
+
|
|
170
258
|
function installWindowsRegistry(browser, extensionId, wrapperPath) {
|
|
171
259
|
const browserConfig = BROWSERS[browser];
|
|
172
260
|
const regPath = `HKCU\\Software\\${browserConfig.win32}\\NativeMessagingHosts\\${HOST_NAME}`;
|
|
173
261
|
|
|
174
262
|
const manifestDir = getWrapperDir();
|
|
175
263
|
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));
|
|
264
|
+
writeManifest(manifestPath, extensionId, wrapperPath);
|
|
186
265
|
|
|
187
266
|
try {
|
|
188
267
|
execSync(`reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath}" /f`, {
|
|
@@ -197,7 +276,7 @@ function installWindowsRegistry(browser, extensionId, wrapperPath) {
|
|
|
197
276
|
|
|
198
277
|
function parseArgs() {
|
|
199
278
|
const args = process.argv.slice(2);
|
|
200
|
-
const result = { extensionId: null, browsers: ["chrome"] };
|
|
279
|
+
const result = { extensionId: null, browsers: ["chrome"], target: "auto", listen: undefined };
|
|
201
280
|
|
|
202
281
|
for (let i = 0; i < args.length; i++) {
|
|
203
282
|
const arg = args[i];
|
|
@@ -208,6 +287,11 @@ function parseArgs() {
|
|
|
208
287
|
} else {
|
|
209
288
|
result.browsers = browserArg.split(",").map((b) => b.trim().toLowerCase());
|
|
210
289
|
}
|
|
290
|
+
} else if (arg === "--target") {
|
|
291
|
+
result.target = args[++i];
|
|
292
|
+
} else if (arg === "--listen") {
|
|
293
|
+
result.listen = args[++i];
|
|
294
|
+
if (!result.listen || result.listen.startsWith("--")) throw new Error("--listen requires a Tailnet IP and port");
|
|
211
295
|
} else if (arg === "--help" || arg === "-h") {
|
|
212
296
|
printHelp();
|
|
213
297
|
process.exit(0);
|
|
@@ -232,20 +316,30 @@ Options:
|
|
|
232
316
|
-b, --browser Browser(s) to install for (default: chrome)
|
|
233
317
|
Values: chrome, chromium, brave, edge, arc, helium, all
|
|
234
318
|
Multiple: --browser chrome,brave
|
|
319
|
+
--target Install target: auto, linux, windows
|
|
320
|
+
On WSL2, auto installs for Windows Chrome. Use linux for WSLg/Linux browsers.
|
|
321
|
+
--listen <tailscale-ip>:<port>
|
|
322
|
+
Persist an authenticated Tailnet-only listener endpoint.
|
|
323
|
+
Requires at least one surf remote authorize client first.
|
|
324
|
+
Supports Tailscale IPv4 or IPv6 addresses; POSIX wrappers only.
|
|
235
325
|
|
|
236
326
|
Examples:
|
|
237
327
|
node install-native-host.cjs abcdefghijklmnopabcdefghijklmnop
|
|
238
328
|
node install-native-host.cjs abcdefghijklmnop --browser brave
|
|
239
329
|
node install-native-host.cjs abcdefghijklmnop --browser all
|
|
330
|
+
node install-native-host.cjs abcdefghijklmnop --target linux
|
|
331
|
+
node install-native-host.cjs abcdefghijklmnop --listen 100.64.1.2:4321
|
|
240
332
|
`);
|
|
241
333
|
}
|
|
242
334
|
|
|
243
335
|
function main() {
|
|
244
|
-
|
|
336
|
+
let parsed;
|
|
337
|
+
try { parsed = parseArgs(); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); }
|
|
338
|
+
const { extensionId, browsers, target, listen } = parsed;
|
|
245
339
|
|
|
246
340
|
if (!extensionId) {
|
|
247
341
|
console.error("Error: Extension ID required");
|
|
248
|
-
console.error("Usage: install-native-host.cjs <extension-id> [--browser chrome|chromium|brave|edge|arc|helium|all]");
|
|
342
|
+
console.error("Usage: install-native-host.cjs <extension-id> [--browser chrome|chromium|brave|edge|arc|helium|all] [--target auto|linux|windows]");
|
|
249
343
|
console.error("\nFind your extension ID at chrome://extensions (enable Developer Mode)");
|
|
250
344
|
process.exit(1);
|
|
251
345
|
}
|
|
@@ -255,6 +349,36 @@ function main() {
|
|
|
255
349
|
console.error("Expected 32 lowercase letters (a-p)");
|
|
256
350
|
process.exit(1);
|
|
257
351
|
}
|
|
352
|
+
let listener;
|
|
353
|
+
try {
|
|
354
|
+
listener = listen ? parseListenEndpoint(listen).display : undefined;
|
|
355
|
+
if (listener) {
|
|
356
|
+
const stateDir = getStateDir();
|
|
357
|
+
loadHostIdentity(stateDir);
|
|
358
|
+
if (loadRegistry(stateDir).clients.length === 0) {
|
|
359
|
+
throw new Error("--listen requires at least one authorized remote client; run `surf remote authorize <label> --output <path>` first");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} catch (error) { console.error(`Error: ${error.message}`); process.exit(1); }
|
|
363
|
+
|
|
364
|
+
if (!["auto", "linux", "windows"].includes(target)) {
|
|
365
|
+
console.error("Error: Invalid --target value. Expected auto, linux, or windows");
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const runningInWsl = isWsl();
|
|
370
|
+
if (target === "windows" && !runningInWsl && process.platform !== "win32") {
|
|
371
|
+
console.error("Error: --target windows is only supported on Windows or WSL2");
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (target === "linux" && process.platform !== "linux") {
|
|
376
|
+
console.error("Error: --target linux is only supported on Linux or WSL2");
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const effectiveTarget = runningInWsl && target !== "linux" ? "wsl-windows" : process.platform;
|
|
381
|
+
try { assertListenTargetSupported(listen, effectiveTarget); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); }
|
|
258
382
|
|
|
259
383
|
const nodePath = findNode();
|
|
260
384
|
if (!nodePath) {
|
|
@@ -270,19 +394,20 @@ function main() {
|
|
|
270
394
|
process.exit(1);
|
|
271
395
|
}
|
|
272
396
|
|
|
273
|
-
const wrapperDir = getWrapperDir();
|
|
397
|
+
const wrapperDir = getWrapperDir(effectiveTarget);
|
|
274
398
|
if (!wrapperDir) {
|
|
275
|
-
console.error("Error: Unsupported platform");
|
|
399
|
+
console.error("Error: Unsupported platform or Windows interop unavailable");
|
|
276
400
|
process.exit(1);
|
|
277
401
|
}
|
|
278
402
|
|
|
279
|
-
console.log(`Platform: ${process.platform}`);
|
|
403
|
+
console.log(`Platform: ${process.platform}${runningInWsl ? " (WSL2 detected)" : ""}`);
|
|
404
|
+
console.log(`Target: ${effectiveTarget === "wsl-windows" ? "Windows browser from WSL2" : effectiveTarget}`);
|
|
280
405
|
console.log(`Node: ${nodePath}`);
|
|
281
406
|
console.log(`Host: ${hostPath}`);
|
|
282
407
|
console.log(`Wrapper dir: ${wrapperDir}`);
|
|
283
408
|
console.log("");
|
|
284
409
|
|
|
285
|
-
const wrapperPath = createWrapper(wrapperDir, nodePath, hostPath);
|
|
410
|
+
const wrapperPath = createWrapper(wrapperDir, nodePath, hostPath, effectiveTarget, listener);
|
|
286
411
|
console.log(`Created wrapper: ${wrapperPath}`);
|
|
287
412
|
console.log("");
|
|
288
413
|
|
|
@@ -295,7 +420,7 @@ function main() {
|
|
|
295
420
|
continue;
|
|
296
421
|
}
|
|
297
422
|
|
|
298
|
-
const result = installManifest(browser, extensionId, wrapperPath);
|
|
423
|
+
const result = installManifest(browser, extensionId, wrapperPath, effectiveTarget);
|
|
299
424
|
if (result) {
|
|
300
425
|
installed.push({ browser: BROWSERS[browser].name, path: result });
|
|
301
426
|
} else {
|
|
@@ -311,10 +436,18 @@ function main() {
|
|
|
311
436
|
}
|
|
312
437
|
|
|
313
438
|
if (skipped.length > 0) {
|
|
314
|
-
console.log(`\nSkipped (not supported
|
|
439
|
+
console.log(`\nSkipped (not supported for ${effectiveTarget}): ${skipped.join(", ")}`);
|
|
315
440
|
}
|
|
316
441
|
|
|
317
442
|
console.log("\nDone! Restart your browser for changes to take effect.");
|
|
318
443
|
}
|
|
319
444
|
|
|
320
|
-
main
|
|
445
|
+
if (require.main === module) {
|
|
446
|
+
main();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
module.exports = {
|
|
450
|
+
createWrapper,
|
|
451
|
+
writeManifest,
|
|
452
|
+
assertListenTargetSupported,
|
|
453
|
+
};
|