Open History to see the last commit
1import type { DesktopBridge, DesktopUpdateChannel, DesktopUpdateState } from "@t3tools/contracts";
2import { acceptsTrackSwitchRelease } from "@t3tools/shared/cliRelease";
3
4type TrackSwitchBridge = Pick<DesktopBridge, "setUpdateChannel">;
5
6/** Saves the track, then, when this app must reinstall, finds a build the database check covers.
7 * The saved track is the user's intent and stays even when no such build is ready yet: a track
8 * saved toward Stable never offers an older build outside a guarded switch, and "Switch them"
9 * retries. Saving when nothing reinstalls lets picking the old track undo a partial switch. */
10export async function prepareLocalTrackSwitch(
11 bridge: TrackSwitchBridge,
12 input: {
13 readonly channel: DesktopUpdateChannel;
14 readonly version: string;
15 readonly reinstall: boolean;
16 },
17): Promise<
18 | { readonly ok: true; readonly local: DesktopUpdateState | null }
19 | { readonly ok: false; readonly message: string }
20> {
21 let local: DesktopUpdateState;
22 try {
23 // A build on another track re-checks the feed, older releases allowed down to the checked one.
24 local = await bridge.setUpdateChannel(
25 input.channel,
26 input.reinstall ? input.version : undefined,
27 );
28 } catch (error) {
29 return {
30 ok: false,
31 message: error instanceof Error ? error.message : "The update track did not change.",
32 };
33 }
34 if (!input.reinstall) return { ok: true, local: null };
35 const found = local.downloadedVersion ?? local.availableVersion;
36 if (
37 (local.status === "available" || local.status === "downloaded") &&
38 found !== null &&
39 acceptsTrackSwitchRelease(found, input.channel, input.version)
40 ) {
41 return { ok: true, local };
42 }
43 return {
44 ok: false,
45 message:
46 found !== null
47 ? `This app found ${found}, which is older than ${input.version}. Try again in a few minutes.`
48 : (local.message ?? `This app found no build of ${input.version} to install.`),
49 };
50}
51
52/** When no machine moves, confirming only saves the track; when that track is already saved too,
53 * confirming would do nothing. Track-only hosts are not rows but still get a request. */
54export function nothingToMove(input: {
55 readonly label: string;
56 readonly channel: DesktopUpdateChannel;
57 // Null until the desktop reports it; an unknown track may still need saving.
58 readonly savedChannel: DesktopUpdateChannel | null;
59 readonly switchesLocal: boolean;
60 readonly crossesLocal: boolean;
61 readonly rows: ReadonlyArray<{ readonly skipped: string | null }>;
62 readonly hasTargets: boolean;
63}): { readonly note: string | null; readonly disabledReason: string | null } {
64 if (input.switchesLocal || input.rows.some((row) => row.skipped === null)) {
65 return { note: null, disabledReason: null };
66 }
67 const reason =
68 input.rows.length === 0 && !input.crossesLocal
69 ? `Every connected machine is already on ${input.label}.`
70 : `No machine can switch to ${input.label} now.`;
71 return !input.hasTargets && input.savedChannel === input.channel
72 ? { note: null, disabledReason: reason }
73 : { note: `${reason} This only saves the track.`, disabledReason: null };
74}
75
76/**
77 * Whether this app stays put while connected machines move: its updater is off, as for a .deb
78 * install or without a feed. Off only because a policy rules out its nightly channel, it moves.
79 */
80export function localUpdaterStaysPut(
81 state: Pick<DesktopUpdateState, "enabled" | "channelBlockedByPolicy"> | null | undefined,
82 target: DesktopUpdateChannel,
83): boolean {
84 return (
85 state?.enabled === false && !(target === "latest" && state.channelBlockedByPolicy === true)
86 );
87}