From d17cabc2f86554e79a2bd8e75a2881f6601ee4a5 Mon Sep 17 00:00:00 2001 From: spoukyii <61633921+spoukyii@users.noreply.github.com> Date: Fri, 6 Nov 2026 01:16:12 -0800 Subject: [PATCH 1/2] test(desktop): a failed t3 launcher install keeps its bin folder clean Occupies the launcher path with a non-empty folder so the launcher install's rename fails, then lists ~/.t3/bin. The staging file the install wrote stays behind, so this fails here. --- apps/desktop/src/app/DesktopCliCommand.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/desktop/src/app/DesktopCliCommand.test.ts b/apps/desktop/src/app/DesktopCliCommand.test.ts index ebb32bda43ce..f6c38b5ecce1 100644 --- a/apps/desktop/src/app/DesktopCliCommand.test.ts +++ b/apps/desktop/src/app/DesktopCliCommand.test.ts @@ -147,6 +147,20 @@ it.layer(NodeServices.layer)("DesktopCliCommand", (it) => { }).pipe(Effect.scoped), ); + it.effect("removes its staging file when the launcher cannot be replaced", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const home = yield* fs.makeTempDirectoryScoped(); + const command = yield* commandIn({ home }); + const launcher = path.join(home, ".t3", "bin", "t3"); + yield* fs.makeDirectory(path.join(launcher, "occupied"), { recursive: true }); + + expect((yield* Effect.flip(command.install)).message).toContain("Could not set up"); + expect(yield* fs.readDirectory(path.dirname(launcher))).toEqual(["t3"]); + }).pipe(Effect.scoped), + ); + it.effect("finds and removes a link left by a previous T3 home", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; From 377913b0629a0d095289b6c617fe5052342a9418 Mon Sep 17 00:00:00 2001 From: spoukyii <61633921+spoukyii@users.noreply.github.com> Date: Fri, 6 Nov 2026 01:16:14 -0800 Subject: [PATCH 2/2] fix(desktop): a failed t3 launcher install no longer leaves temp files in its bin folder The desktop app installs the t3 command by writing ~/.t3/bin/t3 (t3.cmd on Windows) to a ..tmp staging file and renaming it over the launcher. When the write or the rename failed, the staging file stayed. Its name carries the process id, so each launch that retried the install left one more staging file in ~/.t3/bin, which is on the user's PATH on Windows once they install the t3 command. The write and the rename now run under Effect.ensuring, which removes the staging file whether they succeed or fail, as the settings writers do since #29635. After a successful rename there is nothing to remove. Bug: a failed t3 launcher install leaves .tmp staging files in ~/.t3/bin --- apps/desktop/src/app/DesktopCliShim.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/app/DesktopCliShim.ts b/apps/desktop/src/app/DesktopCliShim.ts index 3d4b94abff46..f0f372425df1 100644 --- a/apps/desktop/src/app/DesktopCliShim.ts +++ b/apps/desktop/src/app/DesktopCliShim.ts @@ -148,8 +148,12 @@ export const install = Effect.gen(function* () { if (Option.getOrUndefined(existing) !== content) { yield* fs.makeDirectory(path.dirname(shimPath), { recursive: true }); const staging = `${shimPath}.${process.pid}.tmp`; - yield* fs.writeFileString(staging, content, { mode: 0o755 }); - yield* fs.rename(staging, shimPath); + yield* fs + .writeFileString(staging, content, { mode: 0o755 }) + .pipe( + Effect.andThen(fs.rename(staging, shimPath)), + Effect.ensuring(fs.remove(staging, { force: true }).pipe(Effect.ignore)), + ); yield* logInfo("installed t3 launcher", { shimPath }); } return Option.some(shimPath);