Skip to content

Commit 981aeba

Browse files
juliusmarmingecodex
andcommitted
Merge origin/main into t3code/greeting
Co-authored-by: codex <codex@users.noreply.github.com>
2 parents ece6436 + 5462d50 commit 981aeba

File tree

125 files changed

+6058
-2295
lines changed

Some content is hidden

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

125 files changed

+6058
-2295
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# git autocrlf=true converts LF to CRLF on Windows, causing issues with oxfmt
2+
* text=auto eol=lf

.github/VOUCHED.td

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ github:realAhmedRoach
2828
github:shiroyasha9
2929
github:Yash-Singh1
3030
github:eggfriedrice24
31-
github:Ymit24
31+
github:Ymit24
32+
github:shivamhwp

.github/workflows/pr-size.yml

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -124,25 +124,49 @@ jobs:
124124
group: pr-size-${{ github.event.pull_request.number }}
125125
cancel-in-progress: true
126126
steps:
127+
- name: Checkout base repository
128+
uses: actions/checkout@v4
129+
with:
130+
fetch-depth: 0
127131
- name: Sync PR size label
128132
uses: actions/github-script@v8
129133
env:
130134
PR_SIZE_LABELS_JSON: ${{ needs.prepare-config.outputs.labels_json }}
131135
with:
132136
script: |
137+
const { execFileSync } = require("node:child_process");
138+
133139
const issueNumber = context.payload.pull_request.number;
140+
const baseSha = context.payload.pull_request.base.sha;
141+
const headSha = context.payload.pull_request.head.sha;
142+
const headTrackingRef = `refs/remotes/pr-size/${issueNumber}`;
134143
const managedLabels = JSON.parse(process.env.PR_SIZE_LABELS_JSON ?? "[]");
135144
const managedLabelNames = new Set(managedLabels.map((label) => label.name));
136145
// Keep this aligned with the repo's test entrypoints and test-only support files.
137-
const testFilePatterns = [
138-
/(^|\/)__tests__(\/|$)/,
139-
/(^|\/)tests?(\/|$)/,
140-
/^apps\/server\/integration\//,
141-
/\.(test|spec|browser|integration)\.[^.\/]+$/,
146+
const testExcludePathspecs = [
147+
":(glob,exclude)**/__tests__/**",
148+
":(glob,exclude)**/test/**",
149+
":(glob,exclude)**/tests/**",
150+
":(glob,exclude)apps/server/integration/**",
151+
":(glob,exclude)**/*.test.*",
152+
":(glob,exclude)**/*.spec.*",
153+
":(glob,exclude)**/*.browser.*",
154+
":(glob,exclude)**/*.integration.*",
142155
];
143156
144-
const isTestFile = (filename) =>
145-
testFilePatterns.some((pattern) => pattern.test(filename));
157+
const sumNumstat = (text) =>
158+
text
159+
.split("\n")
160+
.filter(Boolean)
161+
.reduce((total, line) => {
162+
const [insertionsRaw = "0", deletionsRaw = "0"] = line.split("\t");
163+
const additions =
164+
insertionsRaw === "-" ? 0 : Number.parseInt(insertionsRaw, 10) || 0;
165+
const deletions =
166+
deletionsRaw === "-" ? 0 : Number.parseInt(deletionsRaw, 10) || 0;
167+
168+
return total + additions + deletions;
169+
}, 0);
146170
147171
const resolveSizeLabel = (totalChangedLines) => {
148172
if (totalChangedLines < 10) {
@@ -168,40 +192,53 @@ jobs:
168192
return "size:XXL";
169193
};
170194
171-
const files = await github.paginate(
172-
github.rest.pulls.listFiles,
195+
execFileSync("git", ["fetch", "--no-tags", "origin", baseSha], {
196+
stdio: "inherit",
197+
});
198+
199+
execFileSync(
200+
"git",
201+
["fetch", "--no-tags", "origin", `+refs/pull/${issueNumber}/head:${headTrackingRef}`],
173202
{
174-
owner: context.repo.owner,
175-
repo: context.repo.repo,
176-
pull_number: issueNumber,
177-
per_page: 100,
203+
stdio: "inherit",
178204
},
179-
(response) => response.data,
180205
);
181206
182-
if (files.length >= 3000) {
207+
const resolvedHeadSha = execFileSync("git", ["rev-parse", headTrackingRef], {
208+
encoding: "utf8",
209+
}).trim();
210+
211+
if (resolvedHeadSha !== headSha) {
183212
core.warning(
184-
"The GitHub pull request files API may truncate results at 3,000 files; PR size may be undercounted.",
213+
`Fetched head SHA ${resolvedHeadSha} does not match pull request head SHA ${headSha}; using fetched ref for sizing.`,
185214
);
186215
}
187216
188-
let testChangedLines = 0;
189-
let nonTestChangedLines = 0;
190-
191-
for (const file of files) {
192-
const changedLinesForFile = (file.additions ?? 0) + (file.deletions ?? 0);
193-
194-
if (changedLinesForFile === 0) {
195-
continue;
196-
}
217+
execFileSync("git", ["cat-file", "-e", `${baseSha}^{commit}`], {
218+
stdio: "inherit",
219+
});
197220
198-
if (isTestFile(file.filename)) {
199-
testChangedLines += changedLinesForFile;
200-
continue;
201-
}
221+
const diffArgs = [
222+
"diff",
223+
"--numstat",
224+
"--ignore-all-space",
225+
"--ignore-blank-lines",
226+
`${baseSha}...${resolvedHeadSha}`,
227+
];
202228
203-
nonTestChangedLines += changedLinesForFile;
204-
}
229+
const totalChangedLines = sumNumstat(
230+
execFileSync(
231+
"git",
232+
diffArgs,
233+
{ encoding: "utf8" },
234+
),
235+
);
236+
const nonTestChangedLines = sumNumstat(
237+
execFileSync("git", [...diffArgs, "--", ".", ...testExcludePathspecs], {
238+
encoding: "utf8",
239+
}),
240+
);
241+
const testChangedLines = Math.max(0, totalChangedLines - nonTestChangedLines);
205242
206243
const changedLines = nonTestChangedLines === 0 ? testChangedLines : nonTestChangedLines;
207244
const nextLabelName = resolveSizeLabel(changedLines);

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ packages/*/dist
1212
build/
1313
.logs/
1414
release/
15+
release-mock/
1516
.t3
1617
.idea/
1718
apps/web/.playwright

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@t3tools/desktop",
3-
"version": "0.0.14",
3+
"version": "0.0.15",
44
"private": true,
55
"main": "dist-electron/main.js",
66
"scripts": {

apps/desktop/src/main.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as Effect from "effect/Effect";
2020
import type {
2121
DesktopTheme,
2222
DesktopUpdateActionResult,
23+
DesktopUpdateCheckResult,
2324
DesktopUpdateState,
2425
} from "@t3tools/contracts";
2526
import { autoUpdater } from "electron-updater";
@@ -56,6 +57,7 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state";
5657
const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
5758
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
5859
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
60+
const UPDATE_CHECK_CHANNEL = "desktop:update-check";
5961
const GET_WS_URL_CHANNEL = "desktop:get-ws-url";
6062
const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");
6163
const STATE_DIR = Path.join(BASE_DIR, "userdata");
@@ -287,10 +289,12 @@ let updatePollTimer: ReturnType<typeof setInterval> | null = null;
287289
let updateStartupTimer: ReturnType<typeof setTimeout> | null = null;
288290
let updateCheckInFlight = false;
289291
let updateDownloadInFlight = false;
292+
let updateInstallInFlight = false;
290293
let updaterConfigured = false;
291294
let updateState: DesktopUpdateState = initialUpdateState();
292295

293296
function resolveUpdaterErrorContext(): DesktopUpdateErrorContext {
297+
if (updateInstallInFlight) return "install";
294298
if (updateDownloadInFlight) return "download";
295299
if (updateCheckInFlight) return "check";
296300
return updateState.errorContext;
@@ -754,26 +758,28 @@ function shouldEnableAutoUpdates(): boolean {
754758
);
755759
}
756760

757-
async function checkForUpdates(reason: string): Promise<void> {
758-
if (isQuitting || !updaterConfigured || updateCheckInFlight) return;
761+
async function checkForUpdates(reason: string): Promise<boolean> {
762+
if (isQuitting || !updaterConfigured || updateCheckInFlight) return false;
759763
if (updateState.status === "downloading" || updateState.status === "downloaded") {
760764
console.info(
761765
`[desktop-updater] Skipping update check (${reason}) while status=${updateState.status}.`,
762766
);
763-
return;
767+
return false;
764768
}
765769
updateCheckInFlight = true;
766770
setUpdateState(reduceDesktopUpdateStateOnCheckStart(updateState, new Date().toISOString()));
767771
console.info(`[desktop-updater] Checking for updates (${reason})...`);
768772

769773
try {
770774
await autoUpdater.checkForUpdates();
775+
return true;
771776
} catch (error: unknown) {
772777
const message = error instanceof Error ? error.message : String(error);
773778
setUpdateState(
774779
reduceDesktopUpdateStateOnCheckFailure(updateState, message, new Date().toISOString()),
775780
);
776781
console.error(`[desktop-updater] Failed to check for updates: ${message}`);
782+
return true;
777783
} finally {
778784
updateCheckInFlight = false;
779785
}
@@ -807,13 +813,22 @@ async function installDownloadedUpdate(): Promise<{ accepted: boolean; completed
807813
}
808814

809815
isQuitting = true;
816+
updateInstallInFlight = true;
810817
clearUpdatePollTimer();
811818
try {
812819
await stopBackendAndWaitForExit();
813-
autoUpdater.quitAndInstall();
814-
return { accepted: true, completed: true };
820+
// Destroy all windows before launching the NSIS installer to avoid the installer finding live windows it needs to close.
821+
for (const win of BrowserWindow.getAllWindows()) {
822+
win.destroy();
823+
}
824+
// `quitAndInstall()` only starts the handoff to the updater. The actual
825+
// install may still fail asynchronously, so keep the action incomplete
826+
// until we either quit or receive an updater error.
827+
autoUpdater.quitAndInstall(true, true);
828+
return { accepted: true, completed: false };
815829
} catch (error: unknown) {
816830
const message = formatErrorMessage(error);
831+
updateInstallInFlight = false;
817832
isQuitting = false;
818833
setUpdateState(reduceDesktopUpdateStateOnInstallFailure(updateState, message));
819834
console.error(`[desktop-updater] Failed to install update: ${message}`);
@@ -850,6 +865,13 @@ function configureAutoUpdater(): void {
850865
}
851866
}
852867

868+
if (process.env.T3CODE_DESKTOP_MOCK_UPDATES) {
869+
autoUpdater.setFeedURL({
870+
provider: "generic",
871+
url: `http://localhost:${process.env.T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT ?? 3000}`,
872+
});
873+
}
874+
853875
autoUpdater.autoDownload = false;
854876
autoUpdater.autoInstallOnAppQuit = false;
855877
// Keep alpha branding, but force all installs onto the stable update track.
@@ -886,6 +908,13 @@ function configureAutoUpdater(): void {
886908
});
887909
autoUpdater.on("error", (error) => {
888910
const message = formatErrorMessage(error);
911+
if (updateInstallInFlight) {
912+
updateInstallInFlight = false;
913+
isQuitting = false;
914+
setUpdateState(reduceDesktopUpdateStateOnInstallFailure(updateState, message));
915+
console.error(`[desktop-updater] Updater error: ${message}`);
916+
return;
917+
}
889918
if (!updateCheckInFlight && !updateDownloadInFlight) {
890919
setUpdateState({
891920
status: "error",
@@ -1141,6 +1170,7 @@ function registerIpcHandlers(): void {
11411170
id: item.id,
11421171
label: item.label,
11431172
destructive: item.destructive === true,
1173+
disabled: item.disabled === true,
11441174
}));
11451175
if (normalizedItems.length === 0) {
11461176
return null;
@@ -1171,6 +1201,7 @@ function registerIpcHandlers(): void {
11711201
}
11721202
const itemOption: MenuItemConstructorOptions = {
11731203
label: item.label,
1204+
enabled: !item.disabled,
11741205
click: () => resolve(item.id),
11751206
};
11761207
if (item.destructive) {
@@ -1236,6 +1267,21 @@ function registerIpcHandlers(): void {
12361267
state: updateState,
12371268
} satisfies DesktopUpdateActionResult;
12381269
});
1270+
1271+
ipcMain.removeHandler(UPDATE_CHECK_CHANNEL);
1272+
ipcMain.handle(UPDATE_CHECK_CHANNEL, async () => {
1273+
if (!updaterConfigured) {
1274+
return {
1275+
checked: false,
1276+
state: updateState,
1277+
} satisfies DesktopUpdateCheckResult;
1278+
}
1279+
const checked = await checkForUpdates("web-ui");
1280+
return {
1281+
checked,
1282+
state: updateState,
1283+
} satisfies DesktopUpdateCheckResult;
1284+
});
12391285
}
12401286

12411287
function getIconOption(): { icon: string } | Record<string, never> {
@@ -1359,6 +1405,7 @@ async function bootstrap(): Promise<void> {
13591405

13601406
app.on("before-quit", () => {
13611407
isQuitting = true;
1408+
updateInstallInFlight = false;
13621409
writeDesktopLogHeader("before-quit received");
13631410
clearUpdatePollTimer();
13641411
stopBackend();
@@ -1388,7 +1435,7 @@ app
13881435
});
13891436

13901437
app.on("window-all-closed", () => {
1391-
if (process.platform !== "darwin") {
1438+
if (process.platform !== "darwin" && !isQuitting) {
13921439
app.quit();
13931440
}
13941441
});

apps/desktop/src/preload.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
99
const MENU_ACTION_CHANNEL = "desktop:menu-action";
1010
const UPDATE_STATE_CHANNEL = "desktop:update-state";
1111
const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
12+
const UPDATE_CHECK_CHANNEL = "desktop:update-check";
1213
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
1314
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
1415
const GET_WS_URL_CHANNEL = "desktop:get-ws-url";
@@ -35,6 +36,7 @@ contextBridge.exposeInMainWorld("desktopBridge", {
3536
};
3637
},
3738
getUpdateState: () => ipcRenderer.invoke(UPDATE_GET_STATE_CHANNEL),
39+
checkForUpdate: () => ipcRenderer.invoke(UPDATE_CHECK_CHANNEL),
3840
downloadUpdate: () => ipcRenderer.invoke(UPDATE_DOWNLOAD_CHANNEL),
3941
installUpdate: () => ipcRenderer.invoke(UPDATE_INSTALL_CHANNEL),
4042
onUpdateState: (listener) => {

apps/desktop/src/syncShellEnvironment.test.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,13 @@ describe("syncShellEnvironment", () => {
6262
expect(env.SSH_AUTH_SOCK).toBe("/tmp/inherited.sock");
6363
});
6464

65-
it("does nothing outside macOS", () => {
65+
it("hydrates PATH and missing SSH_AUTH_SOCK from the login shell on linux", () => {
6666
const env: NodeJS.ProcessEnv = {
6767
SHELL: "/bin/zsh",
6868
PATH: "/usr/bin",
69-
SSH_AUTH_SOCK: "/tmp/inherited.sock",
7069
};
7170
const readEnvironment = vi.fn(() => ({
72-
PATH: "/opt/homebrew/bin:/usr/bin",
71+
PATH: "/home/linuxbrew/.linuxbrew/bin:/usr/bin",
7372
SSH_AUTH_SOCK: "/tmp/secretive.sock",
7473
}));
7574

@@ -78,8 +77,29 @@ describe("syncShellEnvironment", () => {
7877
readEnvironment,
7978
});
8079

80+
expect(readEnvironment).toHaveBeenCalledWith("/bin/zsh", ["PATH", "SSH_AUTH_SOCK"]);
81+
expect(env.PATH).toBe("/home/linuxbrew/.linuxbrew/bin:/usr/bin");
82+
expect(env.SSH_AUTH_SOCK).toBe("/tmp/secretive.sock");
83+
});
84+
85+
it("does nothing outside macOS and linux", () => {
86+
const env: NodeJS.ProcessEnv = {
87+
SHELL: "C:/Program Files/Git/bin/bash.exe",
88+
PATH: "C:\\Windows\\System32",
89+
SSH_AUTH_SOCK: "/tmp/inherited.sock",
90+
};
91+
const readEnvironment = vi.fn(() => ({
92+
PATH: "/usr/local/bin:/usr/bin",
93+
SSH_AUTH_SOCK: "/tmp/secretive.sock",
94+
}));
95+
96+
syncShellEnvironment(env, {
97+
platform: "win32",
98+
readEnvironment,
99+
});
100+
81101
expect(readEnvironment).not.toHaveBeenCalled();
82-
expect(env.PATH).toBe("/usr/bin");
102+
expect(env.PATH).toBe("C:\\Windows\\System32");
83103
expect(env.SSH_AUTH_SOCK).toBe("/tmp/inherited.sock");
84104
});
85105
});

0 commit comments

Comments
 (0)