feat(cli): support paseo . to open desktop app with project (#189)
* fix(app): reorder settings sections for better grouping * feat(app): add setup hint and paseo.sh link on mobile welcome screen New app store users land on the welcome screen with no context. Show a brief explanation that the desktop app or server is needed, plus a link to paseo.sh — only on mobile (iOS/Android), hidden on web/desktop. * docs(release): add pre-release sanity check and clarify changelog scope Add a Codex 5.4 review step before cutting releases to catch breaking changes and backward-compatibility issues (mobile apps lag behind desktop/daemon updates). Clarify that the changelog always covers the delta from the previous stable release, not from the last RC. * feat(cli): support `paseo .` to open desktop app with project directory Similar to VS Code's `code .`, users can now type `paseo .` or `paseo <path>` to open the Paseo desktop app with that directory as the active project. - Desktop shims detect path-like first args and launch Electron in GUI mode with --open-project instead of CLI passthrough mode - Electron main process parses --open-project, sends IPC event to renderer, and forwards via second-instance for the already-running case - Renderer OpenProjectListener reuses existing openProject() RPC flow - Standalone CLI discovers the desktop app per platform and spawns it
This commit is contained in:
@@ -62,6 +62,20 @@ describe("node-entrypoint-launcher", () => {
|
||||
).toEqual(["--version"]);
|
||||
});
|
||||
|
||||
it("passes --open-project through as a normal CLI arg", () => {
|
||||
expect(
|
||||
parseCliPassthroughArgsFromArgv({
|
||||
argv: [
|
||||
"/Applications/Paseo.app/Contents/MacOS/Paseo",
|
||||
"--open-project",
|
||||
"/tmp/project",
|
||||
],
|
||||
isDefaultApp: false,
|
||||
forceCli: false,
|
||||
}),
|
||||
).toEqual(["--open-project", "/tmp/project"]);
|
||||
});
|
||||
|
||||
it("forces CLI mode for shim launches even without args", () => {
|
||||
expect(
|
||||
parseCliPassthroughArgsFromArgv({
|
||||
|
||||
@@ -42,9 +42,14 @@ export function parseCliPassthroughArgsFromArgv(
|
||||
input: ParseCliPassthroughArgsFromArgvInput,
|
||||
): string[] | null {
|
||||
const startIndex = input.isDefaultApp ? 2 : 1;
|
||||
const effective = input.argv
|
||||
.slice(startIndex)
|
||||
.filter((arg) => !IGNORED_ARG_PREFIXES.some((p) => arg.startsWith(p)));
|
||||
const effective: string[] = [];
|
||||
|
||||
for (const arg of input.argv.slice(startIndex)) {
|
||||
if (IGNORED_ARG_PREFIXES.some((prefix) => arg.startsWith(prefix))) {
|
||||
continue;
|
||||
}
|
||||
effective.push(arg);
|
||||
}
|
||||
|
||||
if (input.forceCli) {
|
||||
return effective;
|
||||
|
||||
@@ -30,8 +30,33 @@ import { setupApplicationMenu } from "./features/menu.js";
|
||||
|
||||
const DEV_SERVER_URL = process.env.EXPO_DEV_URL ?? "http://localhost:8081";
|
||||
const APP_SCHEME = "paseo";
|
||||
const OPEN_PROJECT_EVENT = "paseo:event:open-project";
|
||||
const OPEN_PROJECT_FLAG = "--open-project";
|
||||
const OPEN_PROJECT_IGNORED_ARG_PREFIXES = ["-psn_", "--no-sandbox"];
|
||||
app.setName("Paseo");
|
||||
|
||||
function parseOpenProjectPath(argv: string[]): string | null {
|
||||
const startIndex = process.defaultApp ? 2 : 1;
|
||||
|
||||
for (let index = startIndex; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (OPEN_PROJECT_IGNORED_ARG_PREFIXES.some((prefix) => arg.startsWith(prefix))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg !== OPEN_PROJECT_FLAG) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathArg = argv[index + 1];
|
||||
return pathArg ? pathArg : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const pendingOpenProjectPath = parseOpenProjectPath(process.argv);
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: APP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
||||
]);
|
||||
@@ -126,6 +151,19 @@ async function createMainWindow(): Promise<void> {
|
||||
await mainWindow.loadURL(`${APP_SCHEME}://app/`);
|
||||
}
|
||||
|
||||
function sendOpenProjectEvent(win: BrowserWindow, projectPath: string): void {
|
||||
const send = () => {
|
||||
win.webContents.send(OPEN_PROJECT_EVENT, { path: projectPath });
|
||||
};
|
||||
|
||||
if (win.webContents.isLoadingMainFrame()) {
|
||||
win.webContents.once("did-finish-load", send);
|
||||
return;
|
||||
}
|
||||
|
||||
send();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// App lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -137,12 +175,16 @@ function setupSingleInstanceLock(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
app.on("second-instance", () => {
|
||||
app.on("second-instance", (_event, commandLine) => {
|
||||
const openProjectPath = parseOpenProjectPath(commandLine);
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
if (win) {
|
||||
win.show();
|
||||
if (win.isMinimized()) win.restore();
|
||||
win.focus();
|
||||
if (openProjectPath) {
|
||||
sendOpenProjectEvent(win, openProjectPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -168,7 +210,7 @@ async function runCliPassthroughIfRequested(): Promise<boolean> {
|
||||
}
|
||||
|
||||
async function bootstrap(): Promise<void> {
|
||||
if (await runCliPassthroughIfRequested()) {
|
||||
if (!pendingOpenProjectPath && (await runCliPassthroughIfRequested())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -214,6 +256,10 @@ async function bootstrap(): Promise<void> {
|
||||
registerNotificationHandlers();
|
||||
registerOpenerHandlers();
|
||||
await createMainWindow();
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||
if (mainWindow && pendingOpenProjectPath) {
|
||||
sendOpenProjectEvent(mainWindow, pendingOpenProjectPath);
|
||||
}
|
||||
|
||||
app.on("activate", async () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user