- Add classify.ts as the single source of truth for CLI invocation routing (discriminated union, derives known commands from Commander) - Remove all hardcoded command lists and duplicate path detection logic - Simplify shell wrappers to dumb pipes (zero classification) - Fix hot-start: use `open -n -g -a` (VS Code pattern) so second-instance event fires when app is already running - Fix cold-start race: pull-based IPC (getPendingOpenProject) so renderer fetches the pending path after React mounts, instead of push event that arrived before the listener existed - Fix asar read corruption: unpack node-entrypoint-runner.js from asar (Node.js v24 in Electron 41 can't parse package.json inside asar) - Strip ELECTRON_RUN_AS_NODE from env when spawning desktop app from CLI
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import { spawn } from "node:child_process";
|
|
import { homedir } from "node:os";
|
|
import path from "node:path";
|
|
|
|
function findDesktopApp(): string | null {
|
|
if (process.platform === "darwin") {
|
|
const candidates = [
|
|
"/Applications/Paseo.app",
|
|
path.join(homedir(), "Applications", "Paseo.app"),
|
|
];
|
|
|
|
for (const candidate of candidates) {
|
|
if (existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (process.platform === "linux") {
|
|
const candidates = [
|
|
"/usr/bin/Paseo",
|
|
"/opt/Paseo/Paseo",
|
|
path.join(homedir(), "Applications", "Paseo.AppImage"),
|
|
];
|
|
|
|
for (const candidate of candidates) {
|
|
if (existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (process.platform === "win32") {
|
|
const localAppData = process.env.LOCALAPPDATA;
|
|
if (!localAppData) {
|
|
return null;
|
|
}
|
|
|
|
const candidate = path.join(localAppData, "Programs", "Paseo", "Paseo.exe");
|
|
return existsSync(candidate) ? candidate : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function cleanEnvForDesktopLaunch(): NodeJS.ProcessEnv {
|
|
const env = { ...process.env };
|
|
// The CLI runs via ELECTRON_RUN_AS_NODE=1. On Linux/Windows the spawned
|
|
// desktop process inherits the env directly, so we must strip it or the
|
|
// desktop app would start as a bare Node process instead of Electron.
|
|
delete env.ELECTRON_RUN_AS_NODE;
|
|
return env;
|
|
}
|
|
|
|
function spawnDetached(command: string, args: string[]): void {
|
|
spawn(command, args, {
|
|
detached: true,
|
|
stdio: "ignore",
|
|
env: cleanEnvForDesktopLaunch(),
|
|
}).unref();
|
|
}
|
|
|
|
export async function openDesktopWithProject(projectPath: string): Promise<void> {
|
|
try {
|
|
if (process.env.PASEO_DESKTOP_CLI === "1") {
|
|
throw new Error(
|
|
"Cannot open a desktop project while running in desktop CLI passthrough mode.",
|
|
);
|
|
}
|
|
|
|
const desktopApp = findDesktopApp();
|
|
if (!desktopApp) {
|
|
throw new Error(
|
|
"Paseo desktop app not found. Install it from https://github.com/getpaseo/paseo/releases",
|
|
);
|
|
}
|
|
|
|
if (process.platform === "darwin") {
|
|
// -n forces a new instance even if the app is already running.
|
|
// The new instance hits requestSingleInstanceLock(), fails, and relays
|
|
// the argv to the first instance via the second-instance event.
|
|
// -g keeps the terminal in the foreground (better CLI UX).
|
|
// Without -n, macOS just activates the existing window and drops --args.
|
|
spawnDetached("open", ["-n", "-g", "-a", desktopApp, "--args", projectPath]);
|
|
return;
|
|
}
|
|
|
|
spawnDetached(desktopApp, [projectPath]);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
process.stderr.write(`${message}\n`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|