Skip to content

Commit ce8d1ae

Browse files
committed
feat: Windows support
- Use named pipes (//./pipe/surf) instead of Unix sockets on win32 - Route temp files to %LOCALAPPDATA%/Temp/surf instead of /tmp - Fix ImageMagick resize: quote geometry arg to prevent cmd.exe interpreting > as redirect - Skip Unix-only operations (chmod, unlink socket) on win32 - Network store defaults to %LOCALAPPDATA%/surf on Windows Tested on Windows 11 ARM64 with Chromium (scoop), Node 24, ImageMagick 7. All core features working: navigation, page reading, screenshots (with resize), tab management.
1 parent dfbc169 commit ce8d1ae

File tree

6 files changed

+42
-25
lines changed

6 files changed

+42
-25
lines changed

native/cli.cjs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ const networkStore = require("./network-store.cjs");
1010
const { parseDoCommands } = require("./do-parser.cjs");
1111
const { executeDoSteps } = require("./do-executor.cjs");
1212

13-
const SOCKET_PATH = "/tmp/surf.sock";
13+
const IS_WIN = process.platform === "win32";
14+
const SURF_TMP = IS_WIN ? path.join(os.tmpdir(), "surf") : "/tmp";
15+
const SOCKET_PATH = IS_WIN ? "//./pipe/surf" : "/tmp/surf.sock";
16+
if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
1417

1518
// ============================================================================
1619
// Workflow Resolution and Management
@@ -268,12 +271,14 @@ function resizeImage(filePath, maxSize) {
268271
const height = parseInt(sizeInfo.match(/pixelHeight:\s*(\d+)/)?.[1] || "0", 10);
269272
return { success: true, width, height };
270273
} else {
271-
// Linux/other: use ImageMagick (try IM6 first, then IM7)
274+
// Linux/Windows: use ImageMagick (try IM6 first, then IM7)
275+
// On Windows, \> is interpreted as redirect by cmd.exe — quote the geometry
276+
const resizeArg = IS_WIN ? `"${maxSize}x${maxSize}>"` : `${maxSize}x${maxSize}\\>`;
272277
try {
273-
execSync(`convert "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
278+
execSync(`convert "${filePath}" -resize ${resizeArg} "${filePath}"`, { stdio: "pipe" });
274279
} catch {
275280
// IM7 uses 'magick' as main command
276-
execSync(`magick "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
281+
execSync(`magick "${filePath}" -resize ${resizeArg} "${filePath}"`, { stdio: "pipe" });
277282
}
278283
// Get dimensions (IM7 may need 'magick identify' instead of just 'identify')
279284
let sizeInfo;
@@ -2449,7 +2454,7 @@ tool = ALIASES[tool] || tool;
24492454
const config = loadConfig();
24502455
const autoSaveEnabled = config.autoSaveScreenshots !== false && !options["no-save"];
24512456
if (tool === "screenshot" && !options.output && !options.savePath && autoSaveEnabled) {
2452-
options.savePath = `/tmp/surf-snap-${Date.now()}.png`;
2457+
options.savePath = path.join(SURF_TMP, `surf-snap-${Date.now()}.png`);
24532458
}
24542459

24552460
if (tool === "smoke") {
@@ -2813,7 +2818,7 @@ const sendRequest = (toolName, toolArgs = {}) => {
28132818

28142819
const performAutoCapture = async () => {
28152820
const timestamp = Date.now();
2816-
const screenshotPath = `/tmp/surf-error-${timestamp}.png`;
2821+
const screenshotPath = path.join(SURF_TMP, `surf-error-${timestamp}.png`);
28172822

28182823
try {
28192824
const [screenshotResp, consoleResp] = await Promise.all([

native/do-executor.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
const net = require("net");
1414

15-
const SOCKET_PATH = "/tmp/surf.sock";
15+
const SOCKET_PATH = process.platform === "win32" ? "//./pipe/surf" : "/tmp/surf.sock";
1616

1717
// Maximum iterations for loops (safety cap)
1818
const MAX_LOOP_ITERATIONS = 100;

native/host.cjs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ const perplexityClient = require("./perplexity-client.cjs");
1212
const grokClient = require("./grok-client.cjs");
1313
const { mapToolToMessage, mapComputerAction, formatToolContent } = require("./host-helpers.cjs");
1414

15-
const SOCKET_PATH = "/tmp/surf.sock";
15+
const IS_WIN = process.platform === "win32";
16+
const SURF_TMP = IS_WIN ? path.join(os.tmpdir(), "surf") : "/tmp";
17+
const SOCKET_PATH = IS_WIN ? "//./pipe/surf" : "/tmp/surf.sock";
18+
// Ensure tmp dir exists on Windows
19+
if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
1620

1721
// Cross-platform image resize (macOS: sips, Linux: ImageMagick)
1822
function resizeImage(filePath, maxSize) {
@@ -27,12 +31,14 @@ function resizeImage(filePath, maxSize) {
2731
const height = parseInt(sizeInfo.match(/pixelHeight:\s*(\d+)/)?.[1] || "0", 10);
2832
return { success: true, width, height };
2933
} else {
30-
// Linux/other: use ImageMagick (try IM6 first, then IM7)
34+
// Linux/Windows: use ImageMagick (try IM6 first, then IM7)
35+
// On Windows, \> is interpreted as redirect by cmd.exe — quote the geometry
36+
const resizeArg = IS_WIN ? `"${maxSize}x${maxSize}>"` : `${maxSize}x${maxSize}\\>`;
3137
try {
32-
execSync(`convert "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
38+
execSync(`convert "${filePath}" -resize ${resizeArg} "${filePath}"`, { stdio: "pipe" });
3339
} catch {
3440
// IM7 uses 'magick' as main command
35-
execSync(`magick "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
41+
execSync(`magick "${filePath}" -resize ${resizeArg} "${filePath}"`, { stdio: "pipe" });
3642
}
3743
// Get dimensions (IM7 may need 'magick identify' instead of just 'identify')
3844
let sizeInfo;
@@ -73,7 +79,7 @@ async function processAiQueue() {
7379
setTimeout(processAiQueue, 2000);
7480
}
7581
}
76-
const LOG_FILE = "/tmp/surf-host.log";
82+
const LOG_FILE = path.join(SURF_TMP, "surf-host.log");
7783
const AUTH_FILE = path.join(os.homedir(), ".pi", "agent", "auth.json");
7884

7985
const DEFAULT_RETRY_OPTIONS = {
@@ -288,9 +294,7 @@ const log = (msg) => {
288294

289295
log("Host starting...");
290296

291-
try {
292-
fs.unlinkSync(SOCKET_PATH);
293-
} catch {}
297+
if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
294298

295299
const pendingRequests = new Map();
296300
const pendingToolRequests = new Map();
@@ -1204,15 +1208,15 @@ function processInput() {
12041208
} else if (autoScreenshot && tabId && !msg.error && !msg.base64) {
12051209

12061210
const screenshotId = ++requestCounter;
1207-
const screenshotPath = `/tmp/pi-auto-${Date.now()}.png`;
1211+
const screenshotPath = path.join(SURF_TMP, `pi-auto-${Date.now()}.png`);
12081212

1209-
const autoFiles = fs.readdirSync("/tmp")
1213+
const autoFiles = fs.readdirSync(SURF_TMP)
12101214
.filter(f => f.startsWith("pi-auto-") && f.endsWith(".png"))
12111215
.map(f => ({ name: f, time: parseInt(f.match(/pi-auto-(\d+)\.png/)?.[1] || "0", 10) }))
12121216
.sort((a, b) => b.time - a.time);
12131217
if (autoFiles.length >= 10) {
12141218
autoFiles.slice(9).forEach(f => {
1215-
try { fs.unlinkSync(path.join("/tmp", f.name)); } catch (e) {}
1219+
try { fs.unlinkSync(path.join(SURF_TMP, f.name)); } catch (e) {}
12161220
});
12171221
}
12181222
pendingToolRequests.set(screenshotId, {
@@ -1440,7 +1444,7 @@ const server = net.createServer((socket) => {
14401444

14411445
server.listen(SOCKET_PATH, () => {
14421446
log("Socket server listening on " + SOCKET_PATH);
1443-
fs.chmodSync(SOCKET_PATH, 0o600);
1447+
if (!IS_WIN) { try { fs.chmodSync(SOCKET_PATH, 0o600); } catch {} }
14441448
writeMessage({ type: "HOST_READY" });
14451449
log("Sent HOST_READY to extension");
14461450
});
@@ -1452,14 +1456,14 @@ server.on("error", (err) => {
14521456
process.on("SIGTERM", () => {
14531457
log("SIGTERM received");
14541458
server.close();
1455-
try { fs.unlinkSync(SOCKET_PATH); } catch {}
1459+
if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
14561460
process.exit(0);
14571461
});
14581462

14591463
process.on("SIGINT", () => {
14601464
log("SIGINT received");
14611465
server.close();
1462-
try { fs.unlinkSync(SOCKET_PATH); } catch {}
1466+
if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
14631467
process.exit(0);
14641468
});
14651469

native/mcp-server.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
44
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
55
const { z } = require("zod");
66

7-
const SOCKET_PATH = "/tmp/surf.sock";
7+
const SOCKET_PATH = process.platform === "win32" ? "//./pipe/surf" : "/tmp/surf.sock";
88
const REQUEST_TIMEOUT = 30000;
99

1010
const TOOL_SCHEMAS = {

native/network-store.cjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ const crypto = require("crypto");
1313
const readline = require("readline");
1414

1515
// Configuration
16-
const DEFAULT_BASE = "/tmp/surf";
16+
const DEFAULT_BASE = process.platform === "win32"
17+
? path.join(require("os").tmpdir(), "surf")
18+
: "/tmp/surf";
1719
const DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours
1820
const DEFAULT_MAX_SIZE = 200 * 1024 * 1024; // 200MB
1921
const AUTO_CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour

package-lock.json

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)