import type { EnvironmentId, ServerConfig, ServerSelfUpdateCapability } from "@t3tools/contracts";import * as Schema from "effect/Schema"; import { APP_VERSION } from "./branding";import { getLocalStorageItem, setLocalStorageItem } from "./hooks/useLocalStorage"; export interface VersionMismatch { readonly clientVersion: string; readonly serverVersion: string; readonly hint: string;} export const VERSION_MISMATCH_DISMISSALS_STORAGE_KEY = "t3code:version-mismatch-dismissals:v1"; const VersionMismatchDismissalsSchema = Schema.Struct({ keys: Schema.Array(Schema.String),}); type VersionMismatchDismissals = typeof VersionMismatchDismissalsSchema.Type; function normalizeVersion(version: string | null | undefined): string | null { const trimmed = version?.trim(); return trimmed && trimmed.length > 0 ? trimmed : null;} export function resolveVersionMismatch( serverVersion: string | null | undefined,): VersionMismatch | null { const normalizedClientVersion = normalizeVersion(APP_VERSION); const normalizedServerVersion = normalizeVersion(serverVersion); if ( !normalizedClientVersion || !normalizedServerVersion || normalizedClientVersion === normalizedServerVersion ) { return null; } return { clientVersion: normalizedClientVersion, serverVersion: normalizedServerVersion, hint: "Version mismatch. Try syncing the client and server to the same T3 Code version.", };} export function resolveServerConfigVersionMismatch( serverConfig: Pick<ServerConfig, "environment"> | null | undefined,): VersionMismatch | null { return resolveVersionMismatch(serverConfig?.environment.serverVersion);} /** The update path the connected server offers, or null when it only supports a manual relaunch (older servers, dev checkouts, Windows). */export function resolveServerSelfUpdateCapability( serverConfig: Pick<ServerConfig, "environment"> | null | undefined,): ServerSelfUpdateCapability | null { return serverConfig?.environment.capabilities.serverSelfUpdate ?? null;} /** The command to hand users whose server cannot update itself. */export function manualServerUpdateCommand(targetVersion: string): string { return `npx t3@${targetVersion}`;} /** One sentence telling the user how to resolve version skew for a server, matched to the update path it offers. */export function serverUpdateGuidance( capability: ServerSelfUpdateCapability | null, serverLabel: string,): string { switch (capability) { case "boot-service": case "respawn": return `Update the ${serverLabel} so they stay in sync.`; case "desktop-managed": return `The ${serverLabel} is run by the T3 Code desktop app on its machine — update the desktop app there to sync them.`; default: return `Relaunch the ${serverLabel} with the copied command to sync them.`; }} export function buildVersionMismatchDismissalKey( environmentId: EnvironmentId, mismatch: Pick<VersionMismatch, "clientVersion" | "serverVersion">,): string { return `${environmentId}:${mismatch.clientVersion}:${mismatch.serverVersion}`;} function readVersionMismatchDismissals(): VersionMismatchDismissals { try { return ( getLocalStorageItem( VERSION_MISMATCH_DISMISSALS_STORAGE_KEY, VersionMismatchDismissalsSchema, ) ?? { keys: [] } ); } catch (error) { console.error("Could not read version-mismatch dismissals.", error); return { keys: [] }; }} function writeVersionMismatchDismissals(document: VersionMismatchDismissals): void { try { setLocalStorageItem( VERSION_MISMATCH_DISMISSALS_STORAGE_KEY, document, VersionMismatchDismissalsSchema, ); } catch (error) { console.error("Could not persist version-mismatch dismissals.", error); }} export function isVersionMismatchDismissed(dismissalKey: string | null | undefined): boolean { if (!dismissalKey) { return false; } return readVersionMismatchDismissals().keys.includes(dismissalKey);} export function dismissVersionMismatch(dismissalKey: string | null | undefined): void { if (!dismissalKey) { return; } const document = readVersionMismatchDismissals(); if (document.keys.includes(dismissalKey)) { return; } writeVersionMismatchDismissals({ keys: [...document.keys, dismissalKey], });} export function appendVersionMismatchHint( message: string | null | undefined, mismatch: VersionMismatch | null | undefined,): string | null { const normalizedMessage = normalizeVersion(message); if (!normalizedMessage) { return mismatch?.hint ?? null; } if (!mismatch) { return normalizedMessage; } return `${normalizedMessage} Hint: ${mismatch.hint}`;}
import type { EnvironmentId, ServerConfig, ServerSelfUpdateCapability } from "@t3tools/contracts";
import * as Schema from "effect/Schema";
import { APP_VERSION } from "./branding";
import { getLocalStorageItem, setLocalStorageItem } from "./hooks/useLocalStorage";
export interface VersionMismatch {
readonly clientVersion: string;
readonly serverVersion: string;
readonly hint: string;
}
export const VERSION_MISMATCH_DISMISSALS_STORAGE_KEY = "t3code:version-mismatch-dismissals:v1";
const VersionMismatchDismissalsSchema = Schema.Struct({
keys: Schema.Array(Schema.String),
});
type VersionMismatchDismissals = typeof VersionMismatchDismissalsSchema.Type;
function normalizeVersion(version: string | null | undefined): string | null {
const trimmed = version?.trim();
return trimmed && trimmed.length > 0 ? trimmed : null;
}
export function resolveVersionMismatch(
serverVersion: string | null | undefined,
): VersionMismatch | null {
const normalizedClientVersion = normalizeVersion(APP_VERSION);
const normalizedServerVersion = normalizeVersion(serverVersion);
if (
!normalizedClientVersion ||
!normalizedServerVersion ||
normalizedClientVersion === normalizedServerVersion
) {
return null;
}
return {
clientVersion: normalizedClientVersion,
serverVersion: normalizedServerVersion,
hint: "Version mismatch. Try syncing the client and server to the same T3 Code version.",
};
}
export function resolveServerConfigVersionMismatch(
serverConfig: Pick<ServerConfig, "environment"> | null | undefined,
): VersionMismatch | null {
return resolveVersionMismatch(serverConfig?.environment.serverVersion);
}
/** The update path the connected server offers, or null when it only
supports a manual relaunch (older servers, dev checkouts, Windows). */
export function resolveServerSelfUpdateCapability(
serverConfig: Pick<ServerConfig, "environment"> | null | undefined,
): ServerSelfUpdateCapability | null {
return serverConfig?.environment.capabilities.serverSelfUpdate ?? null;
}
/** The command to hand users whose server cannot update itself. */
export function manualServerUpdateCommand(targetVersion: string): string {
return `npx t3@${targetVersion}`;
}
/** One sentence telling the user how to resolve version skew for a server,
matched to the update path it offers. */
export function serverUpdateGuidance(
capability: ServerSelfUpdateCapability | null,
serverLabel: string,
): string {
switch (capability) {
case "boot-service":
case "respawn":
return `Update the ${serverLabel} so they stay in sync.`;
case "desktop-managed":
return `The ${serverLabel} is run by the T3 Code desktop app on its machine — update the desktop app there to sync them.`;
default:
return `Relaunch the ${serverLabel} with the copied command to sync them.`;
}
}
export function buildVersionMismatchDismissalKey(
environmentId: EnvironmentId,
mismatch: Pick<VersionMismatch, "clientVersion" | "serverVersion">,
): string {
return `${environmentId}:${mismatch.clientVersion}:${mismatch.serverVersion}`;
}
function readVersionMismatchDismissals(): VersionMismatchDismissals {
try {
return (
getLocalStorageItem(
VERSION_MISMATCH_DISMISSALS_STORAGE_KEY,
VersionMismatchDismissalsSchema,
) ?? { keys: [] }
);
} catch (error) {
console.error("Could not read version-mismatch dismissals.", error);
return { keys: [] };
}
}
function writeVersionMismatchDismissals(document: VersionMismatchDismissals): void {
try {
setLocalStorageItem(
VERSION_MISMATCH_DISMISSALS_STORAGE_KEY,
document,
VersionMismatchDismissalsSchema,
);
} catch (error) {
console.error("Could not persist version-mismatch dismissals.", error);
}
}
export function isVersionMismatchDismissed(dismissalKey: string | null | undefined): boolean {
if (!dismissalKey) {
return false;
}
return readVersionMismatchDismissals().keys.includes(dismissalKey);
}
export function dismissVersionMismatch(dismissalKey: string | null | undefined): void {
if (!dismissalKey) {
return;
}
const document = readVersionMismatchDismissals();
if (document.keys.includes(dismissalKey)) {
return;
}
writeVersionMismatchDismissals({
keys: [...document.keys, dismissalKey],
});
}
export function appendVersionMismatchHint(
message: string | null | undefined,
mismatch: VersionMismatch | null | undefined,
): string | null {
const normalizedMessage = normalizeVersion(message);
if (!normalizedMessage) {
return mismatch?.hint ?? null;
}
if (!mismatch) {
return normalizedMessage;
}
return `${normalizedMessage} Hint: ${mismatch.hint}`;
}