diff --git a/apps/server/src/cli/invocation.test.ts b/apps/server/src/cli/invocation.test.ts index f23b96063a576..cd00f6ba1e408 100644 --- a/apps/server/src/cli/invocation.test.ts +++ b/apps/server/src/cli/invocation.test.ts @@ -71,7 +71,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 5ee3c58a68688..dbff3add054f7 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" }; @@ -143,7 +143,7 @@ const resolveInstallLauncher = Effect.gen(function* () { }); /** - * Uses `t3` on PATH, the originating package runner, or this install's absolute launcher path. + * Uses this install's `t3` on PATH, the originating runner, or its absolute launcher path. * Arguments are quoted for POSIX shells or PowerShell on Windows. */ export const resolveHostCliCommand = Effect.fn("invocation.resolveHostCliCommand")(function* ( @@ -155,7 +155,16 @@ 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) { + const shim = (yield* HostProcess.Environment).T3CODE_CLI_PATH?.trim(); + const fs = yield* FileSystem.FileSystem; + const real = (file: string) => fs.realPath(file).pipe(Effect.orElseSucceed(() => null)); + const shimReal = shim ? yield* real(shim) : null; + if (shimReal === null || shimReal === (yield* real(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.