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.
This commit is contained in:
Mohamed Boudra
2026-07-21 17:55:13 +02:00
parent 0707092131
commit ee431bb340
9 changed files with 39 additions and 17 deletions

View File

@@ -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 <event>) else (paseo hooks codex <event>))
```
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.

View File

@@ -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}" "$@"

View File

@@ -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%

View File

@@ -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;
}

View File

@@ -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",
}),
}),
);
});

View File

@@ -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<DesktopDaemonStatus> {
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"],
});

View File

@@ -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");

View File

@@ -1 +1,2 @@
export { getCliInstallStatus, installCli } from "./install.js";
export { getBundledCliShimPath } from "./paths.js";

View File

@@ -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;