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:
@@ -27,6 +27,7 @@ import {
|
|||||||
import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
||||||
import { loadSettingsFromStorage } from "@/hooks/use-settings";
|
import { loadSettingsFromStorage } from "@/hooks/use-settings";
|
||||||
import { useColorScheme } from "@/hooks/use-color-scheme";
|
import { useColorScheme } from "@/hooks/use-color-scheme";
|
||||||
|
import { useOpenProject } from "@/hooks/use-open-project";
|
||||||
import { SessionProvider } from "@/contexts/session-context";
|
import { SessionProvider } from "@/contexts/session-context";
|
||||||
import type { HostProfile } from "@/types/host-connection";
|
import type { HostProfile } from "@/types/host-connection";
|
||||||
import {
|
import {
|
||||||
@@ -67,6 +68,7 @@ import {
|
|||||||
type WebNotificationClickDetail,
|
type WebNotificationClickDetail,
|
||||||
ensureOsNotificationPermission,
|
ensureOsNotificationPermission,
|
||||||
} from "@/utils/os-notifications";
|
} from "@/utils/os-notifications";
|
||||||
|
import { listenToDesktopEvent } from "@/desktop/electron/events";
|
||||||
import { getDesktopHost } from "@/desktop/host";
|
import { getDesktopHost } from "@/desktop/host";
|
||||||
import { updateDesktopWindowControls } from "@/desktop/electron/window";
|
import { updateDesktopWindowControls } from "@/desktop/electron/window";
|
||||||
import { buildNotificationRoute } from "@/utils/notification-routing";
|
import { buildNotificationRoute } from "@/utils/notification-routing";
|
||||||
@@ -548,6 +550,7 @@ function ProvidersWrapper({ children }: { children: ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<VoiceProvider>
|
<VoiceProvider>
|
||||||
<OfferLinkListener upsertDaemonFromOfferUrl={upsertConnectionFromOfferUrl} />
|
<OfferLinkListener upsertDaemonFromOfferUrl={upsertConnectionFromOfferUrl} />
|
||||||
|
<OpenProjectListener />
|
||||||
<HostSessionManager />
|
<HostSessionManager />
|
||||||
<FaviconStatusSync />
|
<FaviconStatusSync />
|
||||||
{children}
|
{children}
|
||||||
@@ -597,6 +600,68 @@ function OfferLinkListener({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OpenProjectEventPayload {
|
||||||
|
path?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OpenProjectListener() {
|
||||||
|
const hosts = useHosts();
|
||||||
|
const serverId = hosts[0]?.serverId ?? null;
|
||||||
|
const client = useHostRuntimeClient(serverId ?? "");
|
||||||
|
const openProject = useOpenProject(serverId);
|
||||||
|
const pendingPathRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let disposed = false;
|
||||||
|
let unlisten: (() => void) | null = null;
|
||||||
|
const maybeOpenProject = (inputPath: string) => {
|
||||||
|
const nextPath = inputPath.trim();
|
||||||
|
if (!nextPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPathRef.current = nextPath;
|
||||||
|
|
||||||
|
if (!serverId || !client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathToOpen = pendingPathRef.current;
|
||||||
|
pendingPathRef.current = null;
|
||||||
|
if (!pathToOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void openProject(pathToOpen).catch(() => undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
void listenToDesktopEvent<OpenProjectEventPayload>("open-project", (payload) => {
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
||||||
|
maybeOpenProject(nextPath);
|
||||||
|
})
|
||||||
|
.then((dispose) => {
|
||||||
|
if (disposed) {
|
||||||
|
dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unlisten = dispose;
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
|
||||||
|
maybeOpenProject(pendingPathRef.current ?? "");
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
unlisten?.();
|
||||||
|
};
|
||||||
|
}, [client, openProject, serverId]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function AppWithSidebar({ children }: { children: ReactNode }) {
|
function AppWithSidebar({ children }: { children: ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { existsSync, statSync } from "node:fs";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const DESKTOP_GUI_FLAG = "--open-project";
|
||||||
|
|
||||||
|
export function isPathLikeArg(arg: string): boolean {
|
||||||
|
return (
|
||||||
|
arg === "." ||
|
||||||
|
arg === ".." ||
|
||||||
|
arg.startsWith("./") ||
|
||||||
|
arg.startsWith("../") ||
|
||||||
|
arg.startsWith("/") ||
|
||||||
|
arg === "~" ||
|
||||||
|
arg.startsWith("~/") ||
|
||||||
|
/^[A-Za-z]:[\\/]/.test(arg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandUserPath(inputPath: string): string {
|
||||||
|
if (inputPath === "~") {
|
||||||
|
return homedir();
|
||||||
|
}
|
||||||
|
if (inputPath.startsWith("~/")) {
|
||||||
|
return path.join(homedir(), inputPath.slice(2));
|
||||||
|
}
|
||||||
|
return inputPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProjectDirectory(inputPath: string): string {
|
||||||
|
const absolutePath = path.resolve(expandUserPath(inputPath));
|
||||||
|
|
||||||
|
if (!existsSync(absolutePath)) {
|
||||||
|
throw new Error(`Path does not exist: ${absolutePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = statSync(absolutePath);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
throw new Error(`Not a directory: ${absolutePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return absolutePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 spawnDetached(command: string, args: string[]): void {
|
||||||
|
spawn(command, args, {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
}).unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openDesktopWithProject(pathArg: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const projectPath = resolveProjectDirectory(pathArg);
|
||||||
|
|
||||||
|
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") {
|
||||||
|
spawnDetached("open", ["-a", desktopApp, "--args", DESKTOP_GUI_FLAG, projectPath]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnDetached(desktopApp, [DESKTOP_GUI_FLAG, projectPath]);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
process.stderr.write(`${message}\n`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
import { createCli } from "./cli.js";
|
import { createCli } from "./cli.js";
|
||||||
|
import { isPathLikeArg, openDesktopWithProject } from "./commands/open.js";
|
||||||
|
|
||||||
const program = createCli();
|
const program = createCli();
|
||||||
if (process.argv.length <= 2) {
|
|
||||||
process.argv.push("onboard");
|
const firstArg = process.argv[2];
|
||||||
|
if (firstArg && isPathLikeArg(firstArg)) {
|
||||||
|
await openDesktopWithProject(firstArg);
|
||||||
|
} else {
|
||||||
|
if (process.argv.length <= 2) {
|
||||||
|
process.argv.push("onboard");
|
||||||
|
}
|
||||||
|
program.parse(process.argv, { from: "node" });
|
||||||
}
|
}
|
||||||
program.parse(process.argv, { from: "node" });
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env npx zx
|
||||||
|
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { isPathLikeArg, openDesktopWithProject } from "../src/commands/open.ts";
|
||||||
|
|
||||||
|
console.log("📋 Phase 32: Open Project CLI Tests\n");
|
||||||
|
|
||||||
|
console.log(" Testing path-like detection exports...");
|
||||||
|
assert.equal(isPathLikeArg("."), true);
|
||||||
|
assert.equal(isPathLikeArg("./app"), true);
|
||||||
|
assert.equal(isPathLikeArg("/tmp/app"), true);
|
||||||
|
assert.equal(isPathLikeArg("~/app"), true);
|
||||||
|
assert.equal(isPathLikeArg("run"), false);
|
||||||
|
assert.equal(isPathLikeArg("foo"), false);
|
||||||
|
console.log(" ✅ path-like detection matches the expected prefixes");
|
||||||
|
|
||||||
|
console.log(" Testing nonexistent project path errors...");
|
||||||
|
const missingProject = join(tmpdir(), "paseo-open-project-missing");
|
||||||
|
const originalWrite = process.stderr.write.bind(process.stderr);
|
||||||
|
const stderrChunks: string[] = [];
|
||||||
|
process.stderr.write = ((chunk: string | Uint8Array) => {
|
||||||
|
stderrChunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
||||||
|
return true;
|
||||||
|
}) as typeof process.stderr.write;
|
||||||
|
|
||||||
|
const previousExitCode = process.exitCode;
|
||||||
|
process.exitCode = undefined;
|
||||||
|
|
||||||
|
await openDesktopWithProject(missingProject);
|
||||||
|
|
||||||
|
process.stderr.write = originalWrite;
|
||||||
|
assert.equal(process.exitCode, 1);
|
||||||
|
assert.match(stderrChunks.join(""), /Path does not exist:/);
|
||||||
|
process.exitCode = previousExitCode;
|
||||||
|
console.log(" ✅ nonexistent paths fail with a helpful error");
|
||||||
|
|
||||||
|
console.log("\n✅ Phase 32: Open Project CLI Tests PASSED");
|
||||||
@@ -31,4 +31,30 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
./*|../*|.|..|/*|~|~/*)
|
||||||
|
case "$1" in
|
||||||
|
~) ARG_PATH="$HOME" ;;
|
||||||
|
~/*) ARG_PATH="$HOME/${1#\~/}" ;;
|
||||||
|
*) ARG_PATH="$1" ;;
|
||||||
|
esac
|
||||||
|
if RESOLVED_PATH=$(CDPATH= cd -- "$ARG_PATH" 2>/dev/null && pwd); then
|
||||||
|
:
|
||||||
|
elif command -v realpath >/dev/null 2>&1; then
|
||||||
|
RESOLVED_PATH=$(realpath "$ARG_PATH" 2>/dev/null || printf "%s" "$ARG_PATH")
|
||||||
|
else
|
||||||
|
RESOLVED_PATH=$ARG_PATH
|
||||||
|
fi
|
||||||
|
if [ ! -e "${RESOLVED_PATH}" ]; then
|
||||||
|
echo "Path does not exist: ${RESOLVED_PATH}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -d "${RESOLVED_PATH}" ]; then
|
||||||
|
echo "Not a directory: ${RESOLVED_PATH}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exec "${APP_EXECUTABLE}" --open-project "${RESOLVED_PATH}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
exec env PASEO_DESKTOP_CLI=1 "${APP_EXECUTABLE}" "$@"
|
exec env PASEO_DESKTOP_CLI=1 "${APP_EXECUTABLE}" "$@"
|
||||||
|
|||||||
@@ -9,6 +9,33 @@ if not exist "%APP_EXECUTABLE%" (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set "FIRST_ARG=%~1"
|
||||||
|
if "%FIRST_ARG%"=="." goto :open_project
|
||||||
|
if "%FIRST_ARG%"==".." goto :open_project
|
||||||
|
if "%FIRST_ARG:~0,2%"==".\" goto :open_project
|
||||||
|
if "%FIRST_ARG:~0,2%"=="./" goto :open_project
|
||||||
|
if "%FIRST_ARG:~0,3%"=="..\" goto :open_project
|
||||||
|
if "%FIRST_ARG:~0,3%"=="../" goto :open_project
|
||||||
|
if "%FIRST_ARG:~0,1%"=="\" goto :open_project
|
||||||
|
if "%FIRST_ARG:~0,1%"=="/" goto :open_project
|
||||||
|
if "%FIRST_ARG:~1,2%"==":\" goto :open_project
|
||||||
|
if "%FIRST_ARG:~1,2%"==":/" goto :open_project
|
||||||
|
goto :cli_mode
|
||||||
|
|
||||||
|
:open_project
|
||||||
|
for %%I in ("%~1") do set "RESOLVED_PATH=%%~fI"
|
||||||
|
if not exist "%RESOLVED_PATH%" (
|
||||||
|
echo Path does not exist: %RESOLVED_PATH% 1>&2
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
if not exist "%RESOLVED_PATH%\NUL" (
|
||||||
|
echo Not a directory: %RESOLVED_PATH% 1>&2
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
"%APP_EXECUTABLE%" --open-project "%RESOLVED_PATH%"
|
||||||
|
exit /b %errorlevel%
|
||||||
|
|
||||||
|
:cli_mode
|
||||||
set "ELECTRON_RUN_AS_NODE=1"
|
set "ELECTRON_RUN_AS_NODE=1"
|
||||||
"%APP_EXECUTABLE%" "%RESOURCES_DIR%\app.asar\dist\daemon\node-entrypoint-runner.js" bare "%RESOURCES_DIR%\app.asar\node_modules\@getpaseo\cli\dist\index.js" %*
|
"%APP_EXECUTABLE%" "%RESOURCES_DIR%\app.asar\dist\daemon\node-entrypoint-runner.js" bare "%RESOURCES_DIR%\app.asar\node_modules\@getpaseo\cli\dist\index.js" %*
|
||||||
exit /b %errorlevel%
|
exit /b %errorlevel%
|
||||||
|
|||||||
@@ -62,6 +62,20 @@ describe("node-entrypoint-launcher", () => {
|
|||||||
).toEqual(["--version"]);
|
).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", () => {
|
it("forces CLI mode for shim launches even without args", () => {
|
||||||
expect(
|
expect(
|
||||||
parseCliPassthroughArgsFromArgv({
|
parseCliPassthroughArgsFromArgv({
|
||||||
|
|||||||
@@ -42,9 +42,14 @@ export function parseCliPassthroughArgsFromArgv(
|
|||||||
input: ParseCliPassthroughArgsFromArgvInput,
|
input: ParseCliPassthroughArgsFromArgvInput,
|
||||||
): string[] | null {
|
): string[] | null {
|
||||||
const startIndex = input.isDefaultApp ? 2 : 1;
|
const startIndex = input.isDefaultApp ? 2 : 1;
|
||||||
const effective = input.argv
|
const effective: string[] = [];
|
||||||
.slice(startIndex)
|
|
||||||
.filter((arg) => !IGNORED_ARG_PREFIXES.some((p) => arg.startsWith(p)));
|
for (const arg of input.argv.slice(startIndex)) {
|
||||||
|
if (IGNORED_ARG_PREFIXES.some((prefix) => arg.startsWith(prefix))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
effective.push(arg);
|
||||||
|
}
|
||||||
|
|
||||||
if (input.forceCli) {
|
if (input.forceCli) {
|
||||||
return effective;
|
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 DEV_SERVER_URL = process.env.EXPO_DEV_URL ?? "http://localhost:8081";
|
||||||
const APP_SCHEME = "paseo";
|
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");
|
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([
|
protocol.registerSchemesAsPrivileged([
|
||||||
{ scheme: APP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
{ 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/`);
|
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
|
// App lifecycle
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -137,12 +175,16 @@ function setupSingleInstanceLock(): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.on("second-instance", () => {
|
app.on("second-instance", (_event, commandLine) => {
|
||||||
|
const openProjectPath = parseOpenProjectPath(commandLine);
|
||||||
const win = BrowserWindow.getAllWindows()[0];
|
const win = BrowserWindow.getAllWindows()[0];
|
||||||
if (win) {
|
if (win) {
|
||||||
win.show();
|
win.show();
|
||||||
if (win.isMinimized()) win.restore();
|
if (win.isMinimized()) win.restore();
|
||||||
win.focus();
|
win.focus();
|
||||||
|
if (openProjectPath) {
|
||||||
|
sendOpenProjectEvent(win, openProjectPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,7 +210,7 @@ async function runCliPassthroughIfRequested(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrap(): Promise<void> {
|
async function bootstrap(): Promise<void> {
|
||||||
if (await runCliPassthroughIfRequested()) {
|
if (!pendingOpenProjectPath && (await runCliPassthroughIfRequested())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +256,10 @@ async function bootstrap(): Promise<void> {
|
|||||||
registerNotificationHandlers();
|
registerNotificationHandlers();
|
||||||
registerOpenerHandlers();
|
registerOpenerHandlers();
|
||||||
await createMainWindow();
|
await createMainWindow();
|
||||||
|
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||||
|
if (mainWindow && pendingOpenProjectPath) {
|
||||||
|
sendOpenProjectEvent(mainWindow, pendingOpenProjectPath);
|
||||||
|
}
|
||||||
|
|
||||||
app.on("activate", async () => {
|
app.on("activate", async () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user