Fix CLI routing: single classifier, open-project for cold & hot start
- 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
This commit is contained in:
@@ -1,126 +1,45 @@
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const OPEN_PROJECT_FLAG = "--open-project";
|
||||
const OPEN_PROJECT_IGNORED_ARG_PREFIXES = ["-psn_", "--no-sandbox"];
|
||||
const TOP_LEVEL_CLI_COMMANDS = new Set([
|
||||
"ls",
|
||||
"run",
|
||||
"attach",
|
||||
"logs",
|
||||
"stop",
|
||||
"delete",
|
||||
"send",
|
||||
"inspect",
|
||||
"wait",
|
||||
"archive",
|
||||
"onboard",
|
||||
"start",
|
||||
"status",
|
||||
"restart",
|
||||
"agent",
|
||||
"daemon",
|
||||
"chat",
|
||||
"terminal",
|
||||
"loop",
|
||||
"schedule",
|
||||
"permit",
|
||||
"provider",
|
||||
"speech",
|
||||
"worktree",
|
||||
"help",
|
||||
]);
|
||||
|
||||
function expandUserPath(inputPath: string): string {
|
||||
if (inputPath === "~") {
|
||||
return homedir();
|
||||
}
|
||||
|
||||
if (inputPath.startsWith("~/")) {
|
||||
return path.join(homedir(), inputPath.slice(2));
|
||||
}
|
||||
|
||||
return inputPath;
|
||||
}
|
||||
|
||||
function isPathLikeArg(arg: string): boolean {
|
||||
return (
|
||||
arg === "." ||
|
||||
arg === ".." ||
|
||||
arg.startsWith("./") ||
|
||||
arg.startsWith("../") ||
|
||||
arg.startsWith("/") ||
|
||||
arg === "~" ||
|
||||
arg.startsWith("~/") ||
|
||||
/^[A-Za-z]:[\\/]/.test(arg)
|
||||
);
|
||||
}
|
||||
|
||||
function isExistingDirectoryArg(input: { arg: string; cwd: string }): boolean {
|
||||
const absolutePath = path.resolve(input.cwd, expandUserPath(input.arg));
|
||||
|
||||
if (!existsSync(absolutePath)) {
|
||||
function isExistingDirectoryAbsolutePath(candidate: string): boolean {
|
||||
if (!path.isAbsolute(candidate) || !existsSync(candidate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return statSync(absolutePath).isDirectory();
|
||||
}
|
||||
|
||||
function resolveProjectPath(input: { arg: string; cwd: string }): string {
|
||||
return path.resolve(input.cwd, expandUserPath(input.arg));
|
||||
}
|
||||
|
||||
function getEffectiveArgs(input: { argv: string[]; isDefaultApp: boolean }): string[] {
|
||||
const startIndex = input.isDefaultApp ? 2 : 1;
|
||||
const effective: string[] = [];
|
||||
|
||||
for (const arg of input.argv.slice(startIndex)) {
|
||||
if (OPEN_PROJECT_IGNORED_ARG_PREFIXES.some((prefix) => arg.startsWith(prefix))) {
|
||||
continue;
|
||||
}
|
||||
effective.push(arg);
|
||||
try {
|
||||
return statSync(candidate).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return effective;
|
||||
}
|
||||
|
||||
export function parseOpenProjectPathFromArgv(input: {
|
||||
argv: string[];
|
||||
isDefaultApp: boolean;
|
||||
cwd: string;
|
||||
}): string | null {
|
||||
const effectiveArgs = getEffectiveArgs({
|
||||
argv: input.argv,
|
||||
isDefaultApp: input.isDefaultApp,
|
||||
});
|
||||
const effectiveArgs = input.argv
|
||||
.slice(input.isDefaultApp ? 2 : 1)
|
||||
.filter(
|
||||
(arg) => !OPEN_PROJECT_IGNORED_ARG_PREFIXES.some((prefix) => arg.startsWith(prefix)),
|
||||
);
|
||||
|
||||
for (let index = 0; index < effectiveArgs.length; index += 1) {
|
||||
const arg = effectiveArgs[index];
|
||||
if (arg !== OPEN_PROJECT_FLAG) {
|
||||
break;
|
||||
}
|
||||
|
||||
const pathArg = effectiveArgs[index + 1];
|
||||
return pathArg ?? null;
|
||||
const positionalProjectPath = effectiveArgs.find(
|
||||
(arg) => !arg.startsWith("-") && isExistingDirectoryAbsolutePath(arg),
|
||||
);
|
||||
if (positionalProjectPath) {
|
||||
return positionalProjectPath;
|
||||
}
|
||||
|
||||
if (effectiveArgs.length !== 1) {
|
||||
const openProjectIndex = effectiveArgs.indexOf(OPEN_PROJECT_FLAG);
|
||||
if (openProjectIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [arg] = effectiveArgs;
|
||||
if (arg.startsWith("-") || TOP_LEVEL_CLI_COMMANDS.has(arg)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isPathLikeArg(arg) && !isExistingDirectoryArg({ arg, cwd: input.cwd })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isExistingDirectoryArg({ arg, cwd: input.cwd })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolveProjectPath({ arg, cwd: input.cwd });
|
||||
const flaggedProjectPath = effectiveArgs[openProjectIndex + 1];
|
||||
return flaggedProjectPath && isExistingDirectoryAbsolutePath(flaggedProjectPath)
|
||||
? flaggedProjectPath
|
||||
: null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user