#!/usr/bin/env node // Installs the com.peachpasswords.sync native messaging host manifest into // the per-browser NativeMessagingHosts directory so chrome.runtime.connectNative // can find the Peach sync host binary at runtime. // // Unix (macOS / Linux): writes a JSON file under the user's NativeMessagingHosts // directory. Windows requires a registry entry and is not handled directly — // the script prints the JSON path and the registry key the user must add. // // Usage: // node scripts/install-native-sync-host.mjs --browser chrome --extension-id // node scripts/install-native-sync-host.mjs --browser firefox // // Optional flags: // --host-path Override the binary path (default: release build) // --dry-run Print what would be written; do not touch the FS import { mkdir, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import process from "node:process"; import { pathToFileURL } from "node:url"; import { buildNativeManifest } from "./generate-native-sync-manifest.mjs"; const HOST_FILENAME = "com.peachpasswords.sync.json"; function usage() { return [ "Usage:", " node scripts/install-native-sync-host.mjs --browser chrome --extension-id ", " node scripts/install-native-sync-host.mjs --browser firefox [--extension-id ]", "", "Optional:", " --host-path Override binary path (default: release build)", " --dry-run Print the destination only; do not write", ].join("\n"); } function parseArgs(argv) { const options = {}; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (arg === "--help" || arg === "-h") { options.help = true; continue; } if (arg === "--dry-run") { options.dryRun = true; continue; } if (arg === "--show-dir") { options.showDir = true; continue; } if (arg === "--platform-override") { options.platformOverride = argv[i + 1]; i += 1; continue; } if (arg === "--home-override") { options.homeOverride = argv[i + 1]; i += 1; continue; } if (!arg.startsWith("--")) { throw new Error(`unexpected argument: ${arg}`); } const key = arg.slice(2); const value = argv[i + 1]; if (!value || value.startsWith("--")) { throw new Error(`missing value for --${key}`); } options[toCamel(key)] = value; i += 1; } return options; } function toCamel(value) { return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); } /** * Resolves the OS-specific NativeMessagingHosts directory for the requested * browser. Returns `{ dir, registryHint }` — `registryHint` is only set on * Windows where a registry write is also required. */ export function resolveInstallDir(browser, platform = process.platform, home = os.homedir()) { if (browser !== "chrome" && browser !== "firefox") { throw new Error("--browser must be chrome or firefox"); } if (platform === "darwin") { if (browser === "chrome") { return { dir: path.join(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts"), }; } return { dir: path.join(home, "Library", "Application Support", "Mozilla", "NativeMessagingHosts"), }; } if (platform === "linux") { if (browser === "chrome") { return { dir: path.join(home, ".config", "google-chrome", "NativeMessagingHosts") }; } return { dir: path.join(home, ".mozilla", "native-messaging-hosts") }; } if (platform === "win32") { // Windows uses the registry. We still write the JSON to a stable path so // the registry value can point at it. const dir = path.join(home, "AppData", "Local", "Peach", "NativeMessagingHosts"); const registryRoot = browser === "chrome" ? "HKEY_CURRENT_USER\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.peachpasswords.sync" : "HKEY_CURRENT_USER\\Software\\Mozilla\\NativeMessagingHosts\\com.peachpasswords.sync"; return { dir, registryHint: `Add a registry value at ${registryRoot} with the (Default) string set to ${path.join( dir, HOST_FILENAME )}`, }; } throw new Error(`unsupported platform: ${platform}`); } async function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { console.log(usage()); return; } if (!options.browser) { throw new Error("--browser is required"); } const platform = options.platformOverride ?? process.platform; const home = options.homeOverride ?? os.homedir(); const { dir, registryHint } = resolveInstallDir(options.browser, platform, home); if (options.showDir) { process.stdout.write(JSON.stringify({ dir, registryHint: registryHint ?? null })); return; } const targetPath = path.join(dir, HOST_FILENAME); const manifest = await buildNativeManifest(options); const json = `${JSON.stringify(manifest, null, 2)}\n`; if (options.dryRun) { console.log(`Would write ${targetPath}`); process.stdout.write(json); if (registryHint) console.log(`\n${registryHint}`); return; } await mkdir(dir, { recursive: true }); await writeFile(targetPath, json, { mode: 0o644 }); console.log(`Installed ${targetPath}`); if (registryHint) { console.log(registryHint); console.log( "Run `reg add` with the path above, or use the Registry Editor — Windows requires this entry to discover the host." ); } } if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { main().catch((err) => { console.error(err instanceof Error ? err.message : String(err)); console.error(usage()); process.exitCode = 1; }); }