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>)) 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. 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 # PASEO_DESKTOP_MANAGED marks daemons started through this bundled CLI as
# desktop-managed, so the desktop app restarts them when it upgrades. # 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 PASEO_DESKTOP_MANAGED marks daemons started through this bundled CLI as
rem desktop-managed, so the desktop app restarts them when it upgrades. rem desktop-managed, so the desktop app restarts them when it upgrades.
set "PASEO_DESKTOP_MANAGED=1" 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" %* "%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% exit /b %errorlevel%

View File

@@ -128,6 +128,14 @@ function shellQuoteCliArg(value) {
return shellQuote(String(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) { function getShellCommand(script) {
if (process.platform === "win32") { if (process.platform === "win32") {
return { return {
@@ -740,8 +748,8 @@ async function smokeCliTerminal({ appPath, env }) {
await runCliShimJsonCommand({ await runCliShimJsonCommand({
appPath, appPath,
env, env,
args: ["terminal", "send-keys", terminalId, "echo", "Space", marker, "Enter"], args: ["terminal", "send-keys", terminalId, getTerminalHookSmokeCommand(marker), "Enter"],
label: "Bundled CLI shim terminal send-keys", label: "Bundled CLI shim terminal hook command",
}); });
for (let attempt = 1; attempt <= TERMINAL_CAPTURE_ATTEMPTS; attempt += 1) { 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 : []; const lines = Array.isArray(capture?.lines) ? capture.lines : [];
if (lines.join("\n").includes(marker)) { 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; return;
} }

View File

@@ -3,6 +3,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_DESKTOP_SETTINGS } from "../settings/desktop-settings"; import { DEFAULT_DESKTOP_SETTINGS } from "../settings/desktop-settings";
import { getBundledCliShimPath } from "../integrations/cli-install";
import { createDaemonCommandHandlers } from "./daemon-manager"; import { createDaemonCommandHandlers } from "./daemon-manager";
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
@@ -451,7 +452,10 @@ describe("daemon-manager commands", () => {
expect.objectContaining({ expect.objectContaining({
detached: true, detached: true,
stdio: ["ignore", "ignore", "ignore"], 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 AppUpdateCheckIntent,
type AppReleaseChannel, type AppReleaseChannel,
} from "../features/auto-updater.js"; } 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 { import {
getSkillsStatus, getSkillsStatus,
installSkills, installSkills,
@@ -413,7 +417,11 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
detached: true, detached: true,
envMode: "internal", envMode: "internal",
env: invocation.env, 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"], stdio: ["ignore", "ignore", "ignore"],
}); });

View File

@@ -52,7 +52,7 @@ function createFakeMacBundle(options: { includeHelper: boolean }): {
helperPath, helperPath,
[ [
"#!/bin/sh", "#!/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" "$*"', 'printf "args=%s\\n" "$*"',
"", "",
].join("\n"), ].join("\n"),
@@ -114,7 +114,7 @@ describe("desktop packaging", () => {
const result = spawnSync(bundle.shimPath, ["--version"], { encoding: "utf8" }); const result = spawnSync(bundle.shimPath, ["--version"], { encoding: "utf8" });
expect(result.status).toBe(0); 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-entrypoint-runner.js");
expect(result.stdout).toContain("node-script"); expect(result.stdout).toContain("node-script");
expect(result.stdout).toContain("@getpaseo/cli/dist/index.js"); expect(result.stdout).toContain("@getpaseo/cli/dist/index.js");

View File

@@ -1 +1,2 @@
export { getCliInstallStatus, installCli } from "./install.js"; 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 { export function resolvePaseoCliBinDir(): string | null {
const cliEntrypoint = resolvePaseoCliBinEntrypoint(); const cliExecutable = resolvePaseoCliExecutablePath();
if (!cliEntrypoint) { return cliExecutable ? dirname(cliExecutable) : null;
return null;
}
const externalCliEntrypoint = resolveExternalProcessPath(cliEntrypoint);
return findNpmBinDir(dirname(externalCliEntrypoint)) ?? dirname(externalCliEntrypoint);
} }
export function resolvePaseoCliExecutablePath(): string | null { export function resolvePaseoCliExecutablePath(): string | null {
const configuredCli = process.env.PASEO_CLI?.trim();
if (configuredCli) {
return resolvePath(configuredCli);
}
const cliEntrypoint = resolvePaseoCliBinEntrypoint(); const cliEntrypoint = resolvePaseoCliBinEntrypoint();
if (!cliEntrypoint) { if (!cliEntrypoint) {
return null; return null;