From 3e42f1de50f4fd3e9f3abd441f250236b2139538 Mon Sep 17 00:00:00 2001 From: Gourav Kumar Dhiman Date: Tue, 20 Oct 2026 20:43:52 +0530 Subject: [PATCH 1/4] fix(desktop): installing the t3 command no longer reports its own launcher as another t3 When ~/.t3/bin was first on PATH, "Install t3 command" refused with "Another t3 at ~/.t3/bin/t3 runs first", because the PATH check only accepted a symlink to the launcher and the launcher itself is a regular file. Treat a first match that resolves to the launcher as ours, for the shadow check and the on-PATH state only. Installed links are still identified by symlink alone, so uninstall never removes the launcher. Co-Authored-By: Claude Sonnet 5.5 --- .../desktop/src/app/DesktopCliCommand.test.ts | 25 +++++++++++++++++++ apps/desktop/src/app/DesktopCliCommand.ts | 16 ++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/app/DesktopCliCommand.test.ts b/apps/desktop/src/app/DesktopCliCommand.test.ts index 7b44fab01855..30b9e59cb054 100644 --- a/apps/desktop/src/app/DesktopCliCommand.test.ts +++ b/apps/desktop/src/app/DesktopCliCommand.test.ts @@ -194,6 +194,31 @@ it.layer(NodeServices.layer)("DesktopCliCommand", (it) => { }).pipe(Effect.scoped), ); + it.effect("does not treat its own launcher as another t3 when its folder is first on PATH", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const home = yield* fs.makeTempDirectoryScoped(); + const launcher = path.join(home, ".t3", "bin", "t3"); + process.env.PATH = [path.dirname(launcher), path.join(home, ".local", "bin")].join(":"); + + const command = yield* commandIn({ home }); + const installed = yield* command.install; + + expect(yield* fs.exists(launcher)).toBe(true); + expect(installed).toMatchObject({ + onPath: true, + installedPath: path.join(home, ".local", "bin", "t3"), + }); + expect(installed.shadowedBy).toBeUndefined(); + expect((yield* command.state).shadowedBy).toBeUndefined(); + + // Reaching the launcher through PATH does not make it a link T3 may remove. + expect((yield* command.uninstall).installedPath).toBeNull(); + expect(yield* fs.exists(launcher)).toBe(true); + }).pipe(Effect.scoped), + ); + it.effect("leaves the Windows PATH alone when it cannot be read", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/desktop/src/app/DesktopCliCommand.ts b/apps/desktop/src/app/DesktopCliCommand.ts index 294f8a8b984e..9a791b52fed1 100644 --- a/apps/desktop/src/app/DesktopCliCommand.ts +++ b/apps/desktop/src/app/DesktopCliCommand.ts @@ -95,6 +95,18 @@ export const make = Effect.gen(function* () { Effect.orElseSucceed(() => false), ); + // The launcher is itself a regular file, so a PATH that lists its folder reaches it with + // no link in between. Only a link we made may be removed, which is why this stays apart + // from `isOurLink`. + const runsOurLauncher = Effect.fn("DesktopCliCommand.runsOurLauncher")( + function* (candidate: string) { + if (yield* isOurLink(candidate)) return true; + const [resolved, ours] = yield* Effect.all([fs.realPath(candidate), fs.realPath(launcher)]); + return resolved === ours; + }, + Effect.orElseSucceed(() => false), + ); + const powershell = (env: Record) => Effect.scoped( Effect.gen(function* () { @@ -135,7 +147,7 @@ export const make = Effect.gen(function* () { const foreignFirstOnPath = Effect.gen(function* () { if (windows) return Option.none(); const first = yield* firstOnPath; - if (Option.isNone(first) || (yield* isOurLink(first.value))) return Option.none(); + if (Option.isNone(first) || (yield* runsOurLauncher(first.value))) return Option.none(); return first; }); @@ -165,7 +177,7 @@ export const make = Effect.gen(function* () { return { supported: true, installedPath: null, onPath: false, ...shadow }; } const first = yield* firstOnPath; - const onPath = windows || (Option.isSome(first) && (yield* isOurLink(first.value))); + const onPath = windows || (Option.isSome(first) && (yield* runsOurLauncher(first.value))); return { supported: true, installedPath: installed.value, onPath, ...shadow }; }).pipe(Effect.orElseSucceed(() => ({ supported: false, installedPath: null, onPath: false }))); From 19a9f0abc5384a66a767fbd6fe972d728db13d8d Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 04:14:12 +0000 Subject: [PATCH 2/4] fix(desktop): tighten launcher regression coverage --- apps/desktop/src/app/DesktopCliCommand.test.ts | 4 ++-- apps/desktop/src/app/DesktopCliCommand.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/app/DesktopCliCommand.test.ts b/apps/desktop/src/app/DesktopCliCommand.test.ts index 58b119541a7a..7440d39af219 100644 --- a/apps/desktop/src/app/DesktopCliCommand.test.ts +++ b/apps/desktop/src/app/DesktopCliCommand.test.ts @@ -194,10 +194,10 @@ it.layer(NodeServices.layer)("DesktopCliCommand", (it) => { installedPath: path.join(home, ".local", "bin", "t3"), }); expect(installed.shadowedBy).toBeUndefined(); - expect((yield* command.state).shadowedBy).toBeUndefined(); + expect(yield* command.state).toEqual(installed); - // Reaching the launcher through PATH does not make it a link T3 may remove. expect((yield* command.uninstall).installedPath).toBeNull(); + expect(yield* fs.exists(installed.installedPath!)).toBe(false); expect(yield* fs.exists(launcher)).toBe(true); }).pipe(Effect.scoped), ); diff --git a/apps/desktop/src/app/DesktopCliCommand.ts b/apps/desktop/src/app/DesktopCliCommand.ts index 80ae3fe8379d..767423a3b77e 100644 --- a/apps/desktop/src/app/DesktopCliCommand.ts +++ b/apps/desktop/src/app/DesktopCliCommand.ts @@ -89,9 +89,7 @@ export const make = Effect.gen(function* () { Effect.orElseSucceed(() => false), ); - // The launcher is itself a regular file, so a PATH that lists its folder reaches it with - // no link in between. Only a link we made may be removed, which is why this stays apart - // from `isOurLink`. + // PATH can reach the regular launcher directly; removal still requires isOurLink. const runsOurLauncher = Effect.fn("DesktopCliCommand.runsOurLauncher")( function* (candidate: string) { if (yield* isOurLink(candidate)) return true; From c821ed2fa1289796d4690a6d3a0db7871e4b2065 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 28 Oct 2026 04:24:41 +0000 Subject: [PATCH 3/4] fix(desktop): report launcher on path after removing its link --- apps/desktop/src/app/DesktopCliCommand.test.ts | 5 ++++- apps/desktop/src/app/DesktopCliCommand.ts | 15 ++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/DesktopCliCommand.test.ts b/apps/desktop/src/app/DesktopCliCommand.test.ts index 9913843c0c3a..d24c77129f29 100644 --- a/apps/desktop/src/app/DesktopCliCommand.test.ts +++ b/apps/desktop/src/app/DesktopCliCommand.test.ts @@ -202,7 +202,10 @@ it.layer(NodeServices.layer)("DesktopCliCommand", (it) => { expect(installed.shadowedBy).toBeUndefined(); expect(yield* command.state).toEqual(installed); - expect((yield* command.uninstall).installedPath).toBeNull(); + const removed = yield* command.uninstall; + expect(removed.installedPath).toBeNull(); + expect(removed.onPath).toBe(true); + expect(yield* command.state).toEqual(removed); expect(yield* fs.exists(installed.installedPath!)).toBe(false); expect(yield* fs.exists(launcher)).toBe(true); }).pipe(Effect.scoped), diff --git a/apps/desktop/src/app/DesktopCliCommand.ts b/apps/desktop/src/app/DesktopCliCommand.ts index 931c79720746..26c20951d525 100644 --- a/apps/desktop/src/app/DesktopCliCommand.ts +++ b/apps/desktop/src/app/DesktopCliCommand.ts @@ -166,12 +166,17 @@ export const make = Effect.gen(function* () { const installed = yield* installedAt; const shadowedBy = yield* foreignFirstOnPath; const shadow = Option.isSome(shadowedBy) ? { shadowedBy: shadowedBy.value } : {}; - if (Option.isNone(installed)) { - return { supported: true, installedPath: null, onPath: false, launcherPath, ...shadow }; - } const first = yield* firstOnPath; - const onPath = windows || (Option.isSome(first) && (yield* runsOurLauncher(first.value))); - return { supported: true, installedPath: installed.value, onPath, launcherPath, ...shadow }; + const onPath = windows + ? Option.isSome(installed) + : Option.isSome(first) && (yield* runsOurLauncher(first.value)); + return { + supported: true, + installedPath: Option.getOrNull(installed), + onPath, + launcherPath, + ...shadow, + }; }).pipe( Effect.orElseSucceed(() => ({ supported: false, From b59c68a613802209e88d4c7349bbefae4184f5bd Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 28 Oct 2026 04:42:45 +0000 Subject: [PATCH 4/4] fix(desktop): verify regular launcher ownership --- apps/desktop/src/app/DesktopCliCommand.test.ts | 8 ++++++++ apps/desktop/src/app/DesktopCliCommand.ts | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/app/DesktopCliCommand.test.ts b/apps/desktop/src/app/DesktopCliCommand.test.ts index d24c77129f29..bea38f54c714 100644 --- a/apps/desktop/src/app/DesktopCliCommand.test.ts +++ b/apps/desktop/src/app/DesktopCliCommand.test.ts @@ -135,6 +135,14 @@ it.layer(NodeServices.layer)("DesktopCliCommand", (it) => { yield* command.uninstall; expect(yield* fs.readFileString(theirs)).toBe("npm's t3\n"); expect(yield* fs.readLink(path.join(home, "bin", "t3"))).toBe(path.join(home, "gone")); + + const launcher = path.join(home, ".t3", "bin", "t3"); + yield* fs.writeFileString(launcher, "npm's t3\n"); + process.env.PATH = path.dirname(launcher); + expect(yield* command.state).toMatchObject({ onPath: false, shadowedBy: launcher }); + expect((yield* Effect.flip(command.install)).message).toContain("Could not set up"); + yield* command.uninstall; + expect(yield* fs.readFileString(launcher)).toBe("npm's t3\n"); }).pipe(Effect.scoped), ); diff --git a/apps/desktop/src/app/DesktopCliCommand.ts b/apps/desktop/src/app/DesktopCliCommand.ts index 26c20951d525..15434568df7c 100644 --- a/apps/desktop/src/app/DesktopCliCommand.ts +++ b/apps/desktop/src/app/DesktopCliCommand.ts @@ -94,7 +94,10 @@ export const make = Effect.gen(function* () { function* (candidate: string) { if (yield* isOurLink(candidate)) return true; const [resolved, ours] = yield* Effect.all([fs.realPath(candidate), fs.realPath(launcher)]); - return resolved === ours; + if (resolved !== ours) return false; + const info = yield* fs.stat(candidate); + if (info.type !== "File" || Number(info.size) > 16_384) return false; + return (yield* fs.readFileString(candidate)).includes(DesktopCliShim.MARKER); }, Effect.orElseSucceed(() => false), );