surf-cli 2.19.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 +169 -3
- 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 +109 -6
- package/native/do-executor.cjs +35 -8
- package/native/doctor.cjs +200 -40
- package/native/host-helpers.cjs +95 -10
- package/native/host.cjs +24 -2
- package/native/native-host-launch-probe.cjs +69 -0
- package/native/private-state.cjs +25 -1
- 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/tool-scope.cjs +1 -0
- package/native/workflow-definition.cjs +125 -1
- package/native/workflow-runtime.cjs +71 -5
- package/package.json +8 -4
- 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 +78 -1
package/native/cli.cjs
CHANGED
|
@@ -16,7 +16,7 @@ const {
|
|
|
16
16
|
validateWorkflowArgs,
|
|
17
17
|
validateWorkflowFile,
|
|
18
18
|
} = require("./workflow-definition.cjs");
|
|
19
|
-
const { executeDoSteps, sendDoRequest } = require("./do-executor.cjs");
|
|
19
|
+
const { executeDoSteps, semanticRequestContext, sendDoRequest } = require("./do-executor.cjs");
|
|
20
20
|
const { runExtraction, renderExtractionMarkdown } = require("./extract.cjs");
|
|
21
21
|
const { applyOptionsPrelude, parseScriptOptions } = require("./script-options.cjs");
|
|
22
22
|
const { openClientTransport } = require("./client-transport.cjs");
|
|
@@ -202,6 +202,22 @@ try {
|
|
|
202
202
|
process.exit(1);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
if (args[0] === "semantic" || args[0]?.startsWith("semantic.")) {
|
|
206
|
+
const { formatSemanticOutput, handleSemanticCli } = require("./semantic-cli.cjs");
|
|
207
|
+
handleSemanticCli(args, { endpoint })
|
|
208
|
+
.then((result) => {
|
|
209
|
+
if (!result.handled) throw new Error("Semantic command was not handled");
|
|
210
|
+
if (result.value !== undefined) console.log(formatSemanticOutput(result));
|
|
211
|
+
process.exit(0);
|
|
212
|
+
})
|
|
213
|
+
.catch((error) => {
|
|
214
|
+
const code = error?.code ? ` [${error.code}]` : "";
|
|
215
|
+
console.error(`Error: ${error?.message || String(error)}${code}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
205
221
|
if (args[0] === "oracle") {
|
|
206
222
|
if (args[1] === "ask" || args[1] === "follow") {
|
|
207
223
|
console.error("[surf] Oracle requires exclusive browser access while dispatching; other sessions will queue.");
|
|
@@ -1200,6 +1216,15 @@ const TOOLS = {
|
|
|
1200
1216
|
args: [],
|
|
1201
1217
|
examples: [{ cmd: "frame.list", desc: "Show frame tree" }]
|
|
1202
1218
|
},
|
|
1219
|
+
"frame.diagnose": {
|
|
1220
|
+
desc: "Compare DOM iframes, extension reachability, and the Chrome DevTools frame tree",
|
|
1221
|
+
args: [],
|
|
1222
|
+
opts: {
|
|
1223
|
+
"tab-id": "Target tab ID",
|
|
1224
|
+
json: "Print full frame inventories as JSON"
|
|
1225
|
+
},
|
|
1226
|
+
examples: [{ cmd: "frame.diagnose", desc: "Explain iframe access and count mismatches" }]
|
|
1227
|
+
},
|
|
1203
1228
|
"frame.switch": {
|
|
1204
1229
|
desc: "Switch to iframe context",
|
|
1205
1230
|
args: [],
|
|
@@ -1308,7 +1333,10 @@ const TOOLS = {
|
|
|
1308
1333
|
"on-error": "stop (default) | continue",
|
|
1309
1334
|
"no-auto-wait": "Disable automatic waits between steps",
|
|
1310
1335
|
"step-delay": "Delay between steps in ms (default: 100)",
|
|
1311
|
-
"dry-run": "Parse and validate without executing"
|
|
1336
|
+
"dry-run": "Parse and validate without executing",
|
|
1337
|
+
"allow-semantic": "Allow bounded TypeSafe semantic decisions",
|
|
1338
|
+
"allow-write": "Allow declared semantic fill/check/click steps",
|
|
1339
|
+
"inputs-stdin": "Read bounded private input slots as one JSON object from stdin",
|
|
1312
1340
|
},
|
|
1313
1341
|
examples: [
|
|
1314
1342
|
{ cmd: 'do \'go "https://example.com" | click e5 | screenshot\'', desc: "Inline workflow" },
|
|
@@ -1584,8 +1612,19 @@ Tips:
|
|
|
1584
1612
|
- Use window.new --incognito for isolated cookies`
|
|
1585
1613
|
},
|
|
1586
1614
|
semantic: {
|
|
1587
|
-
title: "Semantic
|
|
1588
|
-
content: `
|
|
1615
|
+
title: "Semantic browser decisions and locators",
|
|
1616
|
+
content: `Optional Jev commands (page-derived text is sent to TypeSafe only for these commands):
|
|
1617
|
+
semantic.find "the notification control"
|
|
1618
|
+
semantic.verify "Notification preferences were saved"
|
|
1619
|
+
semantic.filter "notification preferences"
|
|
1620
|
+
semantic.act "Open notification settings" --max-steps 5
|
|
1621
|
+
semantic.act "Fill email" --input email="$EMAIL" --allow-write
|
|
1622
|
+
semantic auth set|status|clear
|
|
1623
|
+
|
|
1624
|
+
Every click/fill requires --allow-write. This broadly authorizes even high-impact controls;
|
|
1625
|
+
repeat --allow-ref <ref> to narrow authorization to exact observed refs.
|
|
1626
|
+
|
|
1627
|
+
Local semantic locators find elements by role, text, or label instead of refs or selectors.
|
|
1589
1628
|
|
|
1590
1629
|
By ARIA role:
|
|
1591
1630
|
locate.role button --name "Submit" --action click
|
|
@@ -1691,7 +1730,7 @@ const ALL_SOCKET_TOOLS = [
|
|
|
1691
1730
|
"form.fill",
|
|
1692
1731
|
"perf.start", "perf.stop", "perf.metrics",
|
|
1693
1732
|
"upload",
|
|
1694
|
-
"frame.list", "frame.switch", "frame.main", "frame.js",
|
|
1733
|
+
"frame.list", "frame.diagnose", "frame.switch", "frame.main", "frame.js",
|
|
1695
1734
|
"cookie.list", "cookie.get", "cookie.set", "cookie.clear",
|
|
1696
1735
|
"search", "batch",
|
|
1697
1736
|
"zoom", "resize",
|
|
@@ -1714,6 +1753,7 @@ const SEE_ALSO = {
|
|
|
1714
1753
|
"window.new": ["window.list"],
|
|
1715
1754
|
"window.list": ["tab.list"],
|
|
1716
1755
|
"frame.list": ["frame.switch", "frame.main"],
|
|
1756
|
+
"frame.diagnose": ["frame.list", "frame.switch", "frame.js"],
|
|
1717
1757
|
"frame.switch": ["frame.list", "frame.main", "frame.js"],
|
|
1718
1758
|
"frame.main": ["frame.list", "frame.switch"],
|
|
1719
1759
|
"frame.js": ["frame.switch", "js"],
|
|
@@ -1767,6 +1807,8 @@ Common Commands:
|
|
|
1767
1807
|
animate-audit JSON timeline of element animation/style samples
|
|
1768
1808
|
perf-audit PerformanceObserver snapshot for motion/jank debugging
|
|
1769
1809
|
page.read Get page accessibility tree (alias: read)
|
|
1810
|
+
semantic.find Optional Jev-powered candidate selection
|
|
1811
|
+
semantic.act Bounded semantic browser action controller
|
|
1770
1812
|
locate.role <role> Find element by ARIA role
|
|
1771
1813
|
search <term> Search for text in page (alias: find)
|
|
1772
1814
|
window.new <url> Create isolated browser window
|
|
@@ -1844,6 +1886,11 @@ const showFullHelp = () => {
|
|
|
1844
1886
|
|
|
1845
1887
|
Usage: surf <command> [args] [options]
|
|
1846
1888
|
|
|
1889
|
+
Semantic (optional TypeSafe/Jev):
|
|
1890
|
+
surf semantic.find|verify|filter <goal> [--json]
|
|
1891
|
+
surf semantic.act <goal> [--allow-write] [--allow-ref <ref>] [--input <name=value>]
|
|
1892
|
+
surf semantic auth <set|status|clear>
|
|
1893
|
+
|
|
1847
1894
|
Oracle:
|
|
1848
1895
|
surf oracle <ask|status|result|follow|list>
|
|
1849
1896
|
|
|
@@ -2407,9 +2454,12 @@ if (args[0] === "do") {
|
|
|
2407
2454
|
let windowId = undefined;
|
|
2408
2455
|
let explicitSession = undefined;
|
|
2409
2456
|
let noWait = false;
|
|
2457
|
+
let allowSemantic = false;
|
|
2458
|
+
let allowWrite = false;
|
|
2459
|
+
let inputsStdin = false;
|
|
2410
2460
|
|
|
2411
2461
|
// Reserved flags that aren't workflow args
|
|
2412
|
-
const reservedFlags = ['file', 'f', 'dry-run', 'on-error', 'no-auto-wait', 'step-delay', 'json', 'tab-id', 'window-id', 'session', 'no-lock', 'no-wait'];
|
|
2462
|
+
const reservedFlags = ['file', 'f', 'dry-run', 'on-error', 'no-auto-wait', 'step-delay', 'json', 'tab-id', 'window-id', 'session', 'no-lock', 'no-wait', 'allow-semantic', 'allow-write', 'inputs-stdin'];
|
|
2413
2463
|
|
|
2414
2464
|
// Workflow-specific args (collected for variable substitution)
|
|
2415
2465
|
const workflowArgs = {};
|
|
@@ -2449,6 +2499,12 @@ if (args[0] === "do") {
|
|
|
2449
2499
|
i++;
|
|
2450
2500
|
} else if (arg === "--no-wait") {
|
|
2451
2501
|
noWait = true;
|
|
2502
|
+
} else if (arg === "--allow-semantic") {
|
|
2503
|
+
allowSemantic = true;
|
|
2504
|
+
} else if (arg === "--allow-write") {
|
|
2505
|
+
allowWrite = true;
|
|
2506
|
+
} else if (arg === "--inputs-stdin") {
|
|
2507
|
+
inputsStdin = true;
|
|
2452
2508
|
} else if (arg.startsWith("--")) {
|
|
2453
2509
|
// Workflow-specific arg (e.g., --email, --password)
|
|
2454
2510
|
const key = arg.slice(2);
|
|
@@ -2573,6 +2629,27 @@ if (args[0] === "do") {
|
|
|
2573
2629
|
|
|
2574
2630
|
// Apply arg defaults
|
|
2575
2631
|
const vars = workflow ? applyArgDefaults(workflow, workflowArgs) : workflowArgs;
|
|
2632
|
+
let privateInputs = {};
|
|
2633
|
+
if (inputsStdin) {
|
|
2634
|
+
try {
|
|
2635
|
+
const { SEMANTIC_POLICY } = require("./semantic-core.cjs");
|
|
2636
|
+
const input = fs.readFileSync(0, "utf8");
|
|
2637
|
+
if (Buffer.byteLength(input, "utf8") > 262144) throw new Error("input JSON exceeds 256 KiB");
|
|
2638
|
+
privateInputs = JSON.parse(input);
|
|
2639
|
+
if (!privateInputs || typeof privateInputs !== "object" || Array.isArray(privateInputs)) throw new Error("input JSON must be an object");
|
|
2640
|
+
const entries = Object.entries(privateInputs);
|
|
2641
|
+
if (entries.length > SEMANTIC_POLICY.limits.inputSlots) throw new Error(`input JSON supports at most ${SEMANTIC_POLICY.limits.inputSlots} slots`);
|
|
2642
|
+
for (const [name, value] of entries) {
|
|
2643
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(name)) throw new Error("input JSON contains an invalid slot name");
|
|
2644
|
+
if (!["string", "number", "boolean"].includes(typeof value)) throw new Error(`input slot '${name}' must be a string, number, or boolean`);
|
|
2645
|
+
if (Buffer.byteLength(String(value), "utf8") > SEMANTIC_POLICY.limits.inputValueBytes) throw new Error(`input slot '${name}' exceeds ${SEMANTIC_POLICY.limits.inputValueBytes / 1024} KiB`);
|
|
2646
|
+
if (Object.hasOwn(vars, name)) throw new Error(`input slot '${name}' was supplied more than once`);
|
|
2647
|
+
}
|
|
2648
|
+
} catch (error) {
|
|
2649
|
+
console.error(`Error: Invalid --inputs-stdin JSON: ${error.message}`);
|
|
2650
|
+
process.exit(1);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2576
2653
|
|
|
2577
2654
|
// Validate with --dry-run
|
|
2578
2655
|
if (dryRun) {
|
|
@@ -2593,6 +2670,21 @@ if (args[0] === "do") {
|
|
|
2593
2670
|
process.exit(0);
|
|
2594
2671
|
}
|
|
2595
2672
|
|
|
2673
|
+
const semanticEnabled = Boolean(workflow?.semantic);
|
|
2674
|
+
if (semanticEnabled && !allowSemantic) {
|
|
2675
|
+
console.error("Error: semantic workflows require --allow-semantic");
|
|
2676
|
+
process.exit(1);
|
|
2677
|
+
}
|
|
2678
|
+
if (semanticEnabled && onError === "continue") {
|
|
2679
|
+
console.error("Error: semantic workflows do not support --on-error continue");
|
|
2680
|
+
process.exit(1);
|
|
2681
|
+
}
|
|
2682
|
+
const writeOps = new Set(["ensureChecked", "fill", "click"]);
|
|
2683
|
+
if (semanticEnabled && steps.some((step) => writeOps.has(step.args?.op)) && !allowWrite) {
|
|
2684
|
+
console.error("Error: mutation-capable semantic steps require --allow-write");
|
|
2685
|
+
process.exit(1);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2596
2688
|
if (!wantJson) {
|
|
2597
2689
|
if (workflowName) {
|
|
2598
2690
|
console.log(`Running workflow: ${workflowName} (${steps.length} steps)...\n`);
|
|
@@ -2620,6 +2712,17 @@ if (args[0] === "do") {
|
|
|
2620
2712
|
endpoint,
|
|
2621
2713
|
transport,
|
|
2622
2714
|
},
|
|
2715
|
+
...(semanticEnabled ? {
|
|
2716
|
+
createSemanticExecutor: async ({ context }) => {
|
|
2717
|
+
const { createConcreteSemanticExecutor } = require("./semantic-workflow-executor.cjs");
|
|
2718
|
+
return createConcreteSemanticExecutor({
|
|
2719
|
+
workflow,
|
|
2720
|
+
inputs: { ...vars, ...privateInputs },
|
|
2721
|
+
request: (tool, toolArgs, timeoutMs, identity) =>
|
|
2722
|
+
sendDoRequest(tool, toolArgs, semanticRequestContext(context, timeoutMs, identity)),
|
|
2723
|
+
});
|
|
2724
|
+
},
|
|
2725
|
+
} : {}),
|
|
2623
2726
|
});
|
|
2624
2727
|
|
|
2625
2728
|
// Print summary
|
package/native/do-executor.cjs
CHANGED
|
@@ -35,6 +35,14 @@ function sendDoRequest(toolName, toolArgs, context = {}) {
|
|
|
35
35
|
})();
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function semanticRequestContext(context, timeoutMs, identity) {
|
|
39
|
+
return {
|
|
40
|
+
...context,
|
|
41
|
+
timeoutMs,
|
|
42
|
+
...(identity ? { tabId: identity.tabId, windowId: undefined, session: undefined } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
function summarizeArgs(step) {
|
|
39
47
|
return Object.entries(step.args || {})
|
|
40
48
|
.map(([key, value]) => typeof value === "string" && value.length > 40 ? `${key}="${value.slice(0, 37)}..."` : `${key}=${JSON.stringify(value)}`)
|
|
@@ -63,14 +71,32 @@ function printProgress(event) {
|
|
|
63
71
|
|
|
64
72
|
async function executeDoSteps(steps, options = {}) {
|
|
65
73
|
const context = options.context || {};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
let semanticExecutor;
|
|
75
|
+
const executeSemanticStep = steps.some((step) => step.cmd === "semantic.step")
|
|
76
|
+
? async (step, semanticContext, executionOptions) => {
|
|
77
|
+
if (!semanticExecutor) {
|
|
78
|
+
semanticExecutor = options.executeSemanticStep;
|
|
79
|
+
if (!semanticExecutor && typeof options.createSemanticExecutor === "function") {
|
|
80
|
+
semanticExecutor = await options.createSemanticExecutor({ context });
|
|
81
|
+
}
|
|
82
|
+
if (typeof semanticExecutor !== "function") throw new Error("semantic.step requires an injected semantic executor");
|
|
83
|
+
}
|
|
84
|
+
return semanticExecutor(step, semanticContext, executionOptions);
|
|
85
|
+
}
|
|
86
|
+
: undefined;
|
|
87
|
+
try {
|
|
88
|
+
return await runtime.executeWorkflow(steps, {
|
|
89
|
+
...options,
|
|
90
|
+
executeTool: options.executeTool || ((tool, args) => sendDoRequest(tool, args, context)),
|
|
91
|
+
...(executeSemanticStep ? { executeSemanticStep } : {}),
|
|
92
|
+
onProgress: options.quiet ? options.onProgress : (event) => {
|
|
93
|
+
printProgress(event);
|
|
94
|
+
options.onProgress?.(event);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
} finally {
|
|
98
|
+
await semanticExecutor?.close?.();
|
|
99
|
+
}
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
module.exports = {
|
|
@@ -83,6 +109,7 @@ module.exports = {
|
|
|
83
109
|
extractStepOutput: runtime.extractStepOutput,
|
|
84
110
|
getAutoWaitCommand: runtime.getAutoWaitCommand,
|
|
85
111
|
resolveVar: runtime.resolveVar,
|
|
112
|
+
semanticRequestContext,
|
|
86
113
|
sendDoRequest,
|
|
87
114
|
shouldAutoWait: runtime.shouldAutoWait,
|
|
88
115
|
substituteVars: runtime.substituteVars,
|
package/native/doctor.cjs
CHANGED
|
@@ -4,6 +4,17 @@ const os = require("os");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { execFileSync } = require("child_process");
|
|
6
6
|
const { connectEndpoint, selectEndpoint } = require("./endpoint.cjs");
|
|
7
|
+
const {
|
|
8
|
+
convertWindowsPath,
|
|
9
|
+
getWindowsEnv,
|
|
10
|
+
nativeMessagingRegistryPath,
|
|
11
|
+
runWindowsExecutable,
|
|
12
|
+
} = require("../scripts/windows-interop.cjs");
|
|
13
|
+
const {
|
|
14
|
+
renderWslWrapper,
|
|
15
|
+
probeWindowsWrapper,
|
|
16
|
+
} = require("./native-host-launch-probe.cjs");
|
|
17
|
+
const { findNode, getHostPath } = require("../scripts/install-native-host.cjs");
|
|
7
18
|
|
|
8
19
|
const HOST_NAME = "surf.browser.host";
|
|
9
20
|
|
|
@@ -115,32 +126,12 @@ function resolveBrowsers(browserArg) {
|
|
|
115
126
|
return browsers;
|
|
116
127
|
}
|
|
117
128
|
|
|
118
|
-
function getWindowsEnv(name, { env = process.env, execFileSync: execFile = execFileSync } = {}) {
|
|
119
|
-
if (env[name]) return env[name];
|
|
120
|
-
try {
|
|
121
|
-
return execFile("cmd.exe", ["/c", "echo", `%${name}%`], { encoding: "utf8" })
|
|
122
|
-
.trim()
|
|
123
|
-
.replace(/\r/g, "");
|
|
124
|
-
} catch {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function windowsPathToWslPath(winPath) {
|
|
130
|
-
const normalized = winPath.replace(/\\/g, "/");
|
|
131
|
-
const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
|
132
|
-
if (!match) return normalized;
|
|
133
|
-
return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
129
|
function manifestPathForBrowser(browserKey, context) {
|
|
137
130
|
const browser = BROWSERS[browserKey];
|
|
138
131
|
if (!browser) return null;
|
|
139
132
|
|
|
140
133
|
if (context.effectiveTarget === "wsl-windows") {
|
|
141
|
-
|
|
142
|
-
if (!localAppData || !browser.wsl) return null;
|
|
143
|
-
return path.join(windowsPathToWslPath(localAppData), browser.wsl, `${HOST_NAME}.json`);
|
|
134
|
+
return null;
|
|
144
135
|
}
|
|
145
136
|
|
|
146
137
|
if (context.platform === "win32") {
|
|
@@ -156,7 +147,7 @@ function manifestPathForBrowser(browserKey, context) {
|
|
|
156
147
|
|
|
157
148
|
function fsPathFromManifestPath(manifestPath, context) {
|
|
158
149
|
if (context.platform === "linux" && /^[A-Za-z]:[\\/]/.test(manifestPath)) {
|
|
159
|
-
return
|
|
150
|
+
return convertWindowsPath(manifestPath, context);
|
|
160
151
|
}
|
|
161
152
|
return manifestPath;
|
|
162
153
|
}
|
|
@@ -164,17 +155,21 @@ function fsPathFromManifestPath(manifestPath, context) {
|
|
|
164
155
|
function windowsRegistryPathForBrowser(browserKey) {
|
|
165
156
|
const browser = BROWSERS[browserKey];
|
|
166
157
|
if (!browser?.win32) return null;
|
|
167
|
-
return
|
|
158
|
+
return nativeMessagingRegistryPath(browser.win32, HOST_NAME);
|
|
168
159
|
}
|
|
169
160
|
|
|
170
161
|
function readWindowsRegistryManifestPath(registryPath, context) {
|
|
171
162
|
try {
|
|
172
|
-
const output =
|
|
163
|
+
const output = runWindowsExecutable("reg.exe", ["query", registryPath, "/ve"], {
|
|
164
|
+
execFileSync: context.execFileSync,
|
|
165
|
+
allowWslFallback: context.effectiveTarget === "wsl-windows",
|
|
166
|
+
execOptions: { encoding: "utf8" },
|
|
167
|
+
});
|
|
173
168
|
const line = output.split(/\r?\n/).find((item) => item.includes("REG_SZ"));
|
|
174
|
-
if (!line) return null;
|
|
175
|
-
return line.replace(/^.*REG_SZ\s+/, "").trim() || null;
|
|
176
|
-
} catch {
|
|
177
|
-
return null;
|
|
169
|
+
if (!line) return { manifestPath: null, error: "registry output contained no REG_SZ default value" };
|
|
170
|
+
return { manifestPath: line.replace(/^.*REG_SZ\s+/, "").trim() || null, error: null };
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return { manifestPath: null, error: error.message };
|
|
178
173
|
}
|
|
179
174
|
}
|
|
180
175
|
|
|
@@ -192,15 +187,16 @@ function checkWindowsRegistry(browserKey, context) {
|
|
|
192
187
|
};
|
|
193
188
|
}
|
|
194
189
|
|
|
195
|
-
const
|
|
190
|
+
const registry = readWindowsRegistryManifestPath(registryPath, context);
|
|
191
|
+
const manifestPath = registry.manifestPath;
|
|
196
192
|
return {
|
|
197
193
|
check: {
|
|
198
194
|
id: "windows-registry",
|
|
199
195
|
status: manifestPath ? "pass" : "fail",
|
|
200
196
|
browser: browserKey,
|
|
201
|
-
message: manifestPath
|
|
202
|
-
? `Windows native messaging registry
|
|
203
|
-
: `Windows native messaging registry
|
|
197
|
+
message: !manifestPath
|
|
198
|
+
? `Windows native messaging registry entry not found: ${registryPath}${registry.error ? ` (${registry.error})` : ""}`
|
|
199
|
+
: `Windows native messaging registry points to ${manifestPath}`,
|
|
204
200
|
registryPath,
|
|
205
201
|
path: manifestPath,
|
|
206
202
|
},
|
|
@@ -210,19 +206,32 @@ function checkWindowsRegistry(browserKey, context) {
|
|
|
210
206
|
|
|
211
207
|
function checkManifest(manifestPath, context) {
|
|
212
208
|
const checks = [];
|
|
213
|
-
|
|
209
|
+
let manifestFsPath = manifestPath;
|
|
210
|
+
try {
|
|
211
|
+
manifestFsPath = manifestPath ? fsPathFromManifestPath(manifestPath, context) : manifestPath;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
checks.push({
|
|
214
|
+
id: "manifest-file",
|
|
215
|
+
status: "fail",
|
|
216
|
+
message: `Could not resolve manifest path ${manifestPath}: ${error.message}`,
|
|
217
|
+
path: manifestPath,
|
|
218
|
+
});
|
|
219
|
+
return { checks, manifest: null };
|
|
220
|
+
}
|
|
221
|
+
const exists = manifestFsPath ? context.fs.existsSync(manifestFsPath) : false;
|
|
214
222
|
checks.push({
|
|
215
223
|
id: "manifest-file",
|
|
216
224
|
status: exists ? "pass" : "fail",
|
|
217
|
-
message: exists ? `Manifest found: ${manifestPath}` :
|
|
225
|
+
message: exists ? `Manifest found: ${manifestPath}` : `Native messaging manifest not found: ${manifestPath}`,
|
|
218
226
|
path: manifestPath,
|
|
227
|
+
fsPath: manifestFsPath,
|
|
219
228
|
});
|
|
220
229
|
|
|
221
230
|
if (!exists) return { checks, manifest: null };
|
|
222
231
|
|
|
223
232
|
let manifest;
|
|
224
233
|
try {
|
|
225
|
-
manifest = JSON.parse(context.fs.readFileSync(
|
|
234
|
+
manifest = JSON.parse(context.fs.readFileSync(manifestFsPath, "utf8"));
|
|
226
235
|
checks.push({ id: "manifest-json", status: "pass", message: "Manifest JSON is valid" });
|
|
227
236
|
} catch (error) {
|
|
228
237
|
checks.push({ id: "manifest-json", status: "fail", message: `Manifest JSON is invalid: ${error.message}` });
|
|
@@ -288,6 +297,126 @@ function checkManifest(manifestPath, context) {
|
|
|
288
297
|
return { checks, manifest };
|
|
289
298
|
}
|
|
290
299
|
|
|
300
|
+
function normalizeWindowsPath(filePath) {
|
|
301
|
+
return path.win32.normalize(filePath).replace(/[\\/]+$/, "").toLowerCase();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function checkWslWrapperLaunch(manifest, context) {
|
|
305
|
+
if (!manifest || typeof manifest.path !== "string" || manifest.path.length === 0) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let canonicalWindowsPath;
|
|
310
|
+
try {
|
|
311
|
+
const localAppData = getWindowsEnv("LOCALAPPDATA", { execFileSync: context.execFileSync });
|
|
312
|
+
canonicalWindowsPath = path.win32.join(
|
|
313
|
+
localAppData,
|
|
314
|
+
"surf-cli",
|
|
315
|
+
"host-wrapper-wsl.cmd",
|
|
316
|
+
);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return {
|
|
319
|
+
id: "wrapper-launch",
|
|
320
|
+
status: "fail",
|
|
321
|
+
message: `Could not resolve Surf's managed Windows wrapper path: ${error.message}`,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (normalizeWindowsPath(manifest.path) !== normalizeWindowsPath(canonicalWindowsPath)) {
|
|
326
|
+
return {
|
|
327
|
+
id: "wrapper-launch",
|
|
328
|
+
status: "warn",
|
|
329
|
+
message:
|
|
330
|
+
"Skipped launch probe because the manifest does not point to Surf's managed WSL wrapper. Run `surf install <extension-id>` to restore the managed wrapper.",
|
|
331
|
+
path: manifest.path,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let wrapperFsPath;
|
|
336
|
+
try {
|
|
337
|
+
wrapperFsPath = fsPathFromManifestPath(canonicalWindowsPath, context);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return {
|
|
340
|
+
id: "wrapper-launch",
|
|
341
|
+
status: "fail",
|
|
342
|
+
message: `Could not resolve Surf's managed WSL wrapper: ${error.message}`,
|
|
343
|
+
path: canonicalWindowsPath,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!context.fs.existsSync(wrapperFsPath)) {
|
|
348
|
+
return {
|
|
349
|
+
id: "wrapper-launch",
|
|
350
|
+
status: "fail",
|
|
351
|
+
message: `Surf's managed WSL wrapper does not exist: ${canonicalWindowsPath}`,
|
|
352
|
+
path: canonicalWindowsPath,
|
|
353
|
+
fsPath: wrapperFsPath,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let wrapperContent;
|
|
358
|
+
try {
|
|
359
|
+
wrapperContent = context.fs.readFileSync(wrapperFsPath, "utf8");
|
|
360
|
+
} catch (error) {
|
|
361
|
+
return {
|
|
362
|
+
id: "wrapper-launch",
|
|
363
|
+
status: "fail",
|
|
364
|
+
message: `Could not read Surf's managed WSL wrapper: ${error.message}`,
|
|
365
|
+
path: canonicalWindowsPath,
|
|
366
|
+
fsPath: wrapperFsPath,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const nodePath = context.nodePath || findNode();
|
|
371
|
+
const hostPath = context.hostPath || getHostPath();
|
|
372
|
+
const distro = context.env.WSL_DISTRO_NAME;
|
|
373
|
+
let explicitWrapper = null;
|
|
374
|
+
let defaultWrapper = null;
|
|
375
|
+
if (nodePath && hostPath && distro) {
|
|
376
|
+
try {
|
|
377
|
+
explicitWrapper = renderWslWrapper(nodePath, hostPath, distro);
|
|
378
|
+
defaultWrapper = renderWslWrapper(nodePath, hostPath, null);
|
|
379
|
+
} catch {
|
|
380
|
+
// Do not execute a wrapper whose installed paths cannot be rendered safely.
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (wrapperContent !== explicitWrapper && wrapperContent !== defaultWrapper) {
|
|
384
|
+
return {
|
|
385
|
+
id: "wrapper-launch",
|
|
386
|
+
status: "warn",
|
|
387
|
+
message:
|
|
388
|
+
"Surf's managed WSL wrapper does not match this installation, so it was not executed. Run `surf install <extension-id>` to replace it.",
|
|
389
|
+
path: canonicalWindowsPath,
|
|
390
|
+
fsPath: wrapperFsPath,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const observedDistro = context.probeWindowsWrapper(canonicalWindowsPath, {
|
|
396
|
+
execFileSync: context.execFileSync,
|
|
397
|
+
verifyDistro: wrapperContent === defaultWrapper,
|
|
398
|
+
});
|
|
399
|
+
if (wrapperContent === defaultWrapper && observedDistro !== distro) {
|
|
400
|
+
throw new Error("Windows default WSL distro does not match this installation");
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
id: "wrapper-launch",
|
|
404
|
+
status: "pass",
|
|
405
|
+
message: "Surf's managed WSL wrapper completed its launch probe",
|
|
406
|
+
path: canonicalWindowsPath,
|
|
407
|
+
fsPath: wrapperFsPath,
|
|
408
|
+
};
|
|
409
|
+
} catch (error) {
|
|
410
|
+
return {
|
|
411
|
+
id: "wrapper-launch",
|
|
412
|
+
status: "fail",
|
|
413
|
+
message: `Surf's managed WSL wrapper failed validation: ${error.message}`,
|
|
414
|
+
path: canonicalWindowsPath,
|
|
415
|
+
fsPath: wrapperFsPath,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
291
420
|
async function checkSocket(socketPath, context) {
|
|
292
421
|
const checks = [];
|
|
293
422
|
if (context.platform !== "win32") {
|
|
@@ -371,6 +500,20 @@ function summarize(checks) {
|
|
|
371
500
|
function buildRecommendations(report) {
|
|
372
501
|
const recommendations = [];
|
|
373
502
|
const failedIds = new Set(report.checks.filter((check) => check.status === "fail").map((check) => check.id));
|
|
503
|
+
const debugRelevantIds = new Set([
|
|
504
|
+
"windows-registry",
|
|
505
|
+
"manifest-file",
|
|
506
|
+
"manifest-json",
|
|
507
|
+
"manifest-shape",
|
|
508
|
+
"manifest-name",
|
|
509
|
+
"manifest-type",
|
|
510
|
+
"manifest-origins",
|
|
511
|
+
"manifest-path",
|
|
512
|
+
"manifest-path-executable",
|
|
513
|
+
"wrapper-launch",
|
|
514
|
+
"socket-file",
|
|
515
|
+
"socket-connect",
|
|
516
|
+
]);
|
|
374
517
|
|
|
375
518
|
if (failedIds.has("windows-registry")) {
|
|
376
519
|
recommendations.push("Run `surf install <extension-id> --browser <browser>` so Windows registers the native messaging host, then restart the browser.");
|
|
@@ -384,12 +527,18 @@ function buildRecommendations(report) {
|
|
|
384
527
|
if (failedIds.has("manifest-path") || failedIds.has("manifest-path-executable")) {
|
|
385
528
|
recommendations.push("Reinstall the native host so the manifest path points at the current Surf wrapper.");
|
|
386
529
|
}
|
|
530
|
+
if (failedIds.has("wrapper-launch")) {
|
|
531
|
+
recommendations.push("Rerun `surf install <extension-id>` from the same WSL distro so Surf can replace and validate the Windows wrapper.");
|
|
532
|
+
}
|
|
387
533
|
if (failedIds.has("manifest-supported")) {
|
|
388
534
|
recommendations.push("Choose a browser supported for this target, or rerun with `--browser all` to inspect every supported setup.");
|
|
389
535
|
}
|
|
390
536
|
if (failedIds.has("socket-file") || failedIds.has("socket-connect")) {
|
|
391
537
|
recommendations.push("Make sure the browser is running with the Surf extension enabled, then restart the browser after install changes.");
|
|
392
538
|
}
|
|
539
|
+
if ([...failedIds].some((id) => debugRelevantIds.has(id))) {
|
|
540
|
+
recommendations.push("Open chrome://extensions and inspect Surf's service worker console. In Surf's Details > Extension options, enable Debug Mode, reproduce the failure, then disable Debug Mode when finished.");
|
|
541
|
+
}
|
|
393
542
|
if (report.environment.surfSocketSet) {
|
|
394
543
|
recommendations.push("SURF_SOCKET is set; make sure Chrome launches the native host with the same socket value.");
|
|
395
544
|
}
|
|
@@ -478,6 +627,9 @@ async function runDoctor(rawOptions = {}, deps = {}) {
|
|
|
478
627
|
effectiveTarget,
|
|
479
628
|
fs: deps.fs || fs,
|
|
480
629
|
execFileSync: deps.execFileSync || execFileSync,
|
|
630
|
+
probeWindowsWrapper: deps.probeWindowsWrapper || probeWindowsWrapper,
|
|
631
|
+
nodePath: deps.nodePath,
|
|
632
|
+
hostPath: deps.hostPath,
|
|
481
633
|
connectSocket: deps.connectSocket || connectSocket,
|
|
482
634
|
connectTimeoutMs: options.connectTimeoutMs,
|
|
483
635
|
};
|
|
@@ -493,15 +645,20 @@ async function runDoctor(rawOptions = {}, deps = {}) {
|
|
|
493
645
|
for (const browserKey of browsers) {
|
|
494
646
|
const browser = BROWSERS[browserKey];
|
|
495
647
|
const browserChecks = [];
|
|
496
|
-
|
|
648
|
+
const usesWindowsRegistry =
|
|
649
|
+
(context.platform === "win32" || context.effectiveTarget === "wsl-windows") &&
|
|
650
|
+
Boolean(browser.win32);
|
|
651
|
+
let manifestPath = null;
|
|
497
652
|
|
|
498
|
-
if (
|
|
653
|
+
if (usesWindowsRegistry) {
|
|
499
654
|
const registry = checkWindowsRegistry(browserKey, context);
|
|
500
655
|
browserChecks.push(registry.check);
|
|
501
|
-
|
|
656
|
+
manifestPath = registry.manifestPath;
|
|
657
|
+
} else {
|
|
658
|
+
manifestPath = manifestPathForBrowser(browserKey, context);
|
|
502
659
|
}
|
|
503
660
|
|
|
504
|
-
if (!manifestPath) {
|
|
661
|
+
if (!manifestPath && !usesWindowsRegistry) {
|
|
505
662
|
const check = {
|
|
506
663
|
id: "manifest-supported",
|
|
507
664
|
status: options.browser === "all" ? "warn" : "fail",
|
|
@@ -516,6 +673,10 @@ async function runDoctor(rawOptions = {}, deps = {}) {
|
|
|
516
673
|
|
|
517
674
|
const result = checkManifest(manifestPath, context);
|
|
518
675
|
browserChecks.push(...result.checks.map((check) => ({ ...check, browser: browserKey })));
|
|
676
|
+
if (context.effectiveTarget === "wsl-windows" && result.manifest) {
|
|
677
|
+
const wrapperLaunch = checkWslWrapperLaunch(result.manifest, context);
|
|
678
|
+
if (wrapperLaunch) browserChecks.push({ ...wrapperLaunch, browser: browserKey });
|
|
679
|
+
}
|
|
519
680
|
checks.push(...browserChecks);
|
|
520
681
|
manifests.push({
|
|
521
682
|
browser: browserKey,
|
|
@@ -638,5 +799,4 @@ module.exports = {
|
|
|
638
799
|
parseDoctorArgs,
|
|
639
800
|
runDoctor,
|
|
640
801
|
runDoctorCli,
|
|
641
|
-
windowsPathToWslPath,
|
|
642
802
|
};
|