Skip to content

Commit 5a2d587

Browse files
authored
Merge pull request #69 from aaditagrawal/sync/upstream-2026-04-17
sync: merge 35 upstream commits from pingdotgg/t3code
2 parents b3bd5c3 + 65da3d1 commit 5a2d587

204 files changed

Lines changed: 13151 additions & 4798 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,8 @@ jobs:
9292
with:
9393
node-version-file: package.json
9494

95+
- name: Install dependencies
96+
run: bun install --frozen-lockfile
97+
9598
- name: Exercise release-only workflow steps
9699
run: bun run scripts/release-smoke.ts

.oxfmtrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"*.tsbuildinfo",
1111
"**/routeTree.gen.ts",
1212
"apps/web/public/mockServiceWorker.js",
13-
"apps/web/src/lib/vendor/qrcodegen.ts"
13+
"apps/web/src/lib/vendor/qrcodegen.ts",
14+
"*.icon/**"
1415
],
1516
"sortPackageJson": {}
1617
}

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
AGENTS.md
1+
AGENTS.md

apps/desktop/scripts/electron-launcher.mjs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
cpSync,
77
existsSync,
88
mkdirSync,
9+
mkdtempSync,
910
readFileSync,
1011
readdirSync,
1112
rmSync,
@@ -25,6 +26,8 @@ const LAUNCHER_VERSION = 2;
2526

2627
const __dirname = dirname(fileURLToPath(import.meta.url));
2728
export const desktopDir = resolve(__dirname, "..");
29+
const repoRoot = resolve(desktopDir, "..", "..");
30+
const developmentMacIconPngPath = join(repoRoot, "assets", "dev", "blueprint-macos-1024.png");
2831

