Skip to content

Commit b034fbd

Browse files
committed
fix(server): npm provider updates are found through the configured registry
npm-global provider installs looked up their latest version only at registry.npmjs.org, so a mirror or company registry set in .npmrc was ignored and update prompts never appeared where the public registry is blocked. Ask npm itself with `npm view`, as pnpm installs already ask pnpm, and keep the registry fetch as the fallback. Fixes #24369
1 parent aaf4a90 commit b034fbd

3 files changed

Lines changed: 99 additions & 1 deletion

File tree

‎docs/user/updating.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ directory and pair with the running server.
116116
**Settings → Providers** shows provider updates for the selected environment.
117117
**Update all** updates every outdated provider on every connected environment
118118
at once. Hover it to see which providers it will update. Providers that only
119-
offer a manual update command are not included.
119+
offer a manual update command are not included. Providers installed with npm or
120+
pnpm are checked against the registry in your npm or pnpm configuration, so a
121+
company registry or mirror works.
120122

121123
After a successful provider CLI update, T3 Code refreshes model discovery for
122124
every enabled instance of that provider using its own configuration. Connected

‎packages/provider-core/src/server/maintenanceResolver.test.ts‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
302302
],
303303
lockKey: `npm-global:${normalizeCommandPath(tempDir)}`,
304304
},
305+
lookupLatestVersion: expect.anything(),
305306
});
306307
}),
307308
);
@@ -712,6 +713,77 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
712713
{ skip: !symlinksSupported || windowsHost },
713714
);
714715

716+
it.effect.each([
717+
{
718+
case: "the configured registry's release",
719+
stdout: '"1.1.0"\n',
720+
exitCode: 0,
721+
expected: "1.1.0",
722+
},
723+
{
724+
case: "the npm fallback when the npm probe fails",
725+
stdout: "",
726+
exitCode: 1,
727+
expected: "9.9.9",
728+
},
729+
])(
730+
"advertises $case for npm-global installs",
731+
(fixture) =>
732+
Effect.gen(function* () {
733+
const tempDir = yield* makeTempDir("t3-npm-latest");
734+
const npmBinDir = NodePath.join(tempDir, "npm-bin");
735+
writeExecutable(NodePath.join(npmBinDir, "npm"));
736+
const link = linkIntoPackage(tempDir, "package-tool", [
737+
"lib",
738+
"node_modules",
739+
"@example",
740+
"package-tool",
741+
]);
742+
const probes: Array<ReadonlyArray<string>> = [];
743+
744+
const capabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(
745+
packageToolUpdate,
746+
{ binaryPath: link, env: { PATH: "" } },
747+
).pipe(
748+
Effect.provideService(HostProcess.Environment, { PATH: npmBinDir }),
749+
Effect.provideService(
750+
ChildProcessSpawner.ChildProcessSpawner,
751+
stdoutSpawner(
752+
(_command, args) => {
753+
probes.push(args);
754+
return fixture.stdout;
755+
},
756+
() => fixture.exitCode,
757+
),
758+
),
759+
);
760+
761+
const provider = yield* enrichProviderSnapshotWithVersionAdvisory(
762+
installedPackageToolProvider,
763+
capabilities,
764+
).pipe(
765+
Effect.provideServiceEffect(
766+
ProviderLatestVersions.ProviderLatestVersions,
767+
ProviderLatestVersions.make([["@example/package-tool", "9.9.9"]]),
768+
),
769+
Effect.provideService(
770+
HttpClient.HttpClient,
771+
HttpClient.make(() => Effect.die("the npm version is already cached")),
772+
),
773+
);
774+
// The update installs into this prefix, so the lookup reads the same global npmrc.
775+
expect(probes).toEqual([
776+
["view", "--prefix", tempDir, "--json", "@example/package-tool@latest", "version"],
777+
]);
778+
expect(provider.versionAdvisory).toMatchObject({
779+
status: "behind_latest",
780+
currentVersion: "1.0.0",
781+
latestVersion: fixture.expected,
782+
});
783+
}),
784+
{ skip: !symlinksSupported || windowsHost },
785+
);
786+
715787
it.effect.skipIf(windowsHost)(
716788
"switches to bun updates when the resolved binary lives in bun's global bin",
717789
() =>

‎packages/provider-core/src/server/maintenanceResolver.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,26 @@ const resolvePnpmGlobalLatestVersion = Effect.fn("resolvePnpmGlobalLatestVersion
424424
return nonEmptyString(listedEntries?.[0]?.dependencies?.[packageName]?.version);
425425
});
426426

427+
const decodeNpmViewVersion = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.String));
428+
429+
// npm view reads the registry, scope and auth settings the update installs with,
430+
// so a mirror or private registry answers where registry.npmjs.org is blocked.
431+
const resolveNpmGlobalLatestVersion = Effect.fn("resolveNpmGlobalLatestVersion")(function* (
432+
packageName: string,
433+
npmPrefix: string,
434+
) {
435+
// Match the updater's npm and server environment, rather than the instance's.
436+
const npmPath = yield* resolveCommandPath("npm");
437+
if (!npmPath) return null;
438+
// The prefix selects the global npmrc, as it does for the update.
439+
const viewed = yield* runProbe(
440+
npmPath,
441+
["view", "--prefix", npmPrefix, "--json", `${packageName}@latest`, "version"],
442+
{},
443+
);
444+
return viewed ? nonEmptyString(Option.getOrNull(decodeNpmViewVersion(viewed.trim()))) : null;
445+
});
446+
427447
export const resolvePackageManagedProviderMaintenance = Effect.fn(
428448
"resolvePackageManagedProviderMaintenance",
429449
)(function* (
@@ -551,6 +571,7 @@ export const resolvePackageManagedProviderMaintenance = Effect.fn(
551571
// (claude copies its native binary over a placeholder stub) is left broken
552572
// while the update reports success. Older npm warns about the unknown
553573
// config and continues.
574+
const services = yield* Effect.context<ProviderMaintenanceResolverServices>();
554575
return makeProviderMaintenanceCapabilities({
555576
provider: definition.provider,
556577
packageName,
@@ -564,6 +585,9 @@ export const resolvePackageManagedProviderMaintenance = Effect.fn(
564585
`${packageName}@latest`,
565586
],
566587
updateLockKey: `npm-global:${normalizeCommandPath(npmPrefix)}`,
588+
lookupLatestVersion: resolveNpmGlobalLatestVersion(packageName, npmPrefix).pipe(
589+
Effect.provideContext(services),
590+
),
567591
});
568592
}
569593

0 commit comments

Comments
 (0)