From 2a3f0fa361e80bb18255d0a3253354619feab05c Mon Sep 17 00:00:00 2001 From: Nebularc Date: Sat, 11 Apr 2026 20:04:04 +0200 Subject: [PATCH 1/2] Disable shell execution in process runner - Preserve argument boundaries for shell-sensitive values - Keep timeout and buffer failure cleanup killing the full process tree --- apps/server/src/processRunner.test.ts | 16 ++++++++++++++++ apps/server/src/processRunner.ts | 7 +++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/server/src/processRunner.test.ts b/apps/server/src/processRunner.test.ts index dd909116d4..c136b8ee57 100644 --- a/apps/server/src/processRunner.test.ts +++ b/apps/server/src/processRunner.test.ts @@ -3,6 +3,22 @@ import { describe, expect, it } from "vitest"; import { runProcess } from "./processRunner"; describe("runProcess", () => { + it("preserves argument boundaries for shell-sensitive values", async () => { + const result = await runProcess(process.execPath, [ + "-e", + "process.stdout.write(JSON.stringify(process.argv.slice(1)))", + "Add GHCR Docker publish workflow", + "ampersand & value", + 'quoted " value', + ]); + + expect(JSON.parse(result.stdout)).toEqual([ + "Add GHCR Docker publish workflow", + "ampersand & value", + 'quoted " value', + ]); + }); + it("fails when output exceeds max buffer in default mode", async () => { await expect( runProcess("node", ["-e", "process.stdout.write('x'.repeat(2048))"], { maxBufferBytes: 128 }), diff --git a/apps/server/src/processRunner.ts b/apps/server/src/processRunner.ts index 5402612887..918774bc9b 100644 --- a/apps/server/src/processRunner.ts +++ b/apps/server/src/processRunner.ts @@ -81,9 +81,8 @@ function normalizeBufferError( const DEFAULT_MAX_BUFFER_BYTES = 8 * 1024 * 1024; /** - * On Windows with `shell: true`, `child.kill()` only terminates the `cmd.exe` - * wrapper, leaving the actual command running. Use `taskkill /T` to kill the - * entire process tree instead. + * On Windows, terminate the process tree so child processes launched by the + * command do not survive a timeout or output-buffer failure. */ function killChild(child: ChildProcessHandle, signal: NodeJS.Signals = "SIGTERM"): void { if (process.platform === "win32" && child.pid !== undefined) { @@ -139,7 +138,7 @@ export async function runProcess( cwd: options.cwd, env: options.env, stdio: "pipe", - shell: process.platform === "win32", + shell: false, }); let stdout = ""; From 752f261e5f3c97b0e950cb857c37375695cd92df Mon Sep 17 00:00:00 2001 From: Nebularc Date: Sat, 11 Apr 2026 21:28:05 +0200 Subject: [PATCH 2/2] Rebrand desktop app to H3Code - Rename app display names, IDs, and data paths - Update web splash, sidebar branding, and build artifact names --- apps/desktop/package.json | 2 +- apps/desktop/scripts/electron-launcher.mjs | 6 +- apps/desktop/src/main.ts | 37 +++--- apps/web/index.html | 6 +- apps/web/src/branding.ts | 4 +- apps/web/src/components/Sidebar.tsx | 21 +--- apps/web/src/components/SplashScreen.tsx | 9 +- apps/web/src/vite-env.d.ts | 1 + apps/web/vite.config.ts | 128 +++++++++++---------- scripts/build-desktop-artifact.ts | 16 +-- 10 files changed, 116 insertions(+), 114 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a38ffd2df1..dca6e3dafa 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -26,5 +26,5 @@ "typescript": "catalog:", "vitest": "catalog:" }, - "productName": "T3 Code (Alpha)" + "productName": "H3Code" } diff --git a/apps/desktop/scripts/electron-launcher.mjs b/apps/desktop/scripts/electron-launcher.mjs index b8875b08ab..4f5894bc85 100644 --- a/apps/desktop/scripts/electron-launcher.mjs +++ b/apps/desktop/scripts/electron-launcher.mjs @@ -1,4 +1,4 @@ -// This file mostly exists because we want dev mode to say "T3 Code (Dev)" instead of "electron" +// This file mostly exists because we want dev mode to say "H3Code (Dev)" instead of "electron" import { spawnSync } from "node:child_process"; import { @@ -17,8 +17,8 @@ import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); -const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; -const APP_BUNDLE_ID = isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code"; +const APP_DISPLAY_NAME = isDevelopment ? "H3Code (Dev)" : "H3Code"; +const APP_BUNDLE_ID = isDevelopment ? "com.h3tools.h3code.dev" : "com.h3tools.h3code"; const LAUNCHER_VERSION = 1; const __dirname = dirname(fileURLToPath(import.meta.url)); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index f6e7544d0b..4906679852 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -91,7 +91,10 @@ const SET_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:set-saved-environment-secr const REMOVE_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:remove-saved-environment-secret"; const GET_SERVER_EXPOSURE_STATE_CHANNEL = "desktop:get-server-exposure-state"; const SET_SERVER_EXPOSURE_MODE_CHANNEL = "desktop:set-server-exposure-mode"; -const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3"); +const BASE_DIR = + process.env.H3CODE_HOME?.trim() || + process.env.T3CODE_HOME?.trim() || + Path.join(OS.homedir(), ".h3code"); const STATE_DIR = Path.join(BASE_DIR, "userdata"); const DESKTOP_SETTINGS_PATH = Path.join(STATE_DIR, "desktop-settings.json"); const CLIENT_SETTINGS_PATH = Path.join(STATE_DIR, "client-settings.json"); @@ -99,12 +102,15 @@ const SAVED_ENVIRONMENT_REGISTRY_PATH = Path.join(STATE_DIR, "saved-environments const DESKTOP_SCHEME = "t3"; const ROOT_DIR = Path.resolve(__dirname, "../../.."); const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); -const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; -const APP_USER_MODEL_ID = "com.t3tools.t3code"; -const LINUX_DESKTOP_ENTRY_NAME = isDevelopment ? "t3code-dev.desktop" : "t3code.desktop"; -const LINUX_WM_CLASS = isDevelopment ? "t3code-dev" : "t3code"; -const USER_DATA_DIR_NAME = isDevelopment ? "t3code-dev" : "t3code"; -const LEGACY_USER_DATA_DIR_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; +const APP_DISPLAY_NAME = isDevelopment ? "H3Code (Dev)" : "H3Code"; +const APP_USER_MODEL_ID = "com.h3tools.h3code"; +const LINUX_DESKTOP_ENTRY_NAME = isDevelopment ? "h3code-dev.desktop" : "h3code.desktop"; +const LINUX_WM_CLASS = isDevelopment ? "h3code-dev" : "h3code"; +const USER_DATA_DIR_NAME = isDevelopment ? "h3code-dev" : "h3code"; +const LEGACY_PRE_REBRAND_APP_NAME = ["T3", "Code"].join(" "); +const LEGACY_PRE_REBRAND_USER_DATA_DIR_NAME = isDevelopment + ? `${LEGACY_PRE_REBRAND_APP_NAME} (Dev)` + : `${LEGACY_PRE_REBRAND_APP_NAME} (Alpha)`; const COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/i; const COMMIT_HASH_DISPLAY_LENGTH = 12; const LOG_DIR = Path.join(STATE_DIR, "logs"); @@ -659,7 +665,7 @@ function handleFatalStartupError(stage: string, error: unknown): void { console.error(`[desktop] fatal startup error (${stage})`, error); if (!isQuitting) { isQuitting = true; - dialog.showErrorBox("T3 Code failed to start", `Stage: ${stage}\n${message}${detail}`); + dialog.showErrorBox("H3Code failed to start", `Stage: ${stage}\n${message}${detail}`); } stopBackend(); restoreStdIoCapture?.(); @@ -764,7 +770,7 @@ async function checkForUpdatesFromMenu(): Promise { void dialog.showMessageBox({ type: "info", title: "You're up to date!", - message: `T3 Code ${updateState.currentVersion} is currently the newest version available.`, + message: `H3Code ${updateState.currentVersion} is currently the newest version available.`, buttons: ["OK"], }); } else if (updateState.status === "error") { @@ -881,13 +887,10 @@ function resolveIconPath(ext: "ico" | "icns" | "png"): string | null { * Resolve the Electron userData directory path. * * Electron derives the default userData path from `productName` in - * package.json, which currently produces directories with spaces and - * parentheses (e.g. `~/.config/T3 Code (Alpha)` on Linux). This is - * unfriendly for shell usage and violates Linux naming conventions. - * - * We override it to a clean lowercase name (`t3code`). If the legacy - * directory already exists we keep using it so existing users don't - * lose their Chromium profile data (localStorage, cookies, sessions). + * package.json. Override it to a clean lowercase name so packaged and local + * builds keep stable paths across platforms. If the pre-rebrand path already + * exists we keep using it so existing users don't lose their Chromium profile + * data (localStorage, cookies, sessions). */ function resolveUserDataPath(): string { const appDataBase = @@ -897,7 +900,7 @@ function resolveUserDataPath(): string { ? Path.join(OS.homedir(), "Library", "Application Support") : process.env.XDG_CONFIG_HOME || Path.join(OS.homedir(), ".config"); - const legacyPath = Path.join(appDataBase, LEGACY_USER_DATA_DIR_NAME); + const legacyPath = Path.join(appDataBase, LEGACY_PRE_REBRAND_USER_DATA_DIR_NAME); if (FS.existsSync(legacyPath)) { return legacyPath; } diff --git a/apps/web/index.html b/apps/web/index.html index 9f0329b602..0a70ee7e9a 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -86,13 +86,13 @@ href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..800;1,9..40,300..800&display=swap" rel="stylesheet" /> - T3 Code (Alpha) + H3Code
-
- +
+
diff --git a/apps/web/src/branding.ts b/apps/web/src/branding.ts index bffd983815..1045532158 100644 --- a/apps/web/src/branding.ts +++ b/apps/web/src/branding.ts @@ -1,4 +1,4 @@ -export const APP_BASE_NAME = "T3 Code"; -export const APP_STAGE_LABEL = import.meta.env.DEV ? "Dev" : "Alpha"; +export const APP_BASE_NAME = "H3Code"; +export const APP_STAGE_LABEL = import.meta.env.APP_STAGE_LABEL; export const APP_DISPLAY_NAME = `${APP_BASE_NAME} (${APP_STAGE_LABEL})`; export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0"; diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 03ae979017..e68960ec61 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -55,7 +55,7 @@ import { } from "@t3tools/contracts/settings"; import { usePrimaryEnvironmentId } from "../environments/primary"; import { isElectron } from "../env"; -import { APP_STAGE_LABEL, APP_VERSION } from "../branding"; +import { APP_BASE_NAME, APP_STAGE_LABEL, APP_VERSION } from "../branding"; import { isTerminalFocused } from "../lib/terminalFocus"; import { isLinuxPlatform, isMacPlatform, newCommandId, newProjectId } from "../lib/utils"; import { @@ -1815,22 +1815,6 @@ const SidebarProjectListRow = memo(function SidebarProjectListRow(props: Sidebar ); }); -function T3Wordmark() { - return ( - - - - ); -} - type SortableProjectHandleProps = Pick< ReturnType, "attributes" | "listeners" | "setActivatorNodeRef" @@ -1956,9 +1940,8 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({ className="ml-1 flex min-w-0 flex-1 cursor-pointer items-center gap-1 rounded-md outline-hidden ring-ring transition-colors hover:text-foreground focus-visible:ring-2" to="/" > - - Code + {APP_BASE_NAME} {APP_STAGE_LABEL} diff --git a/apps/web/src/components/SplashScreen.tsx b/apps/web/src/components/SplashScreen.tsx index a0b593a950..f468111455 100644 --- a/apps/web/src/components/SplashScreen.tsx +++ b/apps/web/src/components/SplashScreen.tsx @@ -1,8 +1,13 @@ +import { APP_BASE_NAME } from "../branding"; + export function SplashScreen() { return (
-
- T3 Code +
+ {APP_BASE_NAME}
); diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts index 1d7d41db1b..6a6dd1d5a4 100644 --- a/apps/web/src/vite-env.d.ts +++ b/apps/web/src/vite-env.d.ts @@ -3,6 +3,7 @@ import type { DesktopBridge, LocalApi } from "@t3tools/contracts"; interface ImportMetaEnv { + readonly APP_STAGE_LABEL: string; readonly APP_VERSION: string; } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 01b5076695..ead8fb697b 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -10,6 +10,7 @@ const host = process.env.HOST?.trim() || "localhost"; const configuredHttpUrl = process.env.VITE_HTTP_URL?.trim(); const configuredWsUrl = process.env.VITE_WS_URL?.trim(); const sourcemapEnv = process.env.T3CODE_WEB_SOURCEMAP?.trim().toLowerCase(); +const APP_BASE_NAME = "H3Code"; const buildSourcemap = sourcemapEnv === "0" || sourcemapEnv === "false" @@ -41,65 +42,74 @@ function resolveDevProxyTarget(wsUrl: string | undefined): string | undefined { const devProxyTarget = resolveDevProxyTarget(configuredWsUrl); -export default defineConfig({ - plugins: [ - tanstackRouter(), - react(), - babel({ - // We need to be explicit about the parser options after moving to @vitejs/plugin-react v6.0.0 - // This is because the babel plugin only automatically parses typescript and jsx based on relative paths (e.g. "**/*.ts") - // whereas the previous version of the plugin parsed all files with a .ts extension. - // This is causing our packages/ directory to fail to parse, as they are not relative to the CWD. - parserOpts: { plugins: ["typescript", "jsx"] }, - presets: [reactCompilerPreset()], - }), - tailwindcss(), - ], - optimizeDeps: { - include: ["@pierre/diffs", "@pierre/diffs/react", "@pierre/diffs/worker/worker.js"], - }, - define: { - "import.meta.env.VITE_HTTP_URL": JSON.stringify(configuredHttpUrl ?? ""), - // In dev mode, tell the web app where the WebSocket server lives - "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""), - "import.meta.env.APP_VERSION": JSON.stringify(pkg.version), - }, - resolve: { - tsconfigPaths: true, - }, - server: { - host, - port, - strictPort: true, - ...(devProxyTarget - ? { - proxy: { - "/.well-known": { - target: devProxyTarget, - changeOrigin: true, - }, - "/api": { - target: devProxyTarget, - changeOrigin: true, - }, - "/attachments": { - target: devProxyTarget, - changeOrigin: true, - }, - }, - } - : {}), - hmr: { - // Explicit config so Vite's HMR WebSocket connects reliably - // inside Electron's BrowserWindow. Vite 8 uses console.debug for - // connection logs — enable "Verbose" in DevTools to see them. - protocol: "ws", +export default defineConfig(({ command }) => { + const appStageLabel = command === "serve" ? "Dev" : "Alpha"; + + return { + plugins: [ + { + name: "h3code-html-branding", + transformIndexHtml: (html) => html.replaceAll("%APP_BASE_NAME%", APP_BASE_NAME), + }, + tanstackRouter(), + react(), + babel({ + // We need to be explicit about the parser options after moving to @vitejs/plugin-react v6.0.0 + // This is because the babel plugin only automatically parses typescript and jsx based on relative paths (e.g. "**/*.ts") + // whereas the previous version of the plugin parsed all files with a .ts extension. + // This is causing our packages/ directory to fail to parse, as they are not relative to the CWD. + parserOpts: { plugins: ["typescript", "jsx"] }, + presets: [reactCompilerPreset()], + }), + tailwindcss(), + ], + optimizeDeps: { + include: ["@pierre/diffs", "@pierre/diffs/react", "@pierre/diffs/worker/worker.js"], + }, + define: { + "import.meta.env.VITE_HTTP_URL": JSON.stringify(configuredHttpUrl ?? ""), + // In dev mode, tell the web app where the WebSocket server lives + "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""), + "import.meta.env.APP_STAGE_LABEL": JSON.stringify(appStageLabel), + "import.meta.env.APP_VERSION": JSON.stringify(pkg.version), + }, + resolve: { + tsconfigPaths: true, + }, + server: { host, + port, + strictPort: true, + ...(devProxyTarget + ? { + proxy: { + "/.well-known": { + target: devProxyTarget, + changeOrigin: true, + }, + "/api": { + target: devProxyTarget, + changeOrigin: true, + }, + "/attachments": { + target: devProxyTarget, + changeOrigin: true, + }, + }, + } + : {}), + hmr: { + // Explicit config so Vite's HMR WebSocket connects reliably + // inside Electron's BrowserWindow. Vite 8 uses console.debug for + // connection logs — enable "Verbose" in DevTools to see them. + protocol: "ws", + host, + }, + }, + build: { + outDir: "dist", + emptyOutDir: true, + sourcemap: buildSourcemap, }, - }, - build: { - outDir: "dist", - emptyOutDir: true, - sourcemap: buildSourcemap, - }, + }; }); diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 97d46451ae..86a36e7993 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -475,9 +475,9 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( mockUpdateServerPort: string | undefined, ) { const buildConfig: Record = { - appId: "com.t3tools.t3code", + appId: "com.h3tools.h3code", productName, - artifactName: "T3-Code-${version}-${arch}.${ext}", + artifactName: "H3Code-${version}-${arch}.${ext}", directories: { buildResources: "apps/desktop/resources", }, @@ -505,12 +505,12 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( if (platform === "linux") { buildConfig.linux = { target: [target], - executableName: "t3code", + executableName: "h3code", icon: "icon.png", category: "Development", desktop: { entry: { - StartupWMClass: "t3code", + StartupWMClass: "h3code", }, }, }; @@ -617,7 +617,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( const commitHash = resolveGitCommitHash(repoRoot); const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped; const stageRoot = yield* mkdir({ - prefix: `t3code-desktop-${options.platform}-stage-`, + prefix: `h3code-desktop-${options.platform}-stage-`, }); const stageAppDir = path.join(stageRoot, "app"); @@ -671,18 +671,18 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( yield* fs.copy(stageResourcesDir, path.join(stageAppDir, "apps/desktop/prod-resources")); const stagePackageJson: StagePackageJson = { - name: "t3code", + name: "h3code", version: appVersion, buildVersion: appVersion, t3codeCommitHash: commitHash, private: true, - description: "T3 Code desktop build", + description: "H3Code desktop build", author: "T3 Tools", main: "apps/desktop/dist-electron/main.js", build: yield* createBuildConfig( options.platform, options.target, - desktopPackageJson.productName ?? "T3 Code", + desktopPackageJson.productName ?? "H3Code", options.signed, options.mockUpdates, options.mockUpdateServerPort,