2932
function setPlistString(plistPath, key, value) {
3033
const replaceResult = spawnSync("plutil", ["-replace", key, "-string", value, plistPath], {
@@ -45,6 +48,68 @@ function setPlistString(plistPath, key, value) {
4548
throw new Error(`Failed to update plist key "${key}" at ${plistPath}: ${details}`.trim());
4649
}
4750

51+
function runChecked(command, args) {
52+
const result = spawnSync(command, args, { encoding: "utf8" });
53+
if (result.status === 0) {
54+
return;
55+
}
56+
57+
const details = [result.stdout, result.stderr].filter(Boolean).join("\n");
58+
throw new Error(`Failed to run ${command} ${args.join(" ")}: ${details}`.trim());
59+
}
60+
61+
function ensureDevelopmentIconIcns(runtimeDir, fallbackIconPath) {
62+
const generatedIconPath = join(runtimeDir, "icon-dev.icns");
63+
mkdirSync(runtimeDir, { recursive: true });
64+
65+
if (!existsSync(developmentMacIconPngPath)) {
66+
return fallbackIconPath;
67+
}
68+
69+
const sourceMtimeMs = statSync(developmentMacIconPngPath).mtimeMs;
70+
if (existsSync(generatedIconPath) && statSync(generatedIconPath).mtimeMs >= sourceMtimeMs) {
71+
return generatedIconPath;
72+
}
73+
74+
const iconsetRoot = mkdtempSync(join(runtimeDir, "dev-iconset-"));
75+
const iconsetDir = join(iconsetRoot, "icon.iconset");
76+
mkdirSync(iconsetDir, { recursive: true });
77+
78+
try {
79+
for (const size of [16, 32, 128, 256, 512]) {
80+
runChecked("sips", [
81+
"-z",
82+
String(size),
83+
String(size),
84+
developmentMacIconPngPath,
85+
"--out",
86+
join(iconsetDir, `icon_${size}x${size}.png`),
87+
]);
88+
89+
const retinaSize = size * 2;
90+
runChecked("sips", [
91+
"-z",
92+
String(retinaSize),
93+
String(retinaSize),
94+
developmentMacIconPngPath,
95+
"--out",
96+
join(iconsetDir, `icon_${size}x${size}@2x.png`),
97+
]);
98+
}
99+
100+
runChecked("iconutil", ["-c", "icns", iconsetDir, "-o", generatedIconPath]);
101+
return generatedIconPath;
102+
} catch (error) {
103+
console.warn(
104+
"[desktop-launcher] Failed to generate dev macOS icon, falling back to default icon.",
105+
error,
106+
);
107+
return fallbackIconPath;
108+
} finally {
109+
rmSync(iconsetRoot, { recursive: true, force: true });
110+
}
111+
}
112+
48113
function patchMainBundleInfoPlist(appBundlePath) {
49114
const infoPlistPath = join(appBundlePath, "Contents", "Info.plist");
50115
setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);
@@ -111,10 +176,10 @@ function resolveIconSourceMetadata(desktopResourcesDir) {
111176
};
112177
}
113178

114-
async function stageMainBundleIcons(appBundlePath, desktopResourcesDir) {
179+
async function stageMainBundleIcons(appBundlePath, desktopResourcesDir, legacyIconOverride) {
115180
const resourcesDir = join(appBundlePath, "Contents", "Resources");
116181
const iconComposerPath = join(desktopResourcesDir, "icon.icon");
117-
const legacyIconPath = join(desktopResourcesDir, "icon.icns");
182+
const legacyIconPath = legacyIconOverride ?? join(desktopResourcesDir, "icon.icns");
118183

119184
if (existsSync(iconComposerPath)) {
120185
const compiled = await generateAssetCatalogForIcon(iconComposerPath);
@@ -144,10 +209,18 @@ async function buildMacLauncher(electronBinaryPath) {
144209
const targetAppBundlePath = join(runtimeDir, `${APP_DISPLAY_NAME}.app`);
145210
const targetBinaryPath = join(targetAppBundlePath, "Contents", "MacOS", "Electron");
146211
const desktopResourcesDir = join(desktopDir, "resources");
212+
const defaultLegacyIconPath = join(desktopResourcesDir, "icon.icns");
147213
const metadataPath = join(runtimeDir, "metadata.json");
148214

149215
mkdirSync(runtimeDir, { recursive: true });
150216

217+
// In dev mode, fall back to a generated icon derived from the dev blueprint PNG so dev
218+
// builds are visually distinct. If the modern icon.icon composer source is present, the
219+
// staging step below takes precedence and this override is ignored.
220+
const legacyIconOverride = isDevelopment
221+
? ensureDevelopmentIconIcns(runtimeDir, defaultLegacyIconPath)
222+
: undefined;
223+
151224
const iconMetadata = resolveIconSourceMetadata(desktopResourcesDir);
152225

153226
const expectedMetadata = {
@@ -172,6 +245,7 @@ async function buildMacLauncher(electronBinaryPath) {
172245
const refreshedIconMetadata = await stageMainBundleIcons(
173246
targetAppBundlePath,
174247
desktopResourcesDir,
248+
legacyIconOverride,
175249
);
176250
patchHelperBundleInfoPlists(targetAppBundlePath);
177251
writeFileSync(
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { resolveDesktopAppBranding, resolveDesktopAppStageLabel } from "./appBranding";
4+
5+
describe("resolveDesktopAppStageLabel", () => {
6+
it("uses Dev in desktop development", () => {
7+
expect(
8+
resolveDesktopAppStageLabel({
9+
isDevelopment: true,
10+
appVersion: "0.0.17-nightly.20260414.1",
11+
}),
12+
).toBe("Dev");
13+
});
14+
15+
it("uses Nightly for packaged nightly builds", () => {
16+
expect(
17+
resolveDesktopAppStageLabel({
18+
isDevelopment: false,
19+
appVersion: "0.0.17-nightly.20260414.1",
20+
}),
21+
).toBe("Nightly");
22+
});
23+
24+
it("uses Alpha for packaged stable builds", () => {
25+
expect(
26+
resolveDesktopAppStageLabel({
27+
isDevelopment: false,
28+
appVersion: "0.0.17",
29+
}),
30+
).toBe("Alpha");
31+
});
32+
});
33+
34+
describe("resolveDesktopAppBranding", () => {
35+
it("returns a complete desktop branding payload", () => {
36+
expect(
37+
resolveDesktopAppBranding({
38+
isDevelopment: false,
39+
appVersion: "0.0.17-nightly.20260414.1",
40+
}),
41+
).toEqual({
42+
baseName: "T3 Code",
43+
stageLabel: "Nightly",
44+
displayName: "T3 Code (Nightly)",
45+
});
46+
});
47+
});

apps/desktop/src/appBranding.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { DesktopAppBranding, DesktopAppStageLabel } from "@t3tools/contracts";
2+
3+
import { isNightlyDesktopVersion } from "./updateChannels";
4+
5+
const APP_BASE_NAME = "T3 Code";
6+
7+
export function resolveDesktopAppStageLabel(input: {
8+
readonly isDevelopment: boolean;
9+
readonly appVersion: string;
10+
}): DesktopAppStageLabel {
11+
if (input.isDevelopment) {
12+
return "Dev";
13+
}
14+
15+
return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Alpha";
16+
}
17+
18+
export function resolveDesktopAppBranding(input: {
19+
readonly isDevelopment: boolean;
20+
readonly appVersion: string;
21+
}): DesktopAppBranding {
22+
const stageLabel = resolveDesktopAppStageLabel(input);
23+
return {
24+
baseName: APP_BASE_NAME,
25+
stageLabel,
26+
displayName: `${APP_BASE_NAME} (${stageLabel})`,
27+
};
28+
}

apps/desktop/src/backendReadiness.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from "./backendReadiness";
88

99
describe("waitForHttpReady", () => {
10-
it("returns once the backend reports a successful session endpoint", async () => {
10+
it("returns once the backend serves the requested readiness path", async () => {
1111
const fetchImpl = vi
1212
.fn<typeof fetch>()
1313
.mockResolvedValueOnce(new Response(null, { status: 503 }))
@@ -20,6 +20,11 @@ describe("waitForHttpReady", () => {
2020
});
2121

2222
expect(fetchImpl).toHaveBeenCalledTimes(2);
23+
expect(fetchImpl).toHaveBeenNthCalledWith(
24+
1,
25+
"http://127.0.0.1:3773/",
26+
expect.objectContaining({ redirect: "manual" }),
27+
);
2328
});
2429

2530
it("retries after a readiness request stalls past the per-request timeout", async () => {
@@ -80,4 +85,30 @@ describe("waitForHttpReady", () => {
8085
expect(isBackendReadinessAborted(new BackendReadinessAbortedError())).toBe(true);
8186
expect(isBackendReadinessAborted(new Error("nope"))).toBe(false);
8287
});
88+
89+
it("supports custom readiness predicates", async () => {
90+
const fetchImpl = vi
91+
.fn<typeof fetch>()
92+
.mockResolvedValueOnce(new Response(null, { status: 200 }))
93+
.mockResolvedValueOnce(new Response(null, { status: 204 }));
94+
95+
await waitForHttpReady("http://127.0.0.1:3773", {
96+
fetchImpl,
97+
timeoutMs: 1_000,
98+
intervalMs: 0,
99+
path: "/api/healthz",
100+
isReady: (response) => response.status === 204,
101+
});
102+
103+
expect(fetchImpl).toHaveBeenNthCalledWith(
104+
1,
105+
"http://127.0.0.1:3773/api/healthz",
106+
expect.objectContaining({ redirect: "manual" }),
107+
);
108+
expect(fetchImpl).toHaveBeenNthCalledWith(
109+
2,
110+
"http://127.0.0.1:3773/api/healthz",
111+
expect.objectContaining({ redirect: "manual" }),
112+
);
113+
});
83114
});

apps/desktop/src/backendReadiness.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ export interface WaitForHttpReadyOptions {
44
readonly requestTimeoutMs?: number;
55
readonly fetchImpl?: typeof fetch;
66
readonly signal?: AbortSignal;
7+
readonly path?: string;
8+
readonly isReady?: (response: Response) => boolean;
79
}
810

9-
const DEFAULT_TIMEOUT_MS = 10_000;
11+
const DEFAULT_TIMEOUT_MS = 30_000;
1012
const DEFAULT_INTERVAL_MS = 100;
1113
const DEFAULT_REQUEST_TIMEOUT_MS = 1_000;
1214

@@ -57,6 +59,8 @@ export async function waitForHttpReady(
5759
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
5860
const intervalMs = options?.intervalMs ?? DEFAULT_INTERVAL_MS;
5961
const requestTimeoutMs = options?.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
62+
const readinessPath = options?.path ?? "/";
63+
const isReady = options?.isReady ?? ((response: Response) => response.ok);
6064
const deadline = Date.now() + timeoutMs;
6165

6266
for (;;) {
@@ -74,11 +78,11 @@ export async function waitForHttpReady(
7478
signal?.addEventListener("abort", abortRequest, { once: true });
7579

7680
try {
77-
const response = await fetchImpl(`${baseUrl}/api/auth/session`, {
81+
const response = await fetchImpl(new URL(readinessPath, baseUrl).toString(), {
7882
redirect: "manual",
7983
signal: requestController.signal,
8084
});
81-
if (response.ok) {
85+
if (isReady(response)) {
8286
return;
8387
}
8488
} catch (error) {

0 commit comments

Comments
 (0)