diff --git a/packages/provider-core/src/server/maintenanceResolver.test.ts b/packages/provider-core/src/server/maintenanceResolver.test.ts index cc20da24a33ec..fad5e5b80a40a 100644 --- a/packages/provider-core/src/server/maintenanceResolver.test.ts +++ b/packages/provider-core/src/server/maintenanceResolver.test.ts @@ -1084,10 +1084,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"); @@ -1098,14 +1098,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); @@ -1359,6 +1362,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; diff --git a/packages/provider-core/src/server/maintenanceResolver.ts b/packages/provider-core/src/server/maintenanceResolver.ts index 7b1a46e1dbe47..112baf10db2b7 100644 --- a/packages/provider-core/src/server/maintenanceResolver.ts +++ b/packages/provider-core/src/server/maintenanceResolver.ts @@ -521,10 +521,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)) ) { @@ -614,7 +616,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.