import { existsSync, readFileSync } from "node:fs"; import { spawnSync } from "node:child_process"; import { spawnProcess } from "@getpaseo/server"; import { createRequire } from "node:module"; import path from "node:path"; import { app } from "electron"; import { DESKTOP_CLI_ENV, createNodeEntrypointInvocation as createSharedNodeEntrypointInvocation, parseCliPassthroughArgsFromArgv as parseCliPassthroughArgs, type NodeEntrypointArgvMode, type NodeEntrypointInvocation, type NodeEntrypointSpec, } from "./node-entrypoint-launcher.js"; const CLI_PACKAGE_NAME = "@getpaseo/cli"; const SERVER_PACKAGE_NAME = "@getpaseo/server"; const CLI_BIN_ENTRY = `${CLI_PACKAGE_NAME}/bin/paseo`; type PackageInfo = { root: string; }; const esmRequire = createRequire(__filename); function findPackageRootFromResolvedPath(input: { resolvedPath: string; packageName: string; }): PackageInfo { let currentDir = path.dirname(input.resolvedPath); while (true) { const packageJsonPath = path.join(currentDir, "package.json"); if (existsSync(packageJsonPath)) { try { const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { name?: string; }; if (pkg.name === input.packageName) { return { root: currentDir, }; } } catch { // Ignore malformed package metadata while walking up. } } const parent = path.dirname(currentDir); if (parent === currentDir) { break; } currentDir = parent; } throw new Error(`Unable to resolve ${input.packageName} package root`); } function resolveServerPackageInfo(): PackageInfo { const serverExportPath = esmRequire.resolve(SERVER_PACKAGE_NAME); return findPackageRootFromResolvedPath({ resolvedPath: serverExportPath, packageName: SERVER_PACKAGE_NAME, }); } function resolveCliPackageInfo(): PackageInfo { const cliBinPath = esmRequire.resolve(CLI_BIN_ENTRY); return findPackageRootFromResolvedPath({ resolvedPath: cliBinPath, packageName: CLI_PACKAGE_NAME, }); } function resolvePackagedAsarPath(): string { return path.join(process.resourcesPath, "app.asar"); } function resolvePackagedNodeEntrypointRunnerPath(): string { return path.join(process.resourcesPath, "app.asar.unpacked", "dist", "daemon", "node-entrypoint-runner.js"); } function assertPathExists(input: { label: string; filePath: string }): string { if (!existsSync(input.filePath)) { throw new Error(`${input.label} is missing at ${input.filePath}`); } return input.filePath; } export function parseCliPassthroughArgsFromArgv(argv: string[]): string[] | null { return parseCliPassthroughArgs({ argv, isDefaultApp: process.defaultApp, forceCli: process.env[DESKTOP_CLI_ENV] === "1", }); } export function resolveDaemonRunnerEntrypoint(): NodeEntrypointSpec { if (app.isPackaged) { return { entryPath: assertPathExists({ label: "Bundled daemon runner", filePath: path.join( resolvePackagedAsarPath(), "node_modules", "@getpaseo", "server", "dist", "scripts", "supervisor-entrypoint.js", ), }), execArgv: [], }; } const serverPackage = resolveServerPackageInfo(); const distRunner = path.join( serverPackage.root, "dist", "scripts", "supervisor-entrypoint.js", ); if (existsSync(distRunner)) { return { entryPath: distRunner, execArgv: [], }; } return { entryPath: assertPathExists({ label: "Daemon runner source", filePath: path.join(serverPackage.root, "scripts", "supervisor-entrypoint.ts"), }), execArgv: ["--import", "tsx"], }; } export function resolveCliEntrypoint(): NodeEntrypointSpec { if (app.isPackaged) { return { entryPath: assertPathExists({ label: "Bundled CLI entrypoint", filePath: path.join( resolvePackagedAsarPath(), "node_modules", "@getpaseo", "cli", "dist", "index.js", ), }), execArgv: [], }; } const cliPackage = resolveCliPackageInfo(); const distEntry = path.join(cliPackage.root, "dist", "index.js"); if (existsSync(distEntry)) { return { entryPath: distEntry, execArgv: [], }; } return { entryPath: assertPathExists({ label: "CLI source entrypoint", filePath: path.join(cliPackage.root, "src", "index.ts"), }), execArgv: ["--import", "tsx"], }; } function resolveNodeExecPath(): string { if (app.isPackaged && process.platform === "darwin") { const marker = ".app/Contents/MacOS/"; const markerIndex = process.execPath.indexOf(marker); if (markerIndex !== -1) { const bundleRoot = process.execPath.substring(0, markerIndex + ".app".length); const name = path.basename(process.execPath); const helperPath = path.join( bundleRoot, "Contents", "Frameworks", `${name} Helper.app`, "Contents", "MacOS", `${name} Helper`, ); if (existsSync(helperPath)) { return helperPath; } } } return process.execPath; } export function createNodeEntrypointInvocation(input: { entrypoint: NodeEntrypointSpec; argvMode: NodeEntrypointArgvMode; args: string[]; baseEnv: NodeJS.ProcessEnv; }): NodeEntrypointInvocation { return createSharedNodeEntrypointInvocation({ execPath: resolveNodeExecPath(), isPackaged: app.isPackaged, packagedRunnerPath: app.isPackaged ? assertPathExists({ label: "Bundled node entrypoint runner", filePath: resolvePackagedNodeEntrypointRunnerPath(), }) : null, entrypoint: input.entrypoint, argvMode: input.argvMode, args: input.args, baseEnv: input.baseEnv, }); } function createCliInvocation(args: string[]): NodeEntrypointInvocation { const cli = resolveCliEntrypoint(); return createNodeEntrypointInvocation({ entrypoint: cli, argvMode: "node-script", args, baseEnv: process.env, }); } export function runCliPassthroughCommand(args: string[]): number { const invocation = createCliInvocation(args); const result = spawnSync(invocation.command, invocation.args, { env: invocation.env, stdio: "inherit", windowsHide: true, }); if (result.error) { throw result.error; } if (typeof result.status === "number") { return result.status; } return result.signal ? 1 : 0; } function spawnAsync( command: string, args: string[], options: { env: NodeJS.ProcessEnv }, ): Promise<{ stdout: string; stderr: string; exitCode: number | null }> { return new Promise((resolve, reject) => { const child = spawnProcess(command, args, { env: options.env, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; child.stdout!.on("data", (data: Buffer) => { stdout += data.toString(); }); child.stderr!.on("data", (data: Buffer) => { stderr += data.toString(); }); child.on("error", reject); child.on("close", (exitCode) => { resolve({ stdout, stderr, exitCode }); }); }); } export async function runCliTextCommand(args: string[]): Promise { const invocation = createCliInvocation(args); const result = await spawnAsync(invocation.command, invocation.args, { env: invocation.env, }); if (result.exitCode !== 0) { const stderr = result.stderr.trim(); throw new Error(stderr.length > 0 ? stderr : `CLI command failed with exit code ${result.exitCode}`); } return result.stdout.trimEnd(); } export async function runCliJsonCommand(args: string[]): Promise { const invocation = createCliInvocation(args); const result = await spawnAsync(invocation.command, invocation.args, { env: invocation.env, }); if (result.exitCode !== 0) { const stderr = result.stderr.trim(); throw new Error(stderr.length > 0 ? stderr : `CLI command failed with exit code ${result.exitCode}`); } const stdout = result.stdout.trim(); if (stdout.length === 0) { throw new Error("CLI command did not produce JSON output."); } // The stdout may contain non-JSON preamble (e.g. Node deprecation warnings). // Extract the first valid JSON object or array from the output. const jsonStart = stdout.search(/[{[]/); if (jsonStart < 0) { throw new Error("CLI command output contained no JSON."); } const jsonText = stdout.slice(jsonStart); try { return JSON.parse(jsonText) as unknown; } catch (error) { throw new Error( `CLI command returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`, ); } }