Skip to content
Source
Sign inSign up

desktopUpdate.logic.ts

166 lines6.7 KB

1import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts";
2​
3export type DesktopUpdateButtonAction = "download" | "install" | "none";
4​
5const DEFAULT_DESKTOP_RELEASE_REPOSITORY = "pingdotgg/t3code";
6const GITHUB_REPOSITORY_SEGMENT = /^[A-Za-z0-9._-]+$/u;
7​
8function isValidRepositorySegment(value: string | undefined): value is string {
9 return (
10 value !== undefined && value !== "." && value !== ".." && GITHUB_REPOSITORY_SEGMENT.test(value)
11 );
13​
14function desktopReleaseRepositoryUrl(repository: string | undefined): string {
15 const configuredRepository = repository?.trim() ?? "";
16 const [owner, name, ...extra] = configuredRepository.split("/");
17 const releaseRepository =
18 isValidRepositorySegment(owner) && isValidRepositorySegment(name) && extra.length === 0
19 ? configuredRepository
20 : DEFAULT_DESKTOP_RELEASE_REPOSITORY;
21 return `https://github.com/${releaseRepository}/releases`;
23​
24/** The download RPC can beat the updater's downloadedVersion event; fall back to the requested version. */
25export function getDesktopUpdateDownloadedVersion(state: DesktopUpdateState): string | null {
26 return state.downloadedVersion ?? state.availableVersion;
28​
29export function getDesktopUpdateReleaseUrl(
30 version: string | null,
31 repository: string | undefined = import.meta.env.T3CODE_DESKTOP_UPDATE_REPOSITORY,
32): string | null {
33 const normalizedVersion = version?.trim();
34 if (!normalizedVersion) return null;
35 return `${desktopReleaseRepositoryUrl(repository)}/tag/v${encodeURIComponent(normalizedVersion)}`;
37​
38export function getDesktopUpdateReleaseHistoryUrl(
39 repository: string | undefined = import.meta.env.T3CODE_DESKTOP_UPDATE_REPOSITORY,
40): string {
41 return desktopReleaseRepositoryUrl(repository);
43​
44export function resolveDesktopUpdateButtonAction(
45 state: DesktopUpdateState,
46): DesktopUpdateButtonAction {
47 if (
48 state.downloadedVersion &&
49 (state.status === "downloaded" ||
50 (state.status === "error" &&
51 (state.errorContext === null || state.errorContext === "install")))
52 ) {
53 return "install";
54 }
55 if (state.status === "available") {
56 return "download";
57 }
58 if (state.status === "error") {
59 if (state.errorContext === "download" && state.availableVersion) {
60 return "download";
61 }
62 }
63 return "none";
65​
66export function shouldShowArm64IntelBuildWarning(state: DesktopUpdateState | null): boolean {
67 return state?.hostArch === "arm64" && state.appArch === "x64";
69​
70export function isDesktopUpdateButtonDisabled(state: DesktopUpdateState | null): boolean {
71 return state?.status === "downloading";
73​
74export function getArm64IntelBuildWarningDescription(state: DesktopUpdateState): string {
75 if (!shouldShowArm64IntelBuildWarning(state)) {
76 return "This install is using the correct architecture.";
77 }
78​
79 const action = resolveDesktopUpdateButtonAction(state);
80 if (action === "download") {
81 return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. Download the available update to switch to the native Apple Silicon build.";
82 }
83 if (action === "install") {
84 return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. Restart to install the downloaded Apple Silicon build.";
85 }
86 return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. The next app update will replace it with the native Apple Silicon build.";
88​
89export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string {
90 if (
91 state.notAppliedVersion !== undefined &&
92 (state.status === "available" || state.status === "downloaded") &&
93 getDesktopUpdateDownloadedVersion(state) === state.notAppliedVersion
94 ) {
95 return `Update ${state.notAppliedVersion} did not apply. Click to retry.`;
96 }
97 if (state.status === "available") {
98 return `Update ${state.availableVersion ?? "available"} ready to download`;
99 }
100 if (state.status === "downloading") {
101 const progress =
102 typeof state.downloadPercent === "number" ? ` (${Math.floor(state.downloadPercent)}%)` : "";
103 return `Downloading update${progress}`;
104 }
105 if (state.status === "downloaded") {
106 return `Update ${state.downloadedVersion ?? state.availableVersion ?? "ready"} downloaded. Click to restart and install.`;
107 }
108 if (state.status === "error") {
109 if (state.errorContext === "download" && state.availableVersion) {
110 return `Download failed for ${state.availableVersion}. Click to retry.`;
111 }
112 if (state.errorContext === "install" && state.downloadedVersion) {
113 return `Install failed for ${state.downloadedVersion}. Click to retry.`;
114 }
115 if (state.downloadedVersion) {
116 return `Update ${state.downloadedVersion} downloaded. Click to restart and install.`;
117 }
118 return state.message ?? "Update failed";
119 }
120 return "Up to date";
122​
123export function getDesktopUpdateInstallConfirmationMessage(
124 state: Pick<DesktopUpdateState, "availableVersion" | "downloadedVersion">,
125): string {
126 const version = state.downloadedVersion ?? state.availableVersion;
127 return `Install update${version ? ` ${version}` : ""} and restart T3 Code?\n\nAny running tasks will be interrupted. Make sure you're ready before continuing.`;
129​
130export function getDesktopUpdateActionError(result: DesktopUpdateActionResult): string | null {
131 if (!result.accepted || result.completed) return null;
132 if (typeof result.state.message !== "string") return null;
133 const message = result.state.message.trim();
134 return message.length > 0 ? message : null;
136​
137export function shouldToastDesktopUpdateActionResult(result: DesktopUpdateActionResult): boolean {
138 return getDesktopUpdateActionError(result) !== null;
140​
141export function canCheckForUpdate(state: DesktopUpdateState | null): boolean {
142 if (!state || !state.enabled) return false;
143 return (
144 state.status !== "checking" && state.status !== "downloading" && state.status !== "disabled"
145 );
147​
148/** `managedDenial` is this machine's policy refusing updates (`managedUpdatesDenial`); it wins. */
149export function getDesktopUpdateDisabledReason(
150 state: Pick<DesktopUpdateState, "enabled" | "status" | "message"> | null,
151 managedDenial?: string,
152): string | null {
153 if (managedDenial !== undefined) return managedDenial;
154 if (!state || (state.enabled && state.status !== "disabled")) return null;
155 return state.message?.trim() || "Automatic updates are not available in this build.";
157​
158export function getDesktopUpdateIdleTooltip(
159 state: Pick<DesktopUpdateState, "enabled" | "status" | "message"> | null,
160 managedDenial?: string,
161) {
162 const disabledReason = getDesktopUpdateDisabledReason(state, managedDenial);
163 return disabledReason
164 ? { title: "Updates unavailable", detail: disabledReason }
165 : { title: "Check for updates", detail: null };