12import { mkdir, readFile, writeFile } from "node:fs/promises";3import path from "node:path";4import process from "node:process";5import { pathToFileURL } from "node:url";67const HOST_NAME = "com.peachpasswords.sync";8const DESCRIPTION = "Peach Sync native messaging host";9const DEFAULT_HOST_PATH = "native/peach-sync-host/target/release/peach-sync-host";1011function usage() {12 return [13 "Usage:",14 " node scripts/generate-native-sync-manifest.mjs --browser chrome --extension-id <chrome-id> [--host-path <path>] [--out <path>]",15 " node scripts/generate-native-sync-manifest.mjs --browser firefox [--extension-id <gecko-id>] [--host-path <path>] [--out <path>]",16 ].join("\n");17}1819function parseArgs(argv) {20 const options = {};21 for (let i = 0; i < argv.length; i += 1) {22 const arg = argv[i];23 if (arg === "--help" || arg === "-h") {24 options.help = true;25 continue;26 }27 if (!arg.startsWith("--")) {28 throw new Error(`unexpected argument: ${arg}`);29 }30 const key = arg.slice(2);31 const value = argv[i + 1];32 if (!value || value.startsWith("--")) {33 throw new Error(`missing value for --${key}`);34 }35 options[toCamel(key)] = value;36 i += 1;37 }38 return options;39}4041function toCamel(value) {42 return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());43}4445async function defaultFirefoxExtensionId(projectRoot) {46 const manifestPath = path.join(projectRoot, "manifest.firefox.json");47 const manifest = JSON.parse(await readFile(manifestPath, "utf8"));48 const id = manifest?.browser_specific_settings?.gecko?.id;49 if (typeof id !== "string" || id.length === 0) {50 throw new Error("manifest.firefox.json is missing browser_specific_settings.gecko.id");51 }52 return id;53}5455async function buildNativeManifest(options, projectRoot = process.cwd()) {56 const browser = options.browser;57 if (browser !== "chrome" && browser !== "firefox") {58 throw new Error("--browser must be chrome or firefox");59 }6061 const hostPath = path.resolve(projectRoot, options.hostPath ?? DEFAULT_HOST_PATH);62 const manifest = {63 name: HOST_NAME,64 description: DESCRIPTION,65 path: hostPath,66 type: "stdio",67 };6869 if (browser === "chrome") {70 const extensionId = options.extensionId ?? process.env.PEACH_CHROME_EXTENSION_ID;71 if (!extensionId) {72 throw new Error("--extension-id or PEACH_CHROME_EXTENSION_ID is required for Chrome");73 }74 return {75 ...manifest,76 allowed_origins: [`chrome-extension://${extensionId}/`],77 };78 }7980 return {81 ...manifest,82 allowed_extensions: [options.extensionId ?? (await defaultFirefoxExtensionId(projectRoot))],83 };84}8586async function main() {87 const options = parseArgs(process.argv.slice(2));88 if (options.help) {89 console.log(usage());90 return;91 }9293 const manifest = await buildNativeManifest(options);94 const json = `${JSON.stringify(manifest, null, 2)}\n`;95 if (options.out) {96 const outPath = path.resolve(process.cwd(), options.out);97 await mkdir(path.dirname(outPath), { recursive: true });98 await writeFile(outPath, json, { mode: 0o644 });99 console.log(outPath);100 return;101 }102 process.stdout.write(json);103}104105if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {106 main().catch((err) => {107 console.error(err instanceof Error ? err.message : String(err));108 console.error(usage());109 process.exitCode = 1;110 });111}112113export { buildNativeManifest, parseArgs };