1/**
2 * Naming shared by the release workflow, the runtime installers, and
3 * install scripts for the per-platform CLI archives attached to GitHub
4 * Releases. Every consumer derives the same file names from a version and a
5 * platform key, so a rename here is a release-breaking change.
6 */
7
8const CLI_RELEASE_REPOSITORY = "pingdotgg/t3code";
9export const CLI_RELEASE_CHECKSUMS_FILE = "SHA256SUMS";
10/** Overrides the download origin for mirrors and air-gapped installs. */
11export const CLI_RELEASE_BASE_URL_ENV = "T3CODE_RELEASE_BASE_URL";
12
13/**
14 * The archives a release attaches. Kept in step with the build_linux_cli
15 * matrix, build_windows_arm64_cli, and the `cli_archive` rows in
16 * .github/workflows/release.yml: a key here without a build there produces
17 * download URLs that 404, and a build there without a key here is
18 * unreachable from every installer.
19 */
20// No darwin-x64: Node single-executables are unsupported on x64 macOS (the
21// SEA docs list macOS as arm64 only) and the binary segfaults on start.
22export const CLI_ARCHIVE_PLATFORM_KEYS = [
23 "darwin-arm64",
24 "linux-arm64",
25 "linux-x64",
26 "win32-arm64",
27 "win32-x64",
28] as const;
29export type CliArchivePlatformKey = (typeof CLI_ARCHIVE_PLATFORM_KEYS)[number];
30
31export function cliArchivePlatformKey(
32 platform: NodeJS.Platform,
33 arch: string,
34): CliArchivePlatformKey | undefined {
35 const key = `${platform}-${arch}`;
36 return CLI_ARCHIVE_PLATFORM_KEYS.find((candidate) => candidate === key);
37}
38
39/**
40 * The tar to extract a release archive with. Windows ships bsdtar in
41 * System32, which reads both formats; a Git-for-Windows GNU tar earlier on
42 * PATH cannot open the zip, so the system copy is named by absolute path.
43 */
44export function cliArchiveTarCommand(
45 platform: NodeJS.Platform,
46 env: Readonly<Record<string, string | undefined>>,
47): string {
48 if (platform !== "win32") return "tar";
49 const systemRoot = env["SystemRoot"] ?? env["windir"] ?? "C:\\Windows";
50 return `${systemRoot}\\System32\\tar.exe`;
51}
52
53export function cliArchiveFileName(version: string, platformKey: CliArchivePlatformKey): string {
54 return `t3-${version}-${platformKey}.${platformKey.startsWith("win32") ? "zip" : "tar.gz"}`;
55}
56
57const CLI_RELEASE_DEFAULT_BASE_URL = `https://github.com/${CLI_RELEASE_REPOSITORY}/releases/download`;
58
59/** Directory that `releases/download/<tag>/<asset>` lives under. */
60export function cliReleaseDownloadBaseUrl(
61 version: string,
62 baseUrl: string | undefined = CLI_RELEASE_DEFAULT_BASE_URL,
63): string {
64 return `${(baseUrl?.trim() || CLI_RELEASE_DEFAULT_BASE_URL).replace(/\/+$/, "")}/v${version}`;
65}
66
67/**
68 * Parses the `sha256sum` style checksum file attached to each release.
69 * Lines are `<hex> <file>`; a leading `*` marks binary mode and is ignored.
70 */
71export function parseChecksums(text: string): ReadonlyMap<string, string> {
72 const checksums = new Map<string, string>();
73 for (const line of text.split(/\r?\n/)) {
74 const match = /^([0-9a-fA-F]{64})\s+\*?(\S.*)$/.exec(line.trim());
75 if (match?.[1] !== undefined && match[2] !== undefined) {
76 checksums.set(match[2], match[1].toLowerCase());
77 }
78 }
79 return checksums;
80}
81
82export type CliReleaseChannel = "stable" | "nightly" | "preview";
83export const CLI_RELEASE_CHANNELS: ReadonlyArray<CliReleaseChannel> = [
84 "stable",
85 "nightly",
86 "preview",
87];
88
89/** The release train a version was published on, derived from its prerelease tag. */
90export function cliReleaseChannelOf(version: string): CliReleaseChannel {
91 const channel = /^[^-+]+-(nightly|preview)\.\d{8}\.\d+$/.exec(version)?.[1];
92 return channel === "nightly" || channel === "preview" ? channel : "stable";
93}
94
95/**
96 * One page of GitHub's list-releases endpoint, newest first. Callers walk pages
97 * until a channel match turns up; a busy nightly train can push the newest
98 * preview or stable release past any single page.
99 */
100export function cliReleaseIndexPageUrl(page: number): string {
101 return `https://api.github.com/repos/${CLI_RELEASE_REPOSITORY}/releases?per_page=100&page=${page}`;
102}
103
104/**
105 * Picks the newest version on a channel from the release index. Tags are
106 * `v<version>`; the channel is decided by the same rule the runtime uses, so
107 * a preview tag never satisfies a nightly lookup and vice versa. Drafts are
108 * skipped because their assets are not downloadable.
109 */
110export function newestCliReleaseVersion(
111 releases: ReadonlyArray<{
112 readonly tag_name: string;
113 readonly draft?: boolean | undefined;
114 }>,
115 channel: CliReleaseChannel,
116): string | undefined {
117 for (const release of releases) {
118 if (release.draft) continue;
119 const version = /^v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(release.tag_name)?.[1];
120 if (version === undefined) continue;
121 if (cliReleaseChannelOf(version) === channel) return version;
122 }
123 return undefined;
124}