#!/usr/bin/env node import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import process from "node:process"; import { pathToFileURL } from "node:url"; const HOST_NAME = "com.peachpasswords.sync"; const DESCRIPTION = "Peach Sync native messaging host"; const DEFAULT_HOST_PATH = "native/peach-sync-host/target/release/peach-sync-host"; function usage() { return [ "Usage:", " node scripts/generate-native-sync-manifest.mjs --browser chrome --extension-id [--host-path ] [--out ]", " node scripts/generate-native-sync-manifest.mjs --browser firefox [--extension-id ] [--host-path ] [--out ]", ].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.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()); } async function defaultFirefoxExtensionId(projectRoot) { const manifestPath = path.join(projectRoot, "manifest.firefox.json"); const manifest = JSON.parse(await readFile(manifestPath, "utf8")); const id = manifest?.browser_specific_settings?.gecko?.id; if (typeof id !== "string" || id.length === 0) { throw new Error("manifest.firefox.json is missing browser_specific_settings.gecko.id"); } return id; } async function buildNativeManifest(options, projectRoot = process.cwd()) { const browser = options.browser; if (browser !== "chrome" && browser !== "firefox") { throw new Error("--browser must be chrome or firefox"); } const hostPath = path.resolve(projectRoot, options.hostPath ?? DEFAULT_HOST_PATH); const manifest = { name: HOST_NAME, description: DESCRIPTION, path: hostPath, type: "stdio", }; if (browser === "chrome") { const extensionId = options.extensionId ?? process.env.PEACH_CHROME_EXTENSION_ID; if (!extensionId) { throw new Error("--extension-id or PEACH_CHROME_EXTENSION_ID is required for Chrome"); } return { ...manifest, allowed_origins: [`chrome-extension://${extensionId}/`], }; } return { ...manifest, allowed_extensions: [options.extensionId ?? (await defaultFirefoxExtensionId(projectRoot))], }; } async function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { console.log(usage()); return; } const manifest = await buildNativeManifest(options); const json = `${JSON.stringify(manifest, null, 2)}\n`; if (options.out) { const outPath = path.resolve(process.cwd(), options.out); await mkdir(path.dirname(outPath), { recursive: true }); await writeFile(outPath, json, { mode: 0o644 }); console.log(outPath); return; } process.stdout.write(json); } 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; }); } export { buildNativeManifest, parseArgs };