From ee431bb340a01a8e86dcb924137799b7a62fa8b6 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 21 Jul 2026 17:55:13 +0200 Subject: [PATCH] fix(desktop): restore packaged terminal hooks The desktop daemon resolved the CLI through a packaged module entrypoint that is not an executable outside the archive. Publish the existing bundled shim as the daemon's authoritative CLI path so terminal hooks use a callable command. --- docs/terminal-activity.md | 2 +- packages/desktop/bin/paseo | 2 +- packages/desktop/bin/paseo.cmd | 1 + .../desktop/scripts/smoke-packaged-desktop-app.js | 14 +++++++++++--- packages/desktop/src/daemon/daemon-manager.test.ts | 6 +++++- packages/desktop/src/daemon/daemon-manager.ts | 12 ++++++++++-- .../desktop/src/daemon/desktop-packaging.test.ts | 4 ++-- .../desktop/src/integrations/cli-install/index.ts | 1 + packages/server/src/terminal/terminal.ts | 14 +++++++------- 9 files changed, 39 insertions(+), 17 deletions(-) diff --git a/docs/terminal-activity.md b/docs/terminal-activity.md index 28eca1350..0ada655d0 100644 --- a/docs/terminal-activity.md +++ b/docs/terminal-activity.md @@ -107,6 +107,6 @@ Codex also receives the Windows equivalent: if defined PASEO_TERMINAL_ID (if defined PASEO_HOOK_CLI ("%PASEO_HOOK_CLI%" hooks codex ) else (paseo hooks codex )) ``` -Paseo injects `PASEO_HOOK_CLI` so Codex's hook shell cannot pick up a stale global `paseo` before the current one. The command still falls back to bare `paseo` if the env is missing, and it still no-ops outside Paseo terminals because the `PASEO_TERMINAL_ID` gate remains first. Paseo also prepends the CLI binary directory to each terminal `PATH` as a secondary fallback. All other behavior lives in `paseo hooks`: read the env, map the event, POST activity, and no-op/fail-open when anything is missing or unavailable. +The daemon resolves the current CLI through `PASEO_CLI` when its launcher supplies one, or through the npm package shim for standalone installs. Terminal setup exposes that resolved executable to hooks as `PASEO_HOOK_CLI`; desktop and other daemon launchers do not know about the hook-specific variable. The generated command falls back to bare `paseo` if the hook env is missing and no-ops outside Paseo terminals because the `PASEO_TERMINAL_ID` gate remains first. Paseo also prepends the resolved CLI directory to each terminal `PATH` as a secondary fallback. All other behavior lives in `paseo hooks`: read the env, map the event, POST activity, and no-op/fail-open when anything is missing or unavailable. If config installation fails, daemon startup and terminal spawn continue without terminal activity hooks. diff --git a/packages/desktop/bin/paseo b/packages/desktop/bin/paseo index fefe6c60a..05797ae6d 100755 --- a/packages/desktop/bin/paseo +++ b/packages/desktop/bin/paseo @@ -46,4 +46,4 @@ CLI_ENTRYPOINT="${RESOURCES_DIR}/app.asar/node_modules/@getpaseo/cli/dist/index. # PASEO_DESKTOP_MANAGED marks daemons started through this bundled CLI as # desktop-managed, so the desktop app restarts them when it upgrades. -exec env ELECTRON_RUN_AS_NODE=1 PASEO_NODE_ENV=production PASEO_DESKTOP_MANAGED=1 "${APP_EXECUTABLE}" --disable-warning=DEP0040 "${RUNNER_PATH}" node-script "${CLI_ENTRYPOINT}" "$@" +exec env ELECTRON_RUN_AS_NODE=1 PASEO_NODE_ENV=production PASEO_DESKTOP_MANAGED=1 PASEO_CLI="${SCRIPT_PATH}" "${APP_EXECUTABLE}" --disable-warning=DEP0040 "${RUNNER_PATH}" node-script "${CLI_ENTRYPOINT}" "$@" diff --git a/packages/desktop/bin/paseo.cmd b/packages/desktop/bin/paseo.cmd index 862f55e79..1e767523b 100644 --- a/packages/desktop/bin/paseo.cmd +++ b/packages/desktop/bin/paseo.cmd @@ -14,5 +14,6 @@ set "PASEO_NODE_ENV=production" rem PASEO_DESKTOP_MANAGED marks daemons started through this bundled CLI as rem desktop-managed, so the desktop app restarts them when it upgrades. set "PASEO_DESKTOP_MANAGED=1" +set "PASEO_CLI=%~f0" "%APP_EXECUTABLE%" --disable-warning=DEP0040 "%RESOURCES_DIR%\app.asar.unpacked\dist\daemon\node-entrypoint-runner.js" node-script "%RESOURCES_DIR%\app.asar\node_modules\@getpaseo\cli\dist\index.js" %* exit /b %errorlevel% diff --git a/packages/desktop/scripts/smoke-packaged-desktop-app.js b/packages/desktop/scripts/smoke-packaged-desktop-app.js index f86deec7c..f96834d31 100644 --- a/packages/desktop/scripts/smoke-packaged-desktop-app.js +++ b/packages/desktop/scripts/smoke-packaged-desktop-app.js @@ -128,6 +128,14 @@ function shellQuoteCliArg(value) { return shellQuote(String(value)); } +function getTerminalHookSmokeCommand(marker) { + if (process.platform === "win32") { + return `"%PASEO_HOOK_CLI%" hooks codex Stop && echo ${marker}`; + } + + return `"$PASEO_HOOK_CLI" hooks codex Stop && echo ${marker}`; +} + function getShellCommand(script) { if (process.platform === "win32") { return { @@ -740,8 +748,8 @@ async function smokeCliTerminal({ appPath, env }) { await runCliShimJsonCommand({ appPath, env, - args: ["terminal", "send-keys", terminalId, "echo", "Space", marker, "Enter"], - label: "Bundled CLI shim terminal send-keys", + args: ["terminal", "send-keys", terminalId, getTerminalHookSmokeCommand(marker), "Enter"], + label: "Bundled CLI shim terminal hook command", }); for (let attempt = 1; attempt <= TERMINAL_CAPTURE_ATTEMPTS; attempt += 1) { @@ -753,7 +761,7 @@ async function smokeCliTerminal({ appPath, env }) { }); const lines = Array.isArray(capture?.lines) ? capture.lines : []; if (lines.join("\n").includes(marker)) { - console.log("Packaged desktop smoke: terminal command output captured"); + console.log("Packaged desktop smoke: terminal hook command completed"); return; } diff --git a/packages/desktop/src/daemon/daemon-manager.test.ts b/packages/desktop/src/daemon/daemon-manager.test.ts index 805899e94..70a6bc251 100644 --- a/packages/desktop/src/daemon/daemon-manager.test.ts +++ b/packages/desktop/src/daemon/daemon-manager.test.ts @@ -3,6 +3,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_DESKTOP_SETTINGS } from "../settings/desktop-settings"; +import { getBundledCliShimPath } from "../integrations/cli-install"; import { createDaemonCommandHandlers } from "./daemon-manager"; const mocks = vi.hoisted(() => ({ @@ -451,7 +452,10 @@ describe("daemon-manager commands", () => { expect.objectContaining({ detached: true, stdio: ["ignore", "ignore", "ignore"], - envOverlay: expect.objectContaining({ PASEO_WEB_UI_ENABLED: "false" }), + envOverlay: expect.objectContaining({ + PASEO_CLI: getBundledCliShimPath(), + PASEO_WEB_UI_ENABLED: "false", + }), }), ); }); diff --git a/packages/desktop/src/daemon/daemon-manager.ts b/packages/desktop/src/daemon/daemon-manager.ts index 6393a5bb5..6bac55585 100644 --- a/packages/desktop/src/daemon/daemon-manager.ts +++ b/packages/desktop/src/daemon/daemon-manager.ts @@ -18,7 +18,11 @@ import { type AppUpdateCheckIntent, type AppReleaseChannel, } from "../features/auto-updater.js"; -import { getCliInstallStatus, installCli } from "../integrations/cli-install/index.js"; +import { + getBundledCliShimPath, + getCliInstallStatus, + installCli, +} from "../integrations/cli-install/index.js"; import { getSkillsStatus, installSkills, @@ -413,7 +417,11 @@ async function startDaemon(): Promise { detached: true, envMode: "internal", env: invocation.env, - envOverlay: { PASEO_DESKTOP_MANAGED: "1", PASEO_WEB_UI_ENABLED: "false" }, + envOverlay: { + PASEO_DESKTOP_MANAGED: "1", + PASEO_CLI: getBundledCliShimPath(), + PASEO_WEB_UI_ENABLED: "false", + }, stdio: ["ignore", "ignore", "ignore"], }); diff --git a/packages/desktop/src/daemon/desktop-packaging.test.ts b/packages/desktop/src/daemon/desktop-packaging.test.ts index 8676f0087..b0272d93a 100644 --- a/packages/desktop/src/daemon/desktop-packaging.test.ts +++ b/packages/desktop/src/daemon/desktop-packaging.test.ts @@ -52,7 +52,7 @@ function createFakeMacBundle(options: { includeHelper: boolean }): { helperPath, [ "#!/bin/sh", - 'printf "helper env=%s/%s\\n" "$ELECTRON_RUN_AS_NODE" "$PASEO_NODE_ENV"', + 'printf "helper env=%s/%s cli=%s\\n" "$ELECTRON_RUN_AS_NODE" "$PASEO_NODE_ENV" "$PASEO_CLI"', 'printf "args=%s\\n" "$*"', "", ].join("\n"), @@ -114,7 +114,7 @@ describe("desktop packaging", () => { const result = spawnSync(bundle.shimPath, ["--version"], { encoding: "utf8" }); expect(result.status).toBe(0); - expect(result.stdout).toContain("helper env=1/production"); + expect(result.stdout).toContain(`helper env=1/production cli=${bundle.shimPath}`); expect(result.stdout).toContain("node-entrypoint-runner.js"); expect(result.stdout).toContain("node-script"); expect(result.stdout).toContain("@getpaseo/cli/dist/index.js"); diff --git a/packages/desktop/src/integrations/cli-install/index.ts b/packages/desktop/src/integrations/cli-install/index.ts index ad5423bc6..46c5d5f3f 100644 --- a/packages/desktop/src/integrations/cli-install/index.ts +++ b/packages/desktop/src/integrations/cli-install/index.ts @@ -1 +1,2 @@ export { getCliInstallStatus, installCli } from "./install.js"; +export { getBundledCliShimPath } from "./paths.js"; diff --git a/packages/server/src/terminal/terminal.ts b/packages/server/src/terminal/terminal.ts index 663d68d68..4d3a1aa7d 100644 --- a/packages/server/src/terminal/terminal.ts +++ b/packages/server/src/terminal/terminal.ts @@ -308,16 +308,16 @@ function resolveExternalProcessPath(filePath: string): string { } export function resolvePaseoCliBinDir(): string | null { - const cliEntrypoint = resolvePaseoCliBinEntrypoint(); - if (!cliEntrypoint) { - return null; - } - - const externalCliEntrypoint = resolveExternalProcessPath(cliEntrypoint); - return findNpmBinDir(dirname(externalCliEntrypoint)) ?? dirname(externalCliEntrypoint); + const cliExecutable = resolvePaseoCliExecutablePath(); + return cliExecutable ? dirname(cliExecutable) : null; } export function resolvePaseoCliExecutablePath(): string | null { + const configuredCli = process.env.PASEO_CLI?.trim(); + if (configuredCli) { + return resolvePath(configuredCli); + } + const cliEntrypoint = resolvePaseoCliBinEntrypoint(); if (!cliEntrypoint) { return null;