508 lines · 16.4 KB · TypeScript
RawDownload
import {  ProviderDriverKind,  type ServerProvider,  type ServerProviderVersionAdvisory,} from "@t3tools/contracts";import { compareSemverVersions } from "@t3tools/shared/semver";import { resolveCommandPath } from "@t3tools/shared/shell";import * as Config from "effect/Config";import * as Context from "effect/Context";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Option from "effect/Option";import * as Schema from "effect/Schema";import { HttpClient, HttpClientRequest } from "effect/unstable/http"; const LATEST_VERSION_CACHE_TTL_MS = 60 * 60 * 1_000;const LATEST_VERSION_TIMEOUT_MS = 4_000;const PROVIDER_UPDATE_ACTION_TOAST_MESSAGE = "Install the update now or review provider settings."; const compactEnv = (input: Record<string, Option.Option<string>>): NodeJS.ProcessEnv =>  Object.fromEntries(    Object.entries(input).flatMap(([key, value]) =>      Option.match(value, {        onNone: () => [],        onSome: (resolved) => [[key, resolved]],      }),    ),  ); const CommandLookupEnvConfig = Config.all({  PATH: Config.string("PATH").pipe(Config.option),  Path: Config.string("Path").pipe(Config.option),  path: Config.string("path").pipe(Config.option),  PATHEXT: Config.string("PATHEXT").pipe(Config.option),}).pipe(Config.map(compactEnv)); const readCommandLookupEnv = CommandLookupEnvConfig.pipe(Effect.orElseSucceed(() => ({}))); export interface ProviderMaintenanceCapabilities {  readonly provider: ProviderDriverKind;  readonly packageName: string | null;  readonly update: ProviderMaintenanceCommandAction | null;} export interface ProviderMaintenanceCommandAction {  readonly command: string;  readonly executable: string;  readonly args: ReadonlyArray<string>;  readonly lockKey: string;} export interface ProviderMaintenanceCapabilityResolutionOptions {  readonly binaryPath?: string | null;  readonly env?: NodeJS.ProcessEnv;  readonly resolvedCommandPath?: string | null;  readonly realCommandPath?: string | null;} export interface ProviderMaintenanceCapabilitiesResolver {  readonly resolve: (    options?: ProviderMaintenanceCapabilityResolutionOptions,  ) => ProviderMaintenanceCapabilities;} export interface PackageManagedProviderMaintenanceDefinition {  readonly provider: ProviderDriverKind;  readonly npmPackageName: string;  readonly homebrewFormula: string | null;  readonly nativeUpdate: {    readonly executable: string;    readonly args: ReadonlyArray<string>;    readonly lockKey: string;    readonly isCommandPath: (commandPath: string) => boolean;  } | null;} export interface ProviderVersionCacheEntry {  readonly expiresAt: number;  readonly version: string | null;} export const ProviderVersionCache = Context.Reference<Map<string, ProviderVersionCacheEntry>>(  "@t3tools/server/providerMaintenance/ProviderVersionCache",  {    defaultValue: () => new Map(),  },);const NpmLatestVersionResponse = Schema.Struct({  version: Schema.optional(Schema.String),}); function nonEmptyString(value: unknown): string | null {  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;} export function makeProviderMaintenanceCapabilities(input: {  readonly provider: ProviderDriverKind;  readonly packageName: string | null;  readonly updateExecutable: string | null;  readonly updateArgs: ReadonlyArray<string>;  readonly updateLockKey: string | null;}): ProviderMaintenanceCapabilities {  const update =    input.updateExecutable === null || input.updateLockKey === null      ? null      : {          command: [input.updateExecutable, ...input.updateArgs].join(" "),          executable: input.updateExecutable,          args: input.updateArgs,          lockKey: input.updateLockKey,        };  return {    provider: input.provider,    packageName: input.packageName,    update,  };} export function makeManualOnlyProviderMaintenanceCapabilities(input: {  readonly provider: ProviderDriverKind;  readonly packageName: string | null;}): ProviderMaintenanceCapabilities {  return makeProviderMaintenanceCapabilities({    provider: input.provider,    packageName: input.packageName,    updateExecutable: null,    updateArgs: [],    updateLockKey: null,  });} function makeNpmGlobalProviderMaintenanceCapabilities(  definition: PackageManagedProviderMaintenanceDefinition,): ProviderMaintenanceCapabilities {  return makeProviderMaintenanceCapabilities({    provider: definition.provider,    packageName: definition.npmPackageName,    updateExecutable: "npm",    updateArgs: ["install", "-g", `${definition.npmPackageName}@latest`],    updateLockKey: "npm-global",  });} function makeBunGlobalProviderMaintenanceCapabilities(  definition: PackageManagedProviderMaintenanceDefinition,): ProviderMaintenanceCapabilities {  return makeProviderMaintenanceCapabilities({    provider: definition.provider,    packageName: definition.npmPackageName,    updateExecutable: "bun",    updateArgs: ["i", "-g", `${definition.npmPackageName}@latest`],    updateLockKey: "bun-global",  });} function makePnpmGlobalProviderMaintenanceCapabilities(  definition: PackageManagedProviderMaintenanceDefinition,): ProviderMaintenanceCapabilities {  return makeProviderMaintenanceCapabilities({    provider: definition.provider,    packageName: definition.npmPackageName,    updateExecutable: "pnpm",    updateArgs: ["add", "-g", `${definition.npmPackageName}@latest`],    updateLockKey: "pnpm-global",  });} function makeVitePlusGlobalProviderMaintenanceCapabilities(  definition: PackageManagedProviderMaintenanceDefinition,): ProviderMaintenanceCapabilities {  return makeProviderMaintenanceCapabilities({    provider: definition.provider,    packageName: definition.npmPackageName,    updateExecutable: "vp",    updateArgs: ["i", "-g", definition.npmPackageName],    updateLockKey: "vite-plus-global",  });} function makeHomebrewProviderMaintenanceCapabilities(  definition: PackageManagedProviderMaintenanceDefinition,): ProviderMaintenanceCapabilities {  if (!definition.homebrewFormula) {    return makeManualOnlyProviderMaintenanceCapabilities({      provider: definition.provider,      packageName: definition.npmPackageName,    });  }   return makeProviderMaintenanceCapabilities({    provider: definition.provider,    packageName: definition.npmPackageName,    updateExecutable: "brew",    updateArgs: ["upgrade", definition.homebrewFormula],    updateLockKey: "homebrew",  });} function makeNativeProviderMaintenanceCapabilities(  definition: PackageManagedProviderMaintenanceDefinition,): ProviderMaintenanceCapabilities | null {  if (!definition.nativeUpdate) {    return null;  }   return makeProviderMaintenanceCapabilities({    provider: definition.provider,    packageName: definition.npmPackageName,    updateExecutable: definition.nativeUpdate.executable,    updateArgs: definition.nativeUpdate.args,    updateLockKey: definition.nativeUpdate.lockKey,  });} export function hasPathSeparator(value: string): boolean {  return value.includes("/") || value.includes("\\");} export function normalizeCommandPath(commandPath: string): string {  return commandPath.replaceAll("\\", "/").toLowerCase();} function isBunGlobalCommandPath(commandPath: string): boolean {  return normalizeCommandPath(commandPath).includes("/.bun/bin/");} function isVitePlusGlobalCommandPath(commandPath: string): boolean {  return normalizeCommandPath(commandPath).includes("/.vite-plus/bin/");} function isPnpmGlobalCommandPath(commandPath: string): boolean {  const normalized = normalizeCommandPath(commandPath);  return (    normalized.includes("/.local/share/pnpm/") ||    normalized.includes("/library/pnpm/") ||    normalized.includes("/local/share/pnpm/") ||    normalized.includes("/appdata/local/pnpm/") ||    normalized.includes("/pnpm/global/")  );} function isNpmGlobalCommandPath(commandPath: string): boolean {  const normalized = normalizeCommandPath(commandPath);  return (    normalized.includes("/node_modules/.bin/") ||    normalized.includes("/lib/node_modules/") ||    normalized.includes("/npm/node_modules/")  );} function isHomebrewCommandPath(commandPath: string): boolean {  const normalized = normalizeCommandPath(commandPath);  return (    normalized.includes("/opt/homebrew/cellar/") ||    normalized.includes("/usr/local/cellar/") ||    normalized.includes("/homebrew/cellar/") ||    normalized.includes("/opt/homebrew/caskroom/") ||    normalized.includes("/usr/local/caskroom/") ||    normalized.includes("/homebrew/caskroom/") ||    normalized.startsWith("/opt/homebrew/bin/") ||    normalized.startsWith("/usr/local/bin/")  );} export function resolvePackageManagedProviderMaintenance(  definition: PackageManagedProviderMaintenanceDefinition,  options?: ProviderMaintenanceCapabilityResolutionOptions,): ProviderMaintenanceCapabilities {  const binaryPath = nonEmptyString(options?.binaryPath);  if (!binaryPath) {    return makeNpmGlobalProviderMaintenanceCapabilities(definition);  }   const resolvedCommandPath =    options?.resolvedCommandPath ?? (hasPathSeparator(binaryPath) ? binaryPath : null);   if (resolvedCommandPath) {    const commandPaths = [      resolvedCommandPath,      ...(options?.realCommandPath ? [options.realCommandPath] : []),    ];     const nativeUpdate = definition.nativeUpdate;    if (      nativeUpdate &&      commandPaths.some((commandPath) => nativeUpdate.isCommandPath(commandPath))    ) {      return (        makeNativeProviderMaintenanceCapabilities(definition) ??        makeNpmGlobalProviderMaintenanceCapabilities(definition)      );    }    if (commandPaths.some(isVitePlusGlobalCommandPath)) {      return makeVitePlusGlobalProviderMaintenanceCapabilities(definition);    }    if (commandPaths.some(isBunGlobalCommandPath)) {      return makeBunGlobalProviderMaintenanceCapabilities(definition);    }    if (commandPaths.some(isPnpmGlobalCommandPath)) {      return makePnpmGlobalProviderMaintenanceCapabilities(definition);    }    if (commandPaths.some(isNpmGlobalCommandPath)) {      return makeNpmGlobalProviderMaintenanceCapabilities(definition);    }    if (commandPaths.some(isHomebrewCommandPath)) {      return makeHomebrewProviderMaintenanceCapabilities(definition);    }  }   if (!hasPathSeparator(binaryPath)) {    return makeNpmGlobalProviderMaintenanceCapabilities(definition);  }   return makeManualOnlyProviderMaintenanceCapabilities({    provider: definition.provider,    packageName: definition.npmPackageName,  });} export function makePackageManagedProviderMaintenanceResolver(  definition: PackageManagedProviderMaintenanceDefinition,): ProviderMaintenanceCapabilitiesResolver {  return {    resolve: (options) => resolvePackageManagedProviderMaintenance(definition, options),  };} export function makeStaticProviderMaintenanceResolver(  capabilities: ProviderMaintenanceCapabilities,): ProviderMaintenanceCapabilitiesResolver {  return {    resolve: () => capabilities,  };} function makeManualProviderMaintenanceCapabilities(  provider: ProviderDriverKind,): ProviderMaintenanceCapabilities {  return makeManualOnlyProviderMaintenanceCapabilities({    provider,    packageName: null,  });} export const resolveProviderMaintenanceCapabilitiesEffect = Effect.fn(  "resolveProviderMaintenanceCapabilitiesEffect",)(function* (  resolver: ProviderMaintenanceCapabilitiesResolver,  options?: Omit<ProviderMaintenanceCapabilityResolutionOptions, "realCommandPath">,) {  const binaryPath = nonEmptyString(options?.binaryPath);  if (!binaryPath) {    return resolver.resolve(options);  }   const env = options?.env ?? (yield* readCommandLookupEnv);  const resolvedCommandPath =    (yield* resolveCommandPath(binaryPath, { env }).pipe(      Effect.catchTag("CommandResolutionError", () => Effect.succeed(null)),    )) ?? (hasPathSeparator(binaryPath) ? binaryPath : null);  if (!resolvedCommandPath) {    return resolver.resolve(options);  }   const fileSystem = yield* FileSystem.FileSystem;  const realCommandPath = yield* fileSystem    .realPath(resolvedCommandPath)    .pipe(Effect.orElseSucceed(() => resolvedCommandPath));  return resolver.resolve({    ...options,    env,    resolvedCommandPath,    realCommandPath,  });}); function deriveVersionAdvisory(input: {  readonly currentVersion: string | null;  readonly latestVersion: string | null;}): Pick<ServerProviderVersionAdvisory, "status" | "message"> {  if (!input.currentVersion) {    return { status: "unknown", message: null };  }  if (!input.latestVersion) {    return { status: "unknown", message: null };  }  if (compareSemverVersions(input.currentVersion, input.latestVersion) < 0) {    return {      status: "behind_latest",      message: PROVIDER_UPDATE_ACTION_TOAST_MESSAGE,    };  }  return { status: "current", message: null };} export function createProviderVersionAdvisory(input: {  readonly driver: ProviderDriverKind;  readonly currentVersion: string | null;  readonly latestVersion?: string | null;  readonly checkedAt?: string | null;  readonly maintenanceCapabilities?: ProviderMaintenanceCapabilities;}): ServerProviderVersionAdvisory {  const capabilities =    input.maintenanceCapabilities ?? makeManualProviderMaintenanceCapabilities(input.driver);  const latestVersion = input.latestVersion ?? null;  const advisory = deriveVersionAdvisory({    currentVersion: input.currentVersion,    latestVersion,  });   return {    status: advisory.status,    currentVersion: input.currentVersion,    latestVersion,    updateCommand: capabilities.update?.command ?? null,    canUpdate: capabilities.update !== null,    checkedAt: input.checkedAt ?? null,    message: advisory.message,  };} const fetchNpmLatestVersion = Effect.fn("fetchNpmLatestVersion")(function* (packageName: string) {  const client = yield* HttpClient.HttpClient;  const request = HttpClientRequest.get(    `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,  ).pipe(HttpClientRequest.setHeader("accept", "application/json"));  const response = yield* client.execute(request).pipe(    Effect.timeoutOption(LATEST_VERSION_TIMEOUT_MS),    Effect.orElseSucceed(() => Option.none()),  );  if (Option.isNone(response)) {    return null;  }  const httpResponse = response.value;  if (httpResponse.status < 200 || httpResponse.status >= 300) {    return null;  }  const payload = yield* httpResponse.json.pipe(    Effect.flatMap(Schema.decodeUnknownEffect(NpmLatestVersionResponse)),    Effect.orElseSucceed(() => null),  );  return payload ? nonEmptyString(payload.version) : null;}); export const resolveLatestProviderVersion = Effect.fn("resolveLatestProviderVersion")(function* (  maintenanceCapabilities: ProviderMaintenanceCapabilities,) {  const packageName = maintenanceCapabilities.packageName;  if (!packageName) {    return null;  }   const latestVersionCache = yield* ProviderVersionCache;  const cached = latestVersionCache.get(packageName);  const now = DateTime.toEpochMillis(yield* DateTime.now);  if (cached && cached.expiresAt > now) {    return cached.version;  }   const version = yield* fetchNpmLatestVersion(packageName);  latestVersionCache.set(packageName, {    expiresAt: now + LATEST_VERSION_CACHE_TTL_MS,    version,  });  return version;}); export const enrichProviderSnapshotWithVersionAdvisory = Effect.fn(  "enrichProviderSnapshotWithVersionAdvisory",)(function* (  snapshot: ServerProvider,  maintenanceCapabilities?: ProviderMaintenanceCapabilities,  options?: {    readonly enableProviderUpdateChecks: boolean | undefined;  },) {  const capabilities =    maintenanceCapabilities ?? makeManualProviderMaintenanceCapabilities(snapshot.driver);  const shouldResolveLatestVersion =    options?.enableProviderUpdateChecks !== false &&    snapshot.enabled &&    snapshot.installed &&    Boolean(snapshot.version);  if (!shouldResolveLatestVersion) {    return {      ...snapshot,      versionAdvisory: createProviderVersionAdvisory({        driver: snapshot.driver,        currentVersion: snapshot.version,        checkedAt: snapshot.checkedAt,        maintenanceCapabilities: capabilities,      }),    };  }   const latestVersion = yield* resolveLatestProviderVersion(capabilities);  return {    ...snapshot,    versionAdvisory: createProviderVersionAdvisory({

Lines 1–500 of 508.