From 85c3e954e957343a2a3d6dd449a51083caaa6387 Mon Sep 17 00:00:00 2001 From: Gourav Kumar Dhiman Date: Wed, 21 Oct 2026 10:31:32 +0530 Subject: [PATCH] fix(server): the suggested t3 command names this install, not another t3 on PATH Co-Authored-By: Claude Sonnet 5.5 --- apps/server/src/cli/invocation.test.ts | 8 ++++++++ apps/server/src/cli/invocation.ts | 25 ++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/server/src/cli/invocation.test.ts b/apps/server/src/cli/invocation.test.ts index 1461d1d64b353..672d0226711cd 100644 --- a/apps/server/src/cli/invocation.test.ts +++ b/apps/server/src/cli/invocation.test.ts @@ -73,7 +73,15 @@ it.layer(NodeServices.layer)("root CLI commands", (it) => { expect(yield* rootCommand(desktop)).toBe(`sudo '${shim}' browser setup`); + // An unrelated `t3` on PATH, such as an older npm global, is another install's CLI. expect(yield* rootCommand({ ...desktop, path: yield* pathWithT3 })).toBe( + `sudo '${shim}' browser setup`, + ); + + // The link the app installs onto PATH leads to its own launcher, so plain `t3` is right. + const linked = yield* fs.makeTempDirectoryScoped(); + yield* fs.symlink(shim, path.join(linked, "t3")); + expect(yield* rootCommand({ ...desktop, path: linked })).toBe( 'sudo env "PATH=$PATH" t3 browser setup', ); diff --git a/apps/server/src/cli/invocation.ts b/apps/server/src/cli/invocation.ts index 64a2b099e1394..829ffadf883d2 100644 --- a/apps/server/src/cli/invocation.ts +++ b/apps/server/src/cli/invocation.ts @@ -6,7 +6,7 @@ import * as Schema from "effect/Schema"; import type { ServerInstallation } from "@t3tools/contracts"; import * as HostProcess from "@t3tools/shared/HostProcess"; -import { isCommandAvailable } from "@t3tools/shared/shell"; +import { resolveCommandPath } from "@t3tools/shared/shell"; import packageJson from "../../package.json" with { type: "json" }; @@ -163,9 +163,25 @@ const resolveInstallLauncher = Effect.gen(function* () { : Option.none(); }); +/** + * A `t3` on PATH runs whichever install put it there, which may be an older npm global rather + * than the desktop app that owns this server. It is this install's own only when it resolves to + * the app's launcher; without a launcher to compare to, it is assumed to be. + */ +const pathCommandIsOtherInstall = Effect.fn("invocation.pathCommandIsOtherInstall")(function* ( + pathCommand: string, +) { + const shim = (yield* HostProcess.Environment).T3CODE_CLI_PATH?.trim(); + if (!shim) return false; + const fs = yield* FileSystem.FileSystem; + const real = (file: string) => fs.realPath(file).pipe(Effect.orElseSucceed(() => null)); + const shimReal = yield* real(shim); + return shimReal !== null && shimReal !== (yield* real(pathCommand)); +}); + /** * `t3 ` for a person to run on this host: `t3` when it is - * on PATH, the package runner this process came from, or else the absolute + * on PATH and is this install, the package runner this process came from, or else the absolute * path of the launcher for this install, such as the one the desktop app * installs. `args` are quoted for the host's shell: POSIX, or PowerShell on * Windows. @@ -179,7 +195,10 @@ export const resolveHostCliCommand = Effect.fn("invocation.resolveHostCliCommand [prefix, ...args.map((arg) => hostShellWord(platform, arg))].join(" "); const command = yield* resolveCliCommand(subcommand); if (command !== `t3 ${subcommand}`) return { command: withArgs(command), launcher: false }; - if (yield* isCommandAvailable("t3")) return { command: withArgs(command), launcher: false }; + const onPath = yield* resolveCommandPath("t3"); + if (onPath !== null && !(yield* pathCommandIsOtherInstall(onPath))) { + return { command: withArgs(command), launcher: false }; + } const launcher = yield* resolveInstallLauncher; if (Option.isNone(launcher)) return { command: withArgs(command), launcher: false }; // PowerShell runs a quoted path only through the call operator.