@pxlarified/browser 0.1.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 +182 -0
- package/bin/OpenBrowser.js +8 -0
- package/dist/extensions/firefox/openbrowser-dev.xpi +0 -0
- package/dist/extensions/firefox/openbrowser.xpi +0 -0
- package/dist/extensions/firefox/signed/8cfe45915b2d499ab5d0-0.1.0.xpi +0 -0
- package/extensions/background.js +194 -0
- package/extensions/content.js +275 -0
- package/extensions/manifest.json +26 -0
- package/package.json +37 -0
- package/scripts/build.js +60 -0
- package/scripts/sign-firefox.js +74 -0
- package/src/bridge/client.js +87 -0
- package/src/browsers/firefox-family.js +204 -0
- package/src/browsers/registry.js +19 -0
- package/src/browsers/zen.js +70 -0
- package/src/cli.js +155 -0
- package/src/constants.js +7 -0
- package/src/native-host.cjs +162 -0
- package/src/util/paths.js +24 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pxlarified/browser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Minimal local browser-control CLI for OpenBrowser.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"OpenBrowser": "./bin/OpenBrowser.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "node scripts/build.js firefox",
|
|
11
|
+
"build:firefox": "node scripts/build.js firefox",
|
|
12
|
+
"release:firefox": "node scripts/sign-firefox.js",
|
|
13
|
+
"prepack": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"src/",
|
|
18
|
+
"extensions/manifest.json",
|
|
19
|
+
"extensions/background.js",
|
|
20
|
+
"extensions/content.js",
|
|
21
|
+
"dist/extensions/",
|
|
22
|
+
"scripts/",
|
|
23
|
+
"README.md",
|
|
24
|
+
"package.json"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"web-ext": "^8.3.0",
|
|
34
|
+
"yazl": "^3.3.1"
|
|
35
|
+
},
|
|
36
|
+
"license": "UNLICENSED"
|
|
37
|
+
}
|
package/scripts/build.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import yazl from "yazl";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const target = process.argv[2] || "firefox";
|
|
9
|
+
|
|
10
|
+
if (target !== "firefox") {
|
|
11
|
+
throw new Error(`Unsupported build target: ${target}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sourceDir = path.join(root, "extensions");
|
|
15
|
+
const buildDir = path.join(root, "build", "firefox-extension");
|
|
16
|
+
const outDir = path.join(root, "dist", "extensions", "firefox");
|
|
17
|
+
const outFile = path.join(outDir, "openbrowser-dev.xpi");
|
|
18
|
+
|
|
19
|
+
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
20
|
+
fs.mkdirSync(buildDir, { recursive: true });
|
|
21
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
22
|
+
|
|
23
|
+
copyFile(path.join(sourceDir, "manifest.json"), path.join(buildDir, "manifest.json"));
|
|
24
|
+
copyFile(path.join(sourceDir, "background.js"), path.join(buildDir, "background.js"));
|
|
25
|
+
copyFile(path.join(sourceDir, "content.js"), path.join(buildDir, "content.js"));
|
|
26
|
+
|
|
27
|
+
await zipDirectory(buildDir, outFile);
|
|
28
|
+
console.log(`Built ${path.relative(root, outFile)}`);
|
|
29
|
+
|
|
30
|
+
function copyFile(from, to) {
|
|
31
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
32
|
+
fs.copyFileSync(from, to);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function zipDirectory(directory, destination) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const zip = new yazl.ZipFile();
|
|
38
|
+
const output = fs.createWriteStream(destination);
|
|
39
|
+
|
|
40
|
+
output.on("close", resolve);
|
|
41
|
+
output.on("error", reject);
|
|
42
|
+
zip.outputStream.on("error", reject);
|
|
43
|
+
zip.outputStream.pipe(output);
|
|
44
|
+
|
|
45
|
+
for (const file of listFiles(directory)) {
|
|
46
|
+
zip.addFile(file, path.relative(directory, file).replace(/\\/g, "/"));
|
|
47
|
+
}
|
|
48
|
+
zip.end();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function listFiles(directory) {
|
|
53
|
+
const result = [];
|
|
54
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
55
|
+
const full = path.join(directory, entry.name);
|
|
56
|
+
if (entry.isDirectory()) result.push(...listFiles(full));
|
|
57
|
+
else result.push(full);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
loadDotEnv(path.join(root, ".env"));
|
|
9
|
+
|
|
10
|
+
const sourceDir = path.join(root, "extensions");
|
|
11
|
+
const artifactsDir = path.join(root, "dist", "extensions", "firefox", "signed");
|
|
12
|
+
const bundledArtifact = path.join(root, "dist", "extensions", "firefox", "openbrowser.xpi");
|
|
13
|
+
|
|
14
|
+
if (!process.env.AMO_JWT_ISSUER || !process.env.AMO_JWT_SECRET) {
|
|
15
|
+
throw new Error("Firefox signing requires AMO_JWT_ISSUER and AMO_JWT_SECRET.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(path.join(sourceDir, "manifest.json"))) {
|
|
19
|
+
throw new Error("Missing Firefox extension source manifest at ./extensions/manifest.json.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await run(process.execPath, [path.join(root, "scripts", "build.js"), "firefox"]);
|
|
23
|
+
|
|
24
|
+
fs.rmSync(artifactsDir, { recursive: true, force: true });
|
|
25
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
await run("npx", [
|
|
28
|
+
"web-ext",
|
|
29
|
+
"sign",
|
|
30
|
+
"--source-dir", sourceDir,
|
|
31
|
+
"--channel", "unlisted",
|
|
32
|
+
"--api-key", process.env.AMO_JWT_ISSUER,
|
|
33
|
+
"--api-secret", process.env.AMO_JWT_SECRET,
|
|
34
|
+
"--artifacts-dir", artifactsDir,
|
|
35
|
+
], { stdio: "inherit" });
|
|
36
|
+
|
|
37
|
+
const signed = fs.readdirSync(artifactsDir)
|
|
38
|
+
.filter((file) => file.endsWith(".xpi"))
|
|
39
|
+
.map((file) => path.join(artifactsDir, file))
|
|
40
|
+
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
|
|
41
|
+
|
|
42
|
+
if (!signed) throw new Error("web-ext did not produce a signed .xpi artifact.");
|
|
43
|
+
fs.copyFileSync(signed, bundledArtifact);
|
|
44
|
+
console.log(`Bundled signed Firefox artifact at ${path.relative(root, bundledArtifact)}`);
|
|
45
|
+
|
|
46
|
+
function loadDotEnv(file) {
|
|
47
|
+
if (!fs.existsSync(file)) return;
|
|
48
|
+
|
|
49
|
+
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
53
|
+
const separator = trimmed.indexOf("=");
|
|
54
|
+
if (separator === -1) continue;
|
|
55
|
+
|
|
56
|
+
const key = trimmed.slice(0, separator).trim();
|
|
57
|
+
let value = trimmed.slice(separator + 1).trim();
|
|
58
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
59
|
+
value = value.slice(1, -1);
|
|
60
|
+
}
|
|
61
|
+
if (!process.env[key]) process.env[key] = value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function run(command, args, options = {}) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const child = spawn(command, args, { cwd: root, stdio: options.stdio || "inherit", shell: process.platform === "win32" });
|
|
68
|
+
child.on("error", reject);
|
|
69
|
+
child.on("exit", (code) => {
|
|
70
|
+
if (code === 0) resolve();
|
|
71
|
+
else reject(new Error(`${command} ${args.join(" ")} exited with ${code}`));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { bridgeSocketPath } from "../util/paths.js";
|
|
3
|
+
|
|
4
|
+
export class BridgeUnavailableError extends Error {
|
|
5
|
+
constructor(browser, cause) {
|
|
6
|
+
super(
|
|
7
|
+
`OpenBrowser bridge for ${browser} is not available. Start ${browser}, make sure the OpenBrowser extension is installed, then retry.`,
|
|
8
|
+
);
|
|
9
|
+
this.name = "BridgeUnavailableError";
|
|
10
|
+
this.cause = cause;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function sendBridgeCommand(browser, command, args = {}, options = {}) {
|
|
15
|
+
const socketPath = bridgeSocketPath(browser);
|
|
16
|
+
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
17
|
+
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const socket = net.createConnection(socketPath);
|
|
20
|
+
let settled = false;
|
|
21
|
+
let buffer = "";
|
|
22
|
+
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
finish(new Error(`Timed out waiting for ${browser} to complete ${command}.`));
|
|
25
|
+
socket.destroy();
|
|
26
|
+
}, timeoutMs);
|
|
27
|
+
|
|
28
|
+
function finish(error, value) {
|
|
29
|
+
if (settled) return;
|
|
30
|
+
settled = true;
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
if (error) reject(error);
|
|
33
|
+
else resolve(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
socket.on("connect", () => {
|
|
37
|
+
socket.write(`${JSON.stringify({ command, args })}\n`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
socket.on("data", (chunk) => {
|
|
41
|
+
buffer += chunk.toString("utf8");
|
|
42
|
+
const newline = buffer.indexOf("\n");
|
|
43
|
+
if (newline === -1) return;
|
|
44
|
+
const line = buffer.slice(0, newline);
|
|
45
|
+
socket.end();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const response = JSON.parse(line);
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const error = new Error(response.error?.message || "OpenBrowser command failed.");
|
|
51
|
+
error.code = response.error?.code;
|
|
52
|
+
finish(error);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
finish(null, response.result);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
finish(error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
socket.on("error", (error) => {
|
|
62
|
+
if (error.code === "ENOENT" || error.code === "ECONNREFUSED") {
|
|
63
|
+
finish(new BridgeUnavailableError(browser, error));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
finish(error);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function sendBridgeCommandWithRetry(browser, command, args = {}, options = {}) {
|
|
72
|
+
const attempts = options.attempts ?? 1;
|
|
73
|
+
const delayMs = options.delayMs ?? 500;
|
|
74
|
+
let lastError;
|
|
75
|
+
|
|
76
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
77
|
+
try {
|
|
78
|
+
return await sendBridgeCommand(browser, command, args, options);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
lastError = error;
|
|
81
|
+
if (!(error instanceof BridgeUnavailableError) || attempt === attempts - 1) break;
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw lastError;
|
|
87
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { EXTENSION_ID, FIREFOX_DEV_ARTIFACT, FIREFOX_SIGNED_ARTIFACT, NATIVE_HOST_NAME } from "../constants.js";
|
|
7
|
+
import { openBrowserHome, packageRoot } from "../util/paths.js";
|
|
8
|
+
|
|
9
|
+
export class FirefoxFamilyAdapter {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.name = options.name;
|
|
12
|
+
this.displayName = options.displayName;
|
|
13
|
+
this.profileRoots = options.profileRoots;
|
|
14
|
+
this.launchCommands = options.launchCommands;
|
|
15
|
+
this.nativeManifestRoots = options.nativeManifestRoots;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
artifactPath() {
|
|
19
|
+
const root = packageRoot();
|
|
20
|
+
const signed = path.join(root, FIREFOX_SIGNED_ARTIFACT);
|
|
21
|
+
if (fs.existsSync(signed)) return signed;
|
|
22
|
+
return path.join(root, FIREFOX_DEV_ARTIFACT);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async install() {
|
|
26
|
+
const artifact = this.artifactPath();
|
|
27
|
+
if (!fs.existsSync(artifact)) {
|
|
28
|
+
throw new Error(`Missing Firefox extension artifact: ${artifact}. Run npm run build:firefox first.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const nativeHost = installNativeHost(this.name);
|
|
32
|
+
const manifests = installNativeMessagingManifests(this.nativeManifestRoots, nativeHost);
|
|
33
|
+
const profiles = this.findProfiles();
|
|
34
|
+
if (profiles.length === 0) {
|
|
35
|
+
throw new Error(`No ${this.displayName} profile was found. Open ${this.displayName} once, then run install again.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const installedExtensions = [];
|
|
39
|
+
for (const profile of profiles) {
|
|
40
|
+
const extensionsDir = path.join(profile, "extensions");
|
|
41
|
+
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
42
|
+
const destination = path.join(extensionsDir, `${EXTENSION_ID}.xpi`);
|
|
43
|
+
fs.copyFileSync(artifact, destination);
|
|
44
|
+
installedExtensions.push(destination);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
browser: this.name,
|
|
49
|
+
artifact,
|
|
50
|
+
signed: path.basename(artifact) === "openbrowser.xpi",
|
|
51
|
+
profiles,
|
|
52
|
+
installedExtensions,
|
|
53
|
+
nativeHost,
|
|
54
|
+
nativeManifests: manifests,
|
|
55
|
+
note: "Extension install/update is staged in the profile. Restart the browser if it does not load immediately.",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async launch() {
|
|
60
|
+
for (const command of this.launchCommands) {
|
|
61
|
+
try {
|
|
62
|
+
const child = spawn(command.command, command.args, {
|
|
63
|
+
detached: true,
|
|
64
|
+
stdio: "ignore",
|
|
65
|
+
});
|
|
66
|
+
child.unref();
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
// Try the next known command.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
findProfiles() {
|
|
76
|
+
const roots = this.profileRoots.map((root) => expandHome(root));
|
|
77
|
+
const profiles = [];
|
|
78
|
+
|
|
79
|
+
for (const root of roots) {
|
|
80
|
+
const profilesIni = path.join(root, "profiles.ini");
|
|
81
|
+
if (!fs.existsSync(profilesIni)) continue;
|
|
82
|
+
profiles.push(...readProfilesIni(profilesIni, root));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return uniqueExistingDirectories(profiles);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function installNativeHost(browser) {
|
|
90
|
+
const source = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../native-host.cjs");
|
|
91
|
+
const dir = path.join(openBrowserHome(), "native-host");
|
|
92
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
93
|
+
|
|
94
|
+
const hostScript = path.join(dir, "openbrowser-native-host.cjs");
|
|
95
|
+
fs.copyFileSync(source, hostScript);
|
|
96
|
+
if (process.platform !== "win32") fs.chmodSync(hostScript, 0o755);
|
|
97
|
+
|
|
98
|
+
if (process.platform === "win32") {
|
|
99
|
+
const cmd = path.join(dir, `openbrowser-native-host-${browser}.cmd`);
|
|
100
|
+
fs.writeFileSync(cmd, `@echo off\r\nset OPENBROWSER_BROWSER=${browser}\r\n"${process.execPath}" "${hostScript}"\r\n`, "utf8");
|
|
101
|
+
return cmd;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const launcher = path.join(dir, `openbrowser-native-host-${browser}`);
|
|
105
|
+
fs.writeFileSync(launcher, `#!/bin/sh\nOPENBROWSER_BROWSER=${shellQuote(browser)} exec ${shellQuote(process.execPath)} ${shellQuote(hostScript)}\n`, "utf8");
|
|
106
|
+
fs.chmodSync(launcher, 0o755);
|
|
107
|
+
return launcher;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function installNativeMessagingManifests(roots, hostPath) {
|
|
111
|
+
const manifest = {
|
|
112
|
+
name: NATIVE_HOST_NAME,
|
|
113
|
+
description: "OpenBrowser local user-scoped native bridge",
|
|
114
|
+
path: hostPath,
|
|
115
|
+
type: "stdio",
|
|
116
|
+
allowed_extensions: [EXTENSION_ID],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const written = [];
|
|
120
|
+
const manifestDirectoryName = process.platform === "linux" ? "native-messaging-hosts" : "NativeMessagingHosts";
|
|
121
|
+
for (const root of roots.map((entry) => expandHome(entry))) {
|
|
122
|
+
const dir = path.join(root, manifestDirectoryName);
|
|
123
|
+
try {
|
|
124
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
125
|
+
const target = path.join(dir, `${NATIVE_HOST_NAME}.json`);
|
|
126
|
+
fs.writeFileSync(target, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
127
|
+
written.push(target);
|
|
128
|
+
} catch {
|
|
129
|
+
// Some candidate vendor directories may not be writable or useful on this platform.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (process.platform === "win32") {
|
|
134
|
+
const manifestDir = path.join(openBrowserHome(), "native-messaging-hosts");
|
|
135
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
136
|
+
const manifestPath = path.join(manifestDir, `${NATIVE_HOST_NAME}.json`);
|
|
137
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
138
|
+
written.push(manifestPath);
|
|
139
|
+
try {
|
|
140
|
+
spawn("reg", [
|
|
141
|
+
"add",
|
|
142
|
+
`HKCU\\Software\\Mozilla\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`,
|
|
143
|
+
"/ve",
|
|
144
|
+
"/t",
|
|
145
|
+
"REG_SZ",
|
|
146
|
+
"/d",
|
|
147
|
+
manifestPath,
|
|
148
|
+
"/f",
|
|
149
|
+
], { stdio: "ignore" });
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return written;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function shellQuote(value) {
|
|
157
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function expandHome(value) {
|
|
161
|
+
if (!value.startsWith("~")) return value;
|
|
162
|
+
return path.join(os.homedir(), value.slice(2));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readProfilesIni(file, root) {
|
|
166
|
+
const text = fs.readFileSync(file, "utf8");
|
|
167
|
+
const sections = [];
|
|
168
|
+
let current = null;
|
|
169
|
+
|
|
170
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
171
|
+
const line = rawLine.trim();
|
|
172
|
+
if (!line || line.startsWith(";") || line.startsWith("#")) continue;
|
|
173
|
+
const section = line.match(/^\[(.+)]$/);
|
|
174
|
+
if (section) {
|
|
175
|
+
current = { name: section[1] };
|
|
176
|
+
sections.push(current);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const separator = line.indexOf("=");
|
|
180
|
+
if (separator === -1 || !current) continue;
|
|
181
|
+
current[line.slice(0, separator)] = line.slice(separator + 1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const profileSections = sections.filter((section) => section.name.startsWith("Profile") && section.Path);
|
|
185
|
+
profileSections.sort((a, b) => Number(b.Default || 0) - Number(a.Default || 0));
|
|
186
|
+
|
|
187
|
+
return profileSections.map((section) => {
|
|
188
|
+
if (section.IsRelative === "1") return path.join(root, section.Path);
|
|
189
|
+
return section.Path;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function uniqueExistingDirectories(paths) {
|
|
194
|
+
const seen = new Set();
|
|
195
|
+
const result = [];
|
|
196
|
+
for (const candidate of paths) {
|
|
197
|
+
const resolved = path.resolve(candidate);
|
|
198
|
+
if (seen.has(resolved)) continue;
|
|
199
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) continue;
|
|
200
|
+
seen.add(resolved);
|
|
201
|
+
result.push(resolved);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ZenBrowserAdapter } from "./zen.js";
|
|
2
|
+
import { DEFAULT_BROWSER } from "../constants.js";
|
|
3
|
+
|
|
4
|
+
const adapters = new Map([
|
|
5
|
+
["zen", new ZenBrowserAdapter()],
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export function supportedBrowsers() {
|
|
9
|
+
return [...adapters.keys()];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveBrowser(browser) {
|
|
13
|
+
const name = (browser || DEFAULT_BROWSER).toLowerCase();
|
|
14
|
+
const adapter = adapters.get(name);
|
|
15
|
+
if (!adapter) {
|
|
16
|
+
throw new Error(`Unsupported browser: ${browser}. Supported browsers: ${supportedBrowsers().join(", ")}.`);
|
|
17
|
+
}
|
|
18
|
+
return adapter;
|
|
19
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FirefoxFamilyAdapter } from "./firefox-family.js";
|
|
4
|
+
|
|
5
|
+
function platformProfileRoots() {
|
|
6
|
+
if (process.platform === "darwin") {
|
|
7
|
+
return [
|
|
8
|
+
"~/Library/Application Support/zen",
|
|
9
|
+
"~/Library/Application Support/Zen",
|
|
10
|
+
"~/Library/Application Support/Zen Browser",
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (process.platform === "win32") {
|
|
15
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
16
|
+
return [path.join(appData, "zen"), path.join(appData, "Zen"), path.join(appData, "Zen Browser")];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return ["~/.zen", "~/.zen-browser", "~/.mozilla/zen"];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function platformNativeManifestRoots() {
|
|
23
|
+
if (process.platform === "darwin") {
|
|
24
|
+
return [
|
|
25
|
+
"~/Library/Application Support/Mozilla",
|
|
26
|
+
"~/Library/Application Support/zen",
|
|
27
|
+
"~/Library/Application Support/Zen",
|
|
28
|
+
"~/Library/Application Support/Zen Browser",
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (process.platform === "win32") {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return ["~/.mozilla", "~/.zen", "~/.zen-browser"];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function platformLaunchCommands() {
|
|
40
|
+
if (process.platform === "darwin") {
|
|
41
|
+
return [
|
|
42
|
+
{ command: "open", args: ["-a", "Zen Browser"] },
|
|
43
|
+
{ command: "open", args: ["-a", "Zen"] },
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (process.platform === "win32") {
|
|
48
|
+
return [
|
|
49
|
+
{ command: "cmd", args: ["/c", "start", "", "zen"] },
|
|
50
|
+
{ command: "cmd", args: ["/c", "start", "", "zen-browser"] },
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
{ command: "zen-browser", args: [] },
|
|
56
|
+
{ command: "zen", args: [] },
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ZenBrowserAdapter extends FirefoxFamilyAdapter {
|
|
61
|
+
constructor() {
|
|
62
|
+
super({
|
|
63
|
+
name: "zen",
|
|
64
|
+
displayName: "Zen",
|
|
65
|
+
profileRoots: platformProfileRoots(),
|
|
66
|
+
nativeManifestRoots: platformNativeManifestRoots(),
|
|
67
|
+
launchCommands: platformLaunchCommands(),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|