surf-cli 2.18.0 → 2.20.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 +276 -8
- package/dist/content/index.js +4 -4
- package/dist/content/index.js.map +1 -1
- package/dist/options/options.html +1 -18
- package/dist/service-worker/index.js +44 -15
- package/dist/service-worker/index.js.map +1 -1
- package/native/cli.cjs +430 -14
- package/native/do-executor.cjs +35 -8
- package/native/doctor.cjs +211 -42
- package/native/endpoint.cjs +121 -36
- package/native/extract.cjs +362 -0
- package/native/host-helpers.cjs +133 -11
- package/native/host-sessions.cjs +10 -0
- package/native/host.cjs +53 -23
- package/native/mcp-server.cjs +25 -0
- package/native/native-host-launch-probe.cjs +69 -0
- package/native/private-state.cjs +25 -1
- package/native/script-options.cjs +33 -0
- package/native/semantic-cli.cjs +764 -0
- package/native/semantic-core.cjs +369 -0
- package/native/semantic-credentials.cjs +207 -0
- package/native/semantic-provider.cjs +61 -0
- package/native/semantic-workflow-executor.cjs +65 -0
- package/native/semantic-workflow-state.cjs +271 -0
- package/native/semantic-workflow.cjs +398 -0
- package/native/stdin-frames.cjs +33 -0
- package/native/tool-scope.cjs +3 -2
- package/native/workflow-definition.cjs +125 -1
- package/native/workflow-runtime.cjs +71 -5
- package/package.json +11 -7
- package/scripts/install-native-host.cjs +103 -60
- package/scripts/uninstall-native-host.cjs +40 -38
- package/scripts/windows-interop.cjs +89 -0
- package/skills/surf/SKILL.md +96 -1
|
@@ -75,6 +75,58 @@ function assertNotAborted(signal) {
|
|
|
75
75
|
if (message) throw new Error(message);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
const SEMANTIC_SUCCESS_STATUSES = new Set(["completed", "verified", "skipped_already_satisfied"]);
|
|
79
|
+
|
|
80
|
+
function semanticFailure(result) {
|
|
81
|
+
if (!result || typeof result !== "object") return "semantic executor returned an invalid result";
|
|
82
|
+
if (typeof result.error === "string" && result.error) return result.error;
|
|
83
|
+
if (typeof result.reason === "string" && result.reason) return result.reason;
|
|
84
|
+
return `semantic step did not succeed (status: ${String(result.status || "missing")})`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function semanticDetail(result, step) {
|
|
88
|
+
if (result?.semantic) return result.semantic;
|
|
89
|
+
if (!result || typeof result !== "object") return undefined;
|
|
90
|
+
const detail = {
|
|
91
|
+
...(result.runId ? { runId: result.runId } : {}),
|
|
92
|
+
...(step.id ? { stepId: step.id } : {}),
|
|
93
|
+
...(result.reason ? { reason: result.reason } : {}),
|
|
94
|
+
...(result.write ? { write: result.write } : {}),
|
|
95
|
+
...(result.coverage ? { coverage: result.coverage } : {}),
|
|
96
|
+
...(result.checkpoint ? { checkpoint: result.checkpoint } : {}),
|
|
97
|
+
...(result.usage ? { usage: result.usage } : {}),
|
|
98
|
+
...(result.limits ? { limits: result.limits } : {}),
|
|
99
|
+
};
|
|
100
|
+
return Object.keys(detail).length ? detail : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function executeSemanticStep(step, semanticContext, options) {
|
|
104
|
+
const { executeSemanticStep: executor, onEvent = () => {}, signal } = options;
|
|
105
|
+
if (typeof executor !== "function") throw new Error("semantic.step requires an injected semantic executor");
|
|
106
|
+
const startedAt = new Date().toISOString();
|
|
107
|
+
const baseEvent = { command: step.cmd, argsRedacted: redactCommandArgs(step.cmd, step.args || {}, false), startedAt };
|
|
108
|
+
onEvent({ type: "tool.started", ...baseEvent });
|
|
109
|
+
try {
|
|
110
|
+
assertNotAborted(signal);
|
|
111
|
+
const result = await executor(step, semanticContext, { signal });
|
|
112
|
+
assertNotAborted(signal);
|
|
113
|
+
if (!SEMANTIC_SUCCESS_STATUSES.has(result?.status)) {
|
|
114
|
+
const error = semanticFailure(result);
|
|
115
|
+
onEvent({ type: "tool.failed", ...baseEvent, endedAt: new Date().toISOString(), resultSummary: error });
|
|
116
|
+
const semantic = semanticDetail(result, step);
|
|
117
|
+
return { success: false, error, ...(semantic ? { semantic } : {}) };
|
|
118
|
+
}
|
|
119
|
+
const output = result.publicResult;
|
|
120
|
+
const semantic = semanticDetail(result, step);
|
|
121
|
+
onEvent({ type: "tool.completed", ...baseEvent, endedAt: new Date().toISOString(), resultSummary: result.status });
|
|
122
|
+
return { success: true, ...(output !== undefined ? { output } : {}), ...(semantic ? { semantic } : {}) };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const message = error?.message || String(error);
|
|
125
|
+
onEvent({ type: "tool.failed", ...baseEvent, endedAt: new Date().toISOString(), resultSummary: message });
|
|
126
|
+
return { success: false, error: message };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
78
130
|
async function executeSingleStep(step, vars, options) {
|
|
79
131
|
const {
|
|
80
132
|
autoWait = true,
|
|
@@ -126,6 +178,11 @@ async function executeSingleStep(step, vars, options) {
|
|
|
126
178
|
async function executeStep(step, vars, options) {
|
|
127
179
|
const { onError = "stop" } = options;
|
|
128
180
|
assertNotAborted(options.signal);
|
|
181
|
+
if (step.cmd === "semantic.step") {
|
|
182
|
+
const result = await executeSemanticStep(step, options.semanticContext, options);
|
|
183
|
+
if (result.success && step.as && result.output !== undefined) vars[step.as] = result.output;
|
|
184
|
+
return { ...result, stepsExecuted: 1 };
|
|
185
|
+
}
|
|
129
186
|
if (step.repeat !== undefined) {
|
|
130
187
|
let max = resolveVar(step.repeat, vars);
|
|
131
188
|
if (typeof max === "string") max = Number.parseInt(max, 10);
|
|
@@ -177,9 +234,16 @@ function copyCapturedVars(steps, source, target) {
|
|
|
177
234
|
}
|
|
178
235
|
|
|
179
236
|
async function executeWorkflow(steps, options = {}) {
|
|
237
|
+
const semanticEnabled = steps.some((step) => step.cmd === "semantic.step");
|
|
238
|
+
if (semanticEnabled && (options.onError || "stop") !== "stop") {
|
|
239
|
+
return { status: "failed", completedSteps: 0, totalSteps: steps.length, results: [], error: "semantic workflows require onError='stop'", totalMs: 0, vars: { ...(options.vars || {}), ...(options.context?.vars || {}) } };
|
|
240
|
+
}
|
|
180
241
|
const vars = { ...(options.vars || {}), ...(options.context?.vars || {}) };
|
|
242
|
+
const semanticContext = semanticEnabled ? (options.createSemanticContext?.() || Object.create(null)) : undefined;
|
|
243
|
+
const executionOptions = semanticEnabled ? { ...options, semanticContext } : options;
|
|
181
244
|
const results = [];
|
|
182
245
|
let failed = 0;
|
|
246
|
+
let semantic;
|
|
183
247
|
let stepsExecuted = 0;
|
|
184
248
|
const startTotal = Date.now();
|
|
185
249
|
for (let index = 0; index < steps.length; index++) {
|
|
@@ -189,25 +253,26 @@ async function executeWorkflow(steps, options = {}) {
|
|
|
189
253
|
options.onProgress?.({ phase: "start", index, total: steps.length, step, type });
|
|
190
254
|
let result;
|
|
191
255
|
try {
|
|
192
|
-
result = await executeStep(step, vars,
|
|
256
|
+
result = await executeStep(step, vars, executionOptions);
|
|
193
257
|
} catch (error) {
|
|
194
258
|
result = { success: false, error: error?.message || String(error), stepsExecuted: 0 };
|
|
195
259
|
}
|
|
196
260
|
const ms = Date.now() - startTime;
|
|
261
|
+
if (result.semantic) semantic = result.semantic;
|
|
197
262
|
stepsExecuted += type === "loop" ? result.stepsExecuted || 0 : 1;
|
|
198
263
|
if (!result.success) {
|
|
199
264
|
failed++;
|
|
200
|
-
results.push({ step: index + 1, ...(type === "loop" ? { type: "loop" } : { cmd: step.cmd }), status: "error", error: result.error, ms });
|
|
265
|
+
results.push({ step: index + 1, ...(type === "loop" ? { type: "loop" } : { cmd: step.cmd }), status: "error", error: result.error, ...(result.semantic ? { semantic: result.semantic } : {}), ms });
|
|
201
266
|
options.onProgress?.({ phase: "fail", index, total: steps.length, step, type, ms, error: result.error });
|
|
202
267
|
if ((options.onError || "stop") === "stop") {
|
|
203
|
-
return { status: "failed", completedSteps: type === "loop" ? stepsExecuted : stepsExecuted - 1, totalSteps: steps.length, results, error: result.error, totalMs: Date.now() - startTotal, vars };
|
|
268
|
+
return { status: "failed", completedSteps: type === "loop" ? stepsExecuted : stepsExecuted - 1, totalSteps: steps.length, results, error: result.error, ...(semantic ? { semantic } : {}), totalMs: Date.now() - startTotal, vars };
|
|
204
269
|
}
|
|
205
270
|
} else {
|
|
206
|
-
results.push({ step: index + 1, ...(type === "loop" ? { type: "loop", stepsExecuted: result.stepsExecuted } : { cmd: step.cmd }), status: "ok", ms });
|
|
271
|
+
results.push({ step: index + 1, ...(type === "loop" ? { type: "loop", stepsExecuted: result.stepsExecuted } : { cmd: step.cmd }), status: "ok", ...(result.semantic ? { semantic: result.semantic } : {}), ms });
|
|
207
272
|
options.onProgress?.({ phase: "ok", index, total: steps.length, step, type, ms, stepsExecuted: result.stepsExecuted });
|
|
208
273
|
}
|
|
209
274
|
}
|
|
210
|
-
return { status: failed > 0 ? "partial" : "completed", completedSteps: stepsExecuted, totalSteps: steps.length, results, failed, totalMs: Date.now() - startTotal, vars };
|
|
275
|
+
return { status: failed > 0 ? "partial" : "completed", completedSteps: stepsExecuted, totalSteps: steps.length, results, failed, ...(semantic ? { semantic } : {}), totalMs: Date.now() - startTotal, vars };
|
|
211
276
|
}
|
|
212
277
|
|
|
213
278
|
module.exports = {
|
|
@@ -215,6 +280,7 @@ module.exports = {
|
|
|
215
280
|
AUTO_WAIT_MAP,
|
|
216
281
|
MAX_LOOP_ITERATIONS,
|
|
217
282
|
executeSingleStep,
|
|
283
|
+
executeSemanticStep,
|
|
218
284
|
executeStep,
|
|
219
285
|
executeWorkflow,
|
|
220
286
|
extractStepOutput,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "surf-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chrome",
|
|
@@ -41,13 +41,16 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"dev": "vite build --watch --mode development",
|
|
43
43
|
"build": "vite build",
|
|
44
|
+
"prepack": "npm run build",
|
|
44
45
|
"check": "tsc --noEmit && tsc --noEmit -p tsconfig.pi-extension.json",
|
|
45
46
|
"lint": "biome check .",
|
|
46
47
|
"lint:fix": "biome check --write .",
|
|
47
48
|
"lint:test": "biome check test/",
|
|
48
49
|
"format": "biome format --write .",
|
|
49
50
|
"test": "vitest run",
|
|
51
|
+
"test:package": "node test/package-artifact.mjs",
|
|
50
52
|
"test:e2e:chrome": "node test/e2e/real-chrome.mjs",
|
|
53
|
+
"eval:jev": "node test/eval/real-jev.mjs",
|
|
51
54
|
"test:watch": "vitest",
|
|
52
55
|
"test:coverage": "vitest run --coverage",
|
|
53
56
|
"test:ui": "vitest --ui",
|
|
@@ -57,6 +60,7 @@
|
|
|
57
60
|
"dependencies": {
|
|
58
61
|
"@google/generative-ai": "^0.24.1",
|
|
59
62
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
63
|
+
"@typesafe-ai/sdk": "0.6.0",
|
|
60
64
|
"buffer": "^6.0.3",
|
|
61
65
|
"crypto-browserify": "^3.12.1",
|
|
62
66
|
"events": "^3.3.0",
|
|
@@ -66,16 +70,16 @@
|
|
|
66
70
|
},
|
|
67
71
|
"devDependencies": {
|
|
68
72
|
"@biomejs/biome": "^2.5.4",
|
|
69
|
-
"@types/chrome": "^0.
|
|
73
|
+
"@types/chrome": "^0.3.0",
|
|
70
74
|
"@types/node": "^26.1.2",
|
|
71
|
-
"@vitest/coverage-v8": "^
|
|
72
|
-
"@vitest/ui": "^
|
|
73
|
-
"pi-subagents": "^0.
|
|
74
|
-
"puppeteer": "25.
|
|
75
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
76
|
+
"@vitest/ui": "^5.0.0",
|
|
77
|
+
"pi-subagents": "^0.69.0",
|
|
78
|
+
"puppeteer": "25.11.0",
|
|
75
79
|
"typebox": "^1.3.11",
|
|
76
80
|
"typescript": "^7.0.2",
|
|
77
81
|
"vite": "^8.1.4",
|
|
78
|
-
"vitest": "^
|
|
82
|
+
"vitest": "^5.0.0"
|
|
79
83
|
},
|
|
80
84
|
"pi": {
|
|
81
85
|
"extensions": [
|
|
@@ -3,9 +3,20 @@ const fs = require("fs");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const { execFileSync, execSync } = require("child_process");
|
|
6
|
+
const {
|
|
7
|
+
convertWindowsPath,
|
|
8
|
+
convertWslPath,
|
|
9
|
+
getWindowsEnv,
|
|
10
|
+
nativeMessagingRegistryPath,
|
|
11
|
+
runWindowsExecutable,
|
|
12
|
+
} = require("./windows-interop.cjs");
|
|
6
13
|
const { parseListenEndpoint } = require("../native/listener.cjs");
|
|
7
14
|
const { normalizeSocketConfig } = require("../native/socket-permissions.cjs");
|
|
8
15
|
const { getStateDir, loadHostIdentity, loadRegistry } = require("../native/remote-auth.cjs");
|
|
16
|
+
const {
|
|
17
|
+
renderWslWrapper,
|
|
18
|
+
probeWindowsWrapper,
|
|
19
|
+
} = require("../native/native-host-launch-probe.cjs");
|
|
9
20
|
|
|
10
21
|
const HOST_NAME = "surf.browser.host";
|
|
11
22
|
|
|
@@ -111,8 +122,7 @@ function getWrapperDir(target = process.platform) {
|
|
|
111
122
|
const home = os.homedir();
|
|
112
123
|
if (target === "wsl-windows") {
|
|
113
124
|
const localAppData = getWindowsEnv("LOCALAPPDATA");
|
|
114
|
-
|
|
115
|
-
return path.join(windowsPathToWslPath(localAppData), "surf-cli");
|
|
125
|
+
return path.join(convertWindowsPath(localAppData), "surf-cli");
|
|
116
126
|
}
|
|
117
127
|
switch (process.platform) {
|
|
118
128
|
case "darwin":
|
|
@@ -140,44 +150,21 @@ function getHostPath() {
|
|
|
140
150
|
return null;
|
|
141
151
|
}
|
|
142
152
|
|
|
143
|
-
function getWindowsEnv(name) {
|
|
144
|
-
try {
|
|
145
|
-
return execFileSync("cmd.exe", ["/c", "echo", `%${name}%`], { encoding: "utf8" })
|
|
146
|
-
.trim()
|
|
147
|
-
.replace(/\r/g, "");
|
|
148
|
-
} catch {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function windowsPathToWslPath(winPath) {
|
|
154
|
-
const normalized = winPath.replace(/\\/g, "/");
|
|
155
|
-
const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
|
156
|
-
if (!match) return normalized;
|
|
157
|
-
return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
153
|
function wslPathToWindowsPath(wslPath) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
} catch {
|
|
164
|
-
const match = wslPath.match(/^\/mnt\/([a-zA-Z])\/(.*)$/);
|
|
165
|
-
if (match) return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
166
|
-
return wslPath;
|
|
167
|
-
}
|
|
154
|
+
if (!isWsl()) return wslPath;
|
|
155
|
+
return convertWslPath(wslPath);
|
|
168
156
|
}
|
|
169
157
|
|
|
170
|
-
function createWrapper(wrapperDir, nodePath, hostPath, target = process.platform, listen, socketMode, socketGroup) {
|
|
158
|
+
function createWrapper(wrapperDir, nodePath, hostPath, target = process.platform, listen, socketMode, socketGroup, distro = process.env.WSL_DISTRO_NAME, convertPath = wslPathToWindowsPath) {
|
|
171
159
|
const socketConfig = normalizeSocketConfig(socketMode, socketGroup);
|
|
172
160
|
assertSocketAccessTargetSupported(socketConfig.mode, socketConfig.group, target);
|
|
173
161
|
fs.mkdirSync(wrapperDir, { recursive: true });
|
|
174
162
|
|
|
175
163
|
if (target === "wsl-windows") {
|
|
176
164
|
const cmdPath = path.join(wrapperDir, "host-wrapper-wsl.cmd");
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return wslPathToWindowsPath(cmdPath);
|
|
165
|
+
const windowsPath = convertPath(cmdPath);
|
|
166
|
+
fs.writeFileSync(cmdPath, renderWslWrapper(nodePath, hostPath, distro));
|
|
167
|
+
return windowsPath;
|
|
181
168
|
}
|
|
182
169
|
|
|
183
170
|
if (process.platform === "win32") {
|
|
@@ -202,6 +189,32 @@ ${listen ? `: "\${SURF_LISTEN:=${listen}}"\nexport SURF_LISTEN\n` : ""}${socketE
|
|
|
202
189
|
return shPath;
|
|
203
190
|
}
|
|
204
191
|
|
|
192
|
+
function installWithValidatedWrapper(wrapperPath, target, install, deps = {}) {
|
|
193
|
+
if (target === "wsl-windows") {
|
|
194
|
+
if (!deps.distro) throw new Error("WSL_DISTRO_NAME is required for Windows browser installation");
|
|
195
|
+
try {
|
|
196
|
+
probeWindowsWrapper(wrapperPath, deps);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (!error.message.includes("WSL_E_DISTRO_NOT_FOUND")) {
|
|
199
|
+
throw new Error(`WSL wrapper validation failed before registration: ${error.message}`);
|
|
200
|
+
}
|
|
201
|
+
const explicitFailure = error;
|
|
202
|
+
const original = fs.readFileSync(deps.wrapperFsPath, "utf8");
|
|
203
|
+
fs.writeFileSync(deps.wrapperFsPath, renderWslWrapper(deps.nodePath, deps.hostPath, null));
|
|
204
|
+
try {
|
|
205
|
+
const defaultDistro = probeWindowsWrapper(wrapperPath, { ...deps, verifyDistro: true });
|
|
206
|
+
if (defaultDistro !== deps.distro) {
|
|
207
|
+
throw new Error("Windows default WSL distro does not match the installing distro");
|
|
208
|
+
}
|
|
209
|
+
} catch (fallbackError) {
|
|
210
|
+
fs.writeFileSync(deps.wrapperFsPath, original);
|
|
211
|
+
throw new Error(`WSL wrapper validation failed before registration (explicit distro: ${explicitFailure.message}; default distro: ${fallbackError.message})`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return install();
|
|
216
|
+
}
|
|
217
|
+
|
|
205
218
|
function assertListenTargetSupported(listen, target) {
|
|
206
219
|
if (listen && (target === "win32" || target === "wsl-windows")) {
|
|
207
220
|
throw new Error("--listen is not supported for Windows native-host wrappers");
|
|
@@ -238,22 +251,24 @@ function writeManifest(manifestPath, extensionId, wrapperPath) {
|
|
|
238
251
|
return manifestPath;
|
|
239
252
|
}
|
|
240
253
|
|
|
241
|
-
function getWslWindowsManifestDir(browserConfig) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return path.join(
|
|
254
|
+
function getWslWindowsManifestDir(browserConfig, deps = {}) {
|
|
255
|
+
if (!browserConfig.wsl) return null;
|
|
256
|
+
const localAppData = getWindowsEnv("LOCALAPPDATA", deps);
|
|
257
|
+
return path.join(convertWindowsPath(localAppData, deps), browserConfig.wsl);
|
|
245
258
|
}
|
|
246
259
|
|
|
247
|
-
function installManifest(browser, extensionId, wrapperPath, target) {
|
|
260
|
+
function installManifest(browser, extensionId, wrapperPath, target, deps = {}) {
|
|
248
261
|
const browserConfig = BROWSERS[browser];
|
|
249
262
|
|
|
250
263
|
if (!browserConfig) return null;
|
|
251
264
|
|
|
252
265
|
if (target === "wsl-windows") {
|
|
253
|
-
const manifestDir = getWslWindowsManifestDir(browserConfig);
|
|
266
|
+
const manifestDir = getWslWindowsManifestDir(browserConfig, deps);
|
|
254
267
|
if (!manifestDir) return null;
|
|
255
268
|
const manifestPath = path.join(manifestDir, `${HOST_NAME}.json`);
|
|
256
|
-
|
|
269
|
+
writeManifest(manifestPath, extensionId, wrapperPath);
|
|
270
|
+
addWindowsRegistry(browser, convertWslPath(manifestPath, deps), true, deps);
|
|
271
|
+
return manifestPath;
|
|
257
272
|
}
|
|
258
273
|
|
|
259
274
|
const platform = process.platform;
|
|
@@ -268,23 +283,27 @@ function installManifest(browser, extensionId, wrapperPath, target) {
|
|
|
268
283
|
return writeManifest(manifestPath, extensionId, wrapperPath);
|
|
269
284
|
}
|
|
270
285
|
|
|
286
|
+
function addWindowsRegistry(browser, manifestPath, allowWslFallback = false, deps = {}) {
|
|
287
|
+
const browserConfig = BROWSERS[browser];
|
|
288
|
+
const regPath = nativeMessagingRegistryPath(browserConfig.win32, HOST_NAME);
|
|
289
|
+
|
|
290
|
+
runWindowsExecutable("reg.exe", ["add", regPath, "/ve", "/t", "REG_SZ", "/d", manifestPath, "/f"], {
|
|
291
|
+
execFileSync: deps.execFileSync || execFileSync,
|
|
292
|
+
allowWslFallback,
|
|
293
|
+
execOptions: { stdio: "pipe", encoding: "utf8" },
|
|
294
|
+
});
|
|
295
|
+
return regPath;
|
|
296
|
+
}
|
|
297
|
+
|
|
271
298
|
function installWindowsRegistry(browser, extensionId, wrapperPath) {
|
|
272
299
|
const browserConfig = BROWSERS[browser];
|
|
273
|
-
|
|
300
|
+
if (!browserConfig.win32) return null;
|
|
274
301
|
|
|
275
302
|
const manifestDir = getWrapperDir();
|
|
276
303
|
const manifestPath = path.join(manifestDir, `${HOST_NAME}.json`);
|
|
277
304
|
writeManifest(manifestPath, extensionId, wrapperPath);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
execSync(`reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath}" /f`, {
|
|
281
|
-
stdio: "pipe",
|
|
282
|
-
});
|
|
283
|
-
return manifestPath;
|
|
284
|
-
} catch (e) {
|
|
285
|
-
console.error(`Failed to add registry entry: ${e.message}`);
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
305
|
+
addWindowsRegistry(browser, manifestPath);
|
|
306
|
+
return manifestPath;
|
|
288
307
|
}
|
|
289
308
|
|
|
290
309
|
function parseArgs() {
|
|
@@ -445,6 +464,9 @@ function main() {
|
|
|
445
464
|
console.log(`Wrapper dir: ${wrapperDir}`);
|
|
446
465
|
console.log("");
|
|
447
466
|
|
|
467
|
+
const wrapperFsPath = path.join(wrapperDir, "host-wrapper-wsl.cmd");
|
|
468
|
+
const previousWrapper = effectiveTarget === "wsl-windows" && fs.existsSync(wrapperFsPath)
|
|
469
|
+
? fs.readFileSync(wrapperFsPath, "utf8") : null;
|
|
448
470
|
const wrapperPath = createWrapper(
|
|
449
471
|
wrapperDir,
|
|
450
472
|
nodePath,
|
|
@@ -460,18 +482,34 @@ function main() {
|
|
|
460
482
|
const installed = [];
|
|
461
483
|
const skipped = [];
|
|
462
484
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
485
|
+
try {
|
|
486
|
+
installWithValidatedWrapper(wrapperPath, effectiveTarget, () => {
|
|
487
|
+
for (const browser of browsers) {
|
|
488
|
+
if (!BROWSERS[browser]) {
|
|
489
|
+
console.error(`Unknown browser: ${browser}`);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
let result;
|
|
494
|
+
try {
|
|
495
|
+
result = installManifest(browser, extensionId, wrapperPath, effectiveTarget);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
throw new Error(`Failed to install ${BROWSERS[browser].name}: ${error.message}`);
|
|
498
|
+
}
|
|
499
|
+
if (result) {
|
|
500
|
+
installed.push({ browser: BROWSERS[browser].name, path: result });
|
|
501
|
+
} else {
|
|
502
|
+
skipped.push(BROWSERS[browser].name);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}, { wrapperFsPath, nodePath, hostPath, distro: process.env.WSL_DISTRO_NAME });
|
|
506
|
+
} catch (error) {
|
|
507
|
+
if (effectiveTarget === "wsl-windows" && installed.length === 0) {
|
|
508
|
+
if (previousWrapper === null) fs.rmSync(wrapperFsPath, { force: true });
|
|
509
|
+
else fs.writeFileSync(wrapperFsPath, previousWrapper);
|
|
474
510
|
}
|
|
511
|
+
console.error(`Error: ${error.message}`);
|
|
512
|
+
process.exit(1);
|
|
475
513
|
}
|
|
476
514
|
|
|
477
515
|
if (installed.length > 0) {
|
|
@@ -494,7 +532,12 @@ if (require.main === module) {
|
|
|
494
532
|
|
|
495
533
|
module.exports = {
|
|
496
534
|
createWrapper,
|
|
535
|
+
findNode,
|
|
536
|
+
getHostPath,
|
|
537
|
+
probeWindowsWrapper,
|
|
538
|
+
installWithValidatedWrapper,
|
|
497
539
|
writeManifest,
|
|
498
540
|
assertListenTargetSupported,
|
|
499
541
|
assertSocketAccessTargetSupported,
|
|
542
|
+
installManifest,
|
|
500
543
|
};
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const os = require("os");
|
|
5
|
-
const { execFileSync
|
|
5
|
+
const { execFileSync } = require("child_process");
|
|
6
|
+
const {
|
|
7
|
+
convertWindowsPath,
|
|
8
|
+
getWindowsEnv,
|
|
9
|
+
nativeMessagingRegistryPath,
|
|
10
|
+
runWindowsExecutable,
|
|
11
|
+
} = require("./windows-interop.cjs");
|
|
6
12
|
|
|
7
13
|
const HOST_NAME = "surf.browser.host";
|
|
8
14
|
|
|
@@ -61,29 +67,11 @@ function isWsl() {
|
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
function getWindowsEnv(name) {
|
|
65
|
-
try {
|
|
66
|
-
return execFileSync("cmd.exe", ["/c", "echo", `%${name}%`], { encoding: "utf8" })
|
|
67
|
-
.trim()
|
|
68
|
-
.replace(/\r/g, "");
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function windowsPathToWslPath(winPath) {
|
|
75
|
-
const normalized = winPath.replace(/\\/g, "/");
|
|
76
|
-
const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
|
77
|
-
if (!match) return normalized;
|
|
78
|
-
return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
70
|
function getWrapperDir(target = process.platform) {
|
|
82
71
|
const home = os.homedir();
|
|
83
72
|
if (target === "wsl-windows") {
|
|
84
73
|
const localAppData = getWindowsEnv("LOCALAPPDATA");
|
|
85
|
-
|
|
86
|
-
return path.join(windowsPathToWslPath(localAppData), "surf-cli");
|
|
74
|
+
return path.join(convertWindowsPath(localAppData), "surf-cli");
|
|
87
75
|
}
|
|
88
76
|
switch (process.platform) {
|
|
89
77
|
case "darwin":
|
|
@@ -97,25 +85,27 @@ function getWrapperDir(target = process.platform) {
|
|
|
97
85
|
}
|
|
98
86
|
}
|
|
99
87
|
|
|
100
|
-
function getWslWindowsManifestPath(browserConfig) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return path.join(
|
|
88
|
+
function getWslWindowsManifestPath(browserConfig, deps = {}) {
|
|
89
|
+
if (!browserConfig.wsl) return null;
|
|
90
|
+
const localAppData = getWindowsEnv("LOCALAPPDATA", deps);
|
|
91
|
+
return path.join(convertWindowsPath(localAppData, deps), browserConfig.wsl, `${HOST_NAME}.json`);
|
|
104
92
|
}
|
|
105
93
|
|
|
106
|
-
function removeManifest(browser, target) {
|
|
94
|
+
function removeManifest(browser, target, deps = {}) {
|
|
107
95
|
const browserConfig = BROWSERS[browser];
|
|
108
96
|
|
|
109
97
|
if (!browserConfig) return null;
|
|
110
98
|
|
|
111
99
|
if (target === "wsl-windows") {
|
|
112
|
-
const manifestPath = getWslWindowsManifestPath(browserConfig);
|
|
100
|
+
const manifestPath = getWslWindowsManifestPath(browserConfig, deps);
|
|
113
101
|
if (!manifestPath) return null;
|
|
102
|
+
removeWindowsRegistry(browser, true, deps);
|
|
114
103
|
try {
|
|
115
|
-
fs.unlinkSync(manifestPath);
|
|
104
|
+
(deps.fs || fs).unlinkSync(manifestPath);
|
|
116
105
|
return manifestPath;
|
|
117
|
-
} catch {
|
|
118
|
-
return null;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error?.code === "ENOENT") return null;
|
|
108
|
+
throw new Error(`Failed to remove manifest ${manifestPath}: ${error.message || String(error)}`);
|
|
119
109
|
}
|
|
120
110
|
}
|
|
121
111
|
|
|
@@ -140,16 +130,20 @@ function removeManifest(browser, target) {
|
|
|
140
130
|
}
|
|
141
131
|
}
|
|
142
132
|
|
|
143
|
-
function removeWindowsRegistry(browser) {
|
|
133
|
+
function removeWindowsRegistry(browser, allowWslFallback = false, deps = {}) {
|
|
144
134
|
const browserConfig = BROWSERS[browser];
|
|
145
|
-
const regPath =
|
|
146
|
-
|
|
135
|
+
const regPath = nativeMessagingRegistryPath(browserConfig.win32, HOST_NAME);
|
|
147
136
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
137
|
+
runWindowsExecutable("reg.exe", ["delete", regPath, "/f"], {
|
|
138
|
+
execFileSync: deps.execFileSync || execFileSync,
|
|
139
|
+
allowWslFallback,
|
|
140
|
+
execOptions: { stdio: "pipe", encoding: "utf8" },
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (/unable to find the specified registry key or value/i.test(error.message)) return null;
|
|
144
|
+
throw error;
|
|
152
145
|
}
|
|
146
|
+
return regPath;
|
|
153
147
|
}
|
|
154
148
|
|
|
155
149
|
function removeWrapperDir(target) {
|
|
@@ -246,7 +240,13 @@ function main() {
|
|
|
246
240
|
continue;
|
|
247
241
|
}
|
|
248
242
|
|
|
249
|
-
|
|
243
|
+
let result;
|
|
244
|
+
try {
|
|
245
|
+
result = removeManifest(browser, effectiveTarget);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error(`Error: Failed to uninstall ${BROWSERS[browser].name}: ${error.message}`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
250
|
if (result) {
|
|
251
251
|
removed.push({ browser: BROWSERS[browser].name, path: result });
|
|
252
252
|
} else {
|
|
@@ -275,4 +275,6 @@ function main() {
|
|
|
275
275
|
console.log("\nDone!");
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
main();
|
|
278
|
+
if (require.main === module) main();
|
|
279
|
+
|
|
280
|
+
module.exports = { removeManifest };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const { execFileSync } = require("child_process");
|
|
2
|
+
|
|
3
|
+
function errorDetail(error) {
|
|
4
|
+
const stderr = typeof error.stderr === "string" ? error.stderr.trim() : "";
|
|
5
|
+
return stderr || error.message || String(error);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function runWindowsExecutable(executable, args, options = {}) {
|
|
9
|
+
const execFile = options.execFileSync || execFileSync;
|
|
10
|
+
const execOptions = options.execOptions || { encoding: "utf8" };
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
return execFile(executable, args, execOptions);
|
|
14
|
+
} catch (directError) {
|
|
15
|
+
if (!options.allowWslFallback || directError?.code !== "ENOENT") {
|
|
16
|
+
throw new Error(`Failed to run ${executable}: ${errorDetail(directError)}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const windowsPath = `C:\\Windows\\System32\\${executable}`;
|
|
20
|
+
let resolvedPath;
|
|
21
|
+
try {
|
|
22
|
+
resolvedPath = execFile("wslpath", ["-u", windowsPath], { encoding: "utf8" }).trim();
|
|
23
|
+
} catch (resolveError) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Could not find ${executable} (bare lookup: ${errorDetail(directError)}; ` +
|
|
26
|
+
`wslpath ${windowsPath}: ${errorDetail(resolveError)})`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (!resolvedPath) {
|
|
30
|
+
throw new Error(`Could not find ${executable}: wslpath returned an empty path for ${windowsPath}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return execFile(resolvedPath, args, execOptions);
|
|
35
|
+
} catch (resolvedError) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Failed to run ${executable} at ${resolvedPath}: ${errorDetail(resolvedError)}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getWindowsEnv(name, options = {}) {
|
|
44
|
+
const value = runWindowsExecutable("cmd.exe", ["/c", "echo", `%${name}%`], {
|
|
45
|
+
execFileSync: options.execFileSync || execFileSync,
|
|
46
|
+
allowWslFallback: true,
|
|
47
|
+
execOptions: { encoding: "utf8" },
|
|
48
|
+
}).trim();
|
|
49
|
+
if (!value || value === `%${name}%`) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Windows environment variable ${name} is unavailable (cmd.exe returned ${JSON.stringify(value)})`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function nativeMessagingRegistryPath(browserRegistryRoot, hostName) {
|
|
58
|
+
return `HKCU\\Software\\${browserRegistryRoot}\\NativeMessagingHosts\\${hostName}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function convertWindowsPath(windowsPath, options = {}) {
|
|
62
|
+
const execFile = options.execFileSync || execFileSync;
|
|
63
|
+
try {
|
|
64
|
+
const converted = execFile("wslpath", ["-u", windowsPath], { encoding: "utf8" }).trim();
|
|
65
|
+
if (!converted) throw new Error("wslpath returned an empty path");
|
|
66
|
+
return converted;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new Error(`Could not convert Windows path ${windowsPath}: ${errorDetail(error)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function convertWslPath(wslPath, options = {}) {
|
|
73
|
+
const execFile = options.execFileSync || execFileSync;
|
|
74
|
+
try {
|
|
75
|
+
const converted = execFile("wslpath", ["-w", wslPath], { encoding: "utf8" }).trim();
|
|
76
|
+
if (!converted) throw new Error("wslpath returned an empty path");
|
|
77
|
+
return converted;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error(`Could not convert WSL path ${wslPath}: ${errorDetail(error)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
convertWindowsPath,
|
|
85
|
+
convertWslPath,
|
|
86
|
+
getWindowsEnv,
|
|
87
|
+
nativeMessagingRegistryPath,
|
|
88
|
+
runWindowsExecutable,
|
|
89
|
+
};
|