From 574e78a16ff59f2ac07b74489b7d2197f5571ebe Mon Sep 17 00:00:00 2001 From: bnpne Date: Fri, 15 May 2026 12:30:13 -0700 Subject: [PATCH] Personal fork: rebrand home dir to .bnpnecode, default to worktree, copy .env* into new worktrees - Home directory default changed from ~/.t3 to ~/.bnpnecode across dev runner, desktop fallback, SSH remote launcher, .gitignore, and tests. T3CODE_HOME env var name is unchanged so existing overrides still work. - resolveEffectiveEnvMode now defaults brand-new draft threads to "worktree" instead of "local". Behavior when already inside a worktree is unchanged. - After git worktree add succeeds, root-level .env* files are copied from the source repo into the new worktree so threads inherit env vars. Copy failures are caught per-file and never fail worktree creation. - Add BUILD.md describing personal-build + GitHub-auto-update flow. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 2 +- BUILD.md | 231 ++++++++++++++++++ apps/desktop/src/app/DesktopEnvironment.ts | 2 +- apps/server/src/vcs/GitVcsDriverCore.ts | 21 ++ .../web/src/components/BranchToolbar.logic.ts | 2 +- packages/ssh/src/tunnel.ts | 8 +- scripts/dev-runner.test.ts | 4 +- scripts/dev-runner.ts | 2 +- 8 files changed, 262 insertions(+), 10 deletions(-) create mode 100644 BUILD.md diff --git a/.gitignore b/.gitignore index 5e941c7b9f0..f25d4949d1a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ build/ .logs/ release/ release-mock/ -.t3 +.bnpnecode .idea/ apps/web/.playwright apps/web/playwright-report diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 00000000000..2d67b3e31ae --- /dev/null +++ b/BUILD.md @@ -0,0 +1,231 @@ +# BUILD.md — Personal Desktop Build with GitHub Auto-Updates + +This guide walks through building T3 Code as a personal desktop app (`.dmg` on macOS) wired to **your own GitHub repository** for auto-updates. The app will check your repo's Releases on startup and let you download/install updates from inside the app. + +> **Replace `OWNER/REPO` everywhere below with your GitHub `username/repo-name`** (e.g. `bnpne/ottawa` or wherever your fork lives). + +--- + +## How auto-update works here + +- `electron-updater` is wired into the desktop app (`apps/desktop/src/updates/DesktopUpdates.ts`). +- At **build time**, the `T3CODE_DESKTOP_UPDATE_REPOSITORY` env var is baked into the artifact's `app-update.yml` via `scripts/build-desktop-artifact.ts:514` → `resolveGitHubPublishConfig`. +- At **runtime**, the app checks `https://github.com/OWNER/REPO/releases/latest` for a `latest-mac.yml` (or `latest.yml` / `latest-linux.yml`). +- Update UX is manual: a rocket icon appears in the UI; click once to download, click again to restart & install. No silent auto-install. +- Required Release assets: + - the installer (`.dmg`, `.exe`, or `.AppImage`) + - macOS also needs the `.zip` (Squirrel.Mac uses it for the update payload) + - `latest-mac.yml` / `latest.yml` / `latest-linux.yml` (channel metadata) + - `*.blockmap` (differential downloads) + +--- + +## Prerequisites + +1. Bun `^1.3.11`, Node `^24.13.1` (`bun --version`, `node --version`). +2. A GitHub repo you own (fork or otherwise) to host releases — call it `OWNER/REPO`. +3. `gh` CLI installed and authenticated (`gh auth status`). Used to create the GitHub Release and upload assets. +4. (macOS only, optional but recommended) Apple Developer ID certificate if you want updates to install without a Gatekeeper prompt. See "macOS signing caveat" below. + +--- + +## 1. One-time setup + +### Make sure your repo has a `main` remote + +```bash +# from inside the repo root +git remote -v +# expect: origin git@github.com:OWNER/REPO.git ... +``` + +If you forked from `pingdotgg/t3code`, add your fork as a remote and push: + +```bash +git remote add personal git@github.com:OWNER/REPO.git +git push personal HEAD:main +``` + +### Install dependencies + +```bash +bun install +``` + +--- + +## 2. Build the `.dmg` (Apple Silicon) + +The build command bakes your repo into the artifact's auto-update config. + +```bash +export T3CODE_DESKTOP_UPDATE_REPOSITORY="OWNER/REPO" + +# Apple Silicon +bun dist:desktop:dmg:arm64 + +# OR Intel Mac +bun dist:desktop:dmg:x64 + +# OR both archs in one DMG (universal) +bun dist:desktop:dmg +``` + +Other targets if you ever need them: + +```bash +bun dist:desktop:linux # Linux x64 AppImage +bun dist:desktop:win # Windows NSIS installer +``` + +Artifacts land in `./release/`. For an arm64 build at version `0.0.24` you should see: + +``` +release/ +├── T3-Code-0.0.24-arm64.dmg +├── T3-Code-0.0.24-arm64.dmg.blockmap +├── T3-Code-0.0.24-arm64-mac.zip +├── T3-Code-0.0.24-arm64-mac.zip.blockmap +└── latest-mac.yml +``` + +All of those need to go into the GitHub Release. + +> The version comes from `apps/server/package.json` `version` field. Bump it there (e.g. `0.0.25`) before each new build so the updater sees a newer version. + +--- + +## 3. Create the GitHub Release + +The version tag must match the version inside the build. With `version = 0.0.24`: + +```bash +export VERSION="0.0.24" + +git tag "v${VERSION}" +git push origin "v${VERSION}" + +gh release create "v${VERSION}" \ + --repo OWNER/REPO \ + --title "v${VERSION}" \ + --notes "Personal build" \ + ./release/T3-Code-${VERSION}-arm64.dmg \ + ./release/T3-Code-${VERSION}-arm64.dmg.blockmap \ + ./release/T3-Code-${VERSION}-arm64-mac.zip \ + ./release/T3-Code-${VERSION}-arm64-mac.zip.blockmap \ + ./release/latest-mac.yml +``` + +> **Important:** the release tag must be in the `vX.Y.Z` format and must **not** be marked as a prerelease, otherwise the `latest` channel won't pick it up. (Tags with suffixes like `-nightly.*` go to the `nightly` channel — keep things on plain `vX.Y.Z` unless you want that.) + +--- + +## 4. Install the app + +Open the `.dmg` from `./release/` and drag T3 Code into Applications. + +First launch on macOS will be blocked by Gatekeeper because the build is unsigned. Either: + +```bash +# Remove the quarantine flag +xattr -dr com.apple.quarantine "/Applications/T3 Code.app" +``` + +…or right-click the app → **Open** → confirm the warning once. + +--- + +## 5. Shipping an update + +When you want to push an update: + +1. Bump version in `apps/server/package.json` (e.g. `0.0.24` → `0.0.25`). +2. Commit and push. +3. Rebuild: + ```bash + export T3CODE_DESKTOP_UPDATE_REPOSITORY="OWNER/REPO" + bun dist:desktop:dmg:arm64 + ``` +4. Tag and create the Release: + ```bash + export VERSION="0.0.25" + git tag "v${VERSION}" + git push origin "v${VERSION}" + gh release create "v${VERSION}" --repo OWNER/REPO --title "v${VERSION}" --notes "" \ + ./release/T3-Code-${VERSION}-arm64.dmg \ + ./release/T3-Code-${VERSION}-arm64.dmg.blockmap \ + ./release/T3-Code-${VERSION}-arm64-mac.zip \ + ./release/T3-Code-${VERSION}-arm64-mac.zip.blockmap \ + ./release/latest-mac.yml + ``` +5. Open the installed T3 Code app. Within the startup check interval it will detect the update and show the rocket icon in the UI. Click it to download, click again to restart & install. + +--- + +## Private-repo auth + +If `OWNER/REPO` is private, the desktop app needs a token to read your releases. Set this in the runtime environment **of the installed app** (not the build): + +```bash +# in your shell rc / launchd plist / however you launch the app +export T3CODE_DESKTOP_UPDATE_GITHUB_TOKEN="ghp_xxx" # or GH_TOKEN=... +``` + +The app forwards it as `Authorization: Bearer ` on updater HTTP calls (see `docs/release.md:111-113`). + +--- + +## macOS signing caveat (read this if updates fail to install) + +Unsigned macOS apps can be **installed manually**, but `electron-updater`'s install step uses Squirrel.Mac, which **requires a code-signed app** to apply updates. With no signing: + +- The update will download. +- Clicking "install" will fail silently or kick you back to a manual reinstall. + +Options: + +1. **Just reinstall manually each time** — simplest. Skip auto-install entirely; download the new `.dmg` from your Releases page. +2. **Self-sign with an ad-hoc identity** — not enough; Squirrel.Mac still rejects ad-hoc. +3. **Use a real Developer ID** — needed for true in-app auto-install. If you have an Apple Developer account, set these env vars before building and pass `--signed`: + ```bash + export CSC_LINK="$(base64 < /path/to/cert.p12)" + export CSC_KEY_PASSWORD="..." + export APPLE_API_KEY="$(cat AuthKey_XXX.p8)" + export APPLE_API_KEY_ID="XXX" + export APPLE_API_ISSUER="UUID" + node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch arm64 --signed + ``` + +For a personal-use build, option (1) is the realistic path: auto-update can still **notify** you and **download** the new version even unsigned — you just finish the install by mounting the DMG yourself. + +--- + +## Quick reference: env vars + +| Variable | Where | Purpose | +| --- | --- | --- | +| `T3CODE_DESKTOP_UPDATE_REPOSITORY` | build-time | Baked into `app-update.yml`. Format: `owner/repo`. | +| `T3CODE_DESKTOP_UPDATE_GITHUB_TOKEN` | runtime | Token for private repo updater HTTP. | +| `GH_TOKEN` | runtime | Fallback if `T3CODE_DESKTOP_UPDATE_GITHUB_TOKEN` is not set. | +| `T3CODE_DESKTOP_VERSION` | build-time | Override version baked into artifact. | +| `T3CODE_DESKTOP_OUTPUT_DIR` | build-time | Override output dir (defaults to `release/`). | +| `T3CODE_DESKTOP_VERBOSE` | build-time | Stream subprocess stdout for debugging. | + +--- + +## Build commands cheat sheet + +```bash +# Local install only — no installer +bun build:desktop +bun start:desktop + +# Build a .dmg for personal use (Apple Silicon) +T3CODE_DESKTOP_UPDATE_REPOSITORY=OWNER/REPO bun dist:desktop:dmg:arm64 + +# Cut a release +gh release create vX.Y.Z --repo OWNER/REPO ./release/* + +# Dev mode (Electron + hot reload, not packaged) +bun dev:desktop +``` diff --git a/apps/desktop/src/app/DesktopEnvironment.ts b/apps/desktop/src/app/DesktopEnvironment.ts index a5212f25358..5efa63bda63 100644 --- a/apps/desktop/src/app/DesktopEnvironment.ts +++ b/apps/desktop/src/app/DesktopEnvironment.ts @@ -151,7 +151,7 @@ const makeDesktopEnvironment = Effect.fn("desktop.environment.make")(function* ( : input.platform === "darwin" ? path.join(homeDirectory, "Library", "Application Support") : Option.getOrElse(config.xdgConfigHome, () => path.join(homeDirectory, ".config")); - const baseDir = Option.getOrElse(config.t3Home, () => path.join(homeDirectory, ".t3")); + const baseDir = Option.getOrElse(config.t3Home, () => path.join(homeDirectory, ".bnpnecode")); const rootDir = path.resolve(input.dirname, "../../.."); const appRoot = input.isPackaged ? input.appPath : rootDir; const branding = resolveDesktopAppBranding({ diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 19f12862dad..042ef34a300 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -1884,6 +1884,8 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* fallbackErrorMessage: "git worktree add failed", }); + yield* copyGitignoredEnvFiles(input.cwd, worktreePath); + return { worktree: { path: worktreePath, @@ -1892,6 +1894,25 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }; }); + const copyGitignoredEnvFiles = Effect.fn("copyGitignoredEnvFiles")(function* ( + sourceCwd: string, + worktreePath: string, + ) { + const entries: ReadonlyArray = yield* fileSystem + .readDirectory(sourceCwd) + .pipe(Effect.catch(() => Effect.succeed>([]))); + const envEntries = entries.filter((entry: string) => entry.startsWith(".env")); + for (const entry of envEntries) { + const from = path.join(sourceCwd, entry); + const to = path.join(worktreePath, entry); + const stat = yield* fileSystem + .stat(from) + .pipe(Effect.catch(() => Effect.succeed(null))); + if (!stat || stat.type !== "File") continue; + yield* fileSystem.copyFile(from, to).pipe(Effect.catch(() => Effect.void)); + } + }); + const fetchPullRequestBranch: GitVcsDriver.GitVcsDriverShape["fetchPullRequestBranch"] = Effect.fn("fetchPullRequestBranch")(function* (input) { const remoteName = yield* resolvePrimaryRemoteName(input.cwd); diff --git a/apps/web/src/components/BranchToolbar.logic.ts b/apps/web/src/components/BranchToolbar.logic.ts index 65388962c08..4eb84f75b44 100644 --- a/apps/web/src/components/BranchToolbar.logic.ts +++ b/apps/web/src/components/BranchToolbar.logic.ts @@ -64,7 +64,7 @@ export function resolveEffectiveEnvMode(input: { if (activeWorktreePath) { return "local"; } - return draftThreadEnvMode === "worktree" ? "worktree" : "local"; + return draftThreadEnvMode === "local" ? "local" : "worktree"; } return activeWorktreePath ? "worktree" : "local"; } diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 5ee5c684779..b86d1099a78 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -465,8 +465,8 @@ exit 1 export const REMOTE_LAUNCH_SCRIPT = `set -eu @@T3_NODE_ENV_SCRIPT@@ STATE_KEY="$1" -STATE_DIR="$HOME/.t3/ssh-launch/$STATE_KEY" -DEFAULT_SERVER_HOME="$HOME/.t3" +STATE_DIR="$HOME/.bnpnecode/ssh-launch/$STATE_KEY" +DEFAULT_SERVER_HOME="$HOME/.bnpnecode" DEFAULT_RUNTIME_FILE="$DEFAULT_SERVER_HOME/userdata/server-runtime.json" PORT_FILE="$STATE_DIR/port" PID_FILE="$STATE_DIR/pid" @@ -618,8 +618,8 @@ printf '{"remotePort":%s,"serverKind":"%s"}\\n' "$REMOTE_PORT" "\${REMOTE_MANAGE `; export const REMOTE_PAIRING_SCRIPT = `set -eu -STATE_DIR="$HOME/.t3/ssh-launch/@@T3_STATE_KEY@@" -DEFAULT_SERVER_HOME="$HOME/.t3" +STATE_DIR="$HOME/.bnpnecode/ssh-launch/@@T3_STATE_KEY@@" +DEFAULT_SERVER_HOME="$HOME/.bnpnecode" RUNNER_FILE="$STATE_DIR/run-t3.sh" mkdir -p "$STATE_DIR" cat >"$RUNNER_FILE" <<'SH' diff --git a/scripts/dev-runner.test.ts b/scripts/dev-runner.test.ts index 724b328c4c6..7d81fc59f3d 100644 --- a/scripts/dev-runner.test.ts +++ b/scripts/dev-runner.test.ts @@ -47,7 +47,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { }); describe("createDevRunnerEnv", () => { - it.effect("defaults T3CODE_HOME to ~/.t3 when not provided", () => + it.effect("defaults T3CODE_HOME to ~/.bnpnecode when not provided", () => Effect.gen(function* () { const path = yield* Path.Path; const env = yield* createDevRunnerEnv({ @@ -64,7 +64,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { devUrl: undefined, }); - assert.equal(env.T3CODE_HOME, path.resolve(NodeOS.homedir(), ".t3")); + assert.equal(env.T3CODE_HOME, path.resolve(NodeOS.homedir(), ".bnpnecode")); }), ); diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts index 58bbb1ac35e..b23b010b079 100644 --- a/scripts/dev-runner.ts +++ b/scripts/dev-runner.ts @@ -25,7 +25,7 @@ const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1"; const DEV_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::1", "::"] as const; export const DEFAULT_T3_HOME = Effect.map(Effect.service(Path.Path), (path) => - path.join(NodeOS.homedir(), ".t3"), + path.join(NodeOS.homedir(), ".bnpnecode"), ); const MODE_ARGS = {