From c37fbf8c29b943ee361ea05c92e6f0fb1301bbf3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 1 Nov 2026 02:14:43 +0000 Subject: [PATCH 1/2] test(provider-core): reproduce #28486 Homebrew install behind native launcher These tests reproduce #28486: a Homebrew cask or formula symlinked into ~/.local/bin gets the provider's native updater instead of brew upgrade. Co-Authored-By: Claude Opus 5.5 --- .../src/server/maintenanceResolver.test.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/provider-core/src/server/maintenanceResolver.test.ts b/packages/provider-core/src/server/maintenanceResolver.test.ts index da59b77886a59..9d1105a2aed70 100644 --- a/packages/provider-core/src/server/maintenanceResolver.test.ts +++ b/packages/provider-core/src/server/maintenanceResolver.test.ts @@ -1194,6 +1194,52 @@ it.layer( }), ); + it.effect.each([ + { + kind: "cask", + segments: ["Caskroom", "native-package-tool", "2.1.287", "native-package-tool"], + command: "brew upgrade --cask native-package-tool", + }, + { + kind: "formula", + segments: ["Cellar", "native-package-tool", "2.1.287", "bin", "native-package-tool"], + command: "brew upgrade native-package-tool", + }, + ])( + "upgrades a Homebrew $kind behind a native-looking launcher with brew", + (fixture) => + Effect.gen(function* () { + const tempDir = yield* makeTempDir("t3-homebrew-native-launcher"); + const brewPrefix = NodePath.join(tempDir, "opt", "homebrew"); + writeExecutable(NodePath.join(brewPrefix, "bin", "brew")); + const target = NodePath.join(brewPrefix, ...fixture.segments); + writeExecutable(target); + const nativeBinDir = NodePath.join(tempDir, "home", ".local", "bin"); + NodeFS.mkdirSync(nativeBinDir, { recursive: true }); + NodeFS.symlinkSync(target, NodePath.join(nativeBinDir, "native-package-tool")); + + const capabilities = yield* resolveProviderMaintenanceCapabilitiesEffect( + nativePackageToolUpdate, + { + binaryPath: "native-package-tool", + env: { PATH: [nativeBinDir, NodePath.join(brewPrefix, "bin")].join(":") }, + }, + ).pipe( + Effect.provideService(HostProcess.Platform, "darwin"), + Effect.provideService( + ChildProcessSpawner.ChildProcessSpawner, + stdoutSpawner((_, args) => (args[0] === "--prefix" ? `${brewPrefix}\n` : "")), + ), + ); + + expect(capabilities.update).toMatchObject({ + command: fixture.command, + lockKey: "homebrew", + }); + }), + { skip: !symlinksSupported || windowsHost }, + ); + it("reads the stable formula version from brew info", () => { const info = JSON.stringify({ formulae: [{ versions: { stable: "2.1.5" } }] }); const formula = { kind: "formula", name: "claude-code", prefix: "/opt/homebrew" } as const; From a72577c3115ccad75440fd0c863403ce11512172 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 1 Nov 2026 02:17:36 +0000 Subject: [PATCH 2/2] fix(provider-core): Homebrew installs behind native launchers update with brew A Claude, Codex, or OpenCode launcher at the provider's native path that symlinks into a Homebrew keg or cask was updated with the provider's native updater. Skip the native branch when the real path is Homebrew-owned, so it resolves through brew upgrade, or stays manual-only for a mise keg. Fixes pingdotgg/t3code#28486 Co-Authored-By: Claude Opus 5.5 --- .../src/server/maintenanceResolver.test.ts | 23 +++++++++++-------- .../src/server/maintenanceResolver.ts | 5 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/provider-core/src/server/maintenanceResolver.test.ts b/packages/provider-core/src/server/maintenanceResolver.test.ts index 9d1105a2aed70..30add9d395ce9 100644 --- a/packages/provider-core/src/server/maintenanceResolver.test.ts +++ b/packages/provider-core/src/server/maintenanceResolver.test.ts @@ -926,10 +926,10 @@ it.layer( }), ); - it.effect.each(["wrapper", "symlink"])( + it.effect.each(["wrapper", "symlink", "Homebrew keg"])( "keeps a mise-owned %s in a native install location manual-only", (launcher, { skip }) => { - if (windowsHost || (launcher === "symlink" && !symlinksSupported)) skip(); + if (windowsHost || (launcher !== "wrapper" && !symlinksSupported)) skip(); return Effect.gen(function* () { const tempDir = yield* makeTempDir("t3-native-mise"); const nativePath = NodePath.join(tempDir, ".local", "bin", "native-package-tool"); @@ -940,14 +940,17 @@ it.layer( '#!/bin/sh\nexec mise x native-package-tool -- native-package-tool "$@"\n', ); } else { - const target = NodePath.join( - tempDir, - "mise", - "installs", - "native-package-tool", - "1.0.0", - "native-package-tool", - ); + const target = + launcher === "symlink" + ? NodePath.join( + tempDir, + "mise", + "installs", + "native-package-tool", + "1.0.0", + "native-package-tool", + ) + : NodePath.join(tempDir, "Cellar", "mise", "2026.9.1", "bin", "mise"); writeExecutable(target); NodeFS.mkdirSync(NodePath.dirname(nativePath), { recursive: true }); NodeFS.symlinkSync(target, nativePath); diff --git a/packages/provider-core/src/server/maintenanceResolver.ts b/packages/provider-core/src/server/maintenanceResolver.ts index 61ff6edc0d9ac..52501bbd72ddb 100644 --- a/packages/provider-core/src/server/maintenanceResolver.ts +++ b/packages/provider-core/src/server/maintenanceResolver.ts @@ -463,10 +463,12 @@ export const resolvePackageManagedProviderMaintenance = Effect.fn( : {}), }) : manual; - // A native-looking launcher can still delegate to a mise-owned install. + const homebrew = homebrewOwnershipFromCommandPath(context.realCommandPath); + // A native-looking launcher can still delegate to a mise- or Homebrew-owned install. if ( nativeUpdate?.isCommandPath && commandPaths.some(nativeUpdate.isCommandPath) && + !homebrew && !commandPaths.some(isMiseCommandPath) && !(yield* isMiseWrapperScript(context)) ) { @@ -556,7 +558,6 @@ export const resolvePackageManagedProviderMaintenance = Effect.fn( }); } - const homebrew = homebrewOwnershipFromCommandPath(context.realCommandPath); if ( commandPaths.some(isMiseCommandPath) || // Mise shims resolve to the version manager's keg, not the provider's.