Skip to content
Source
Sign inSign up
Open History to see the last commit

development.md

360 lines18.2 KB

1# Development
2​
3## First checkout
4​
5T3 Code uses [Vite+](https://viteplus.dev/guide/). Install its global `vp` command:
6​
7```sh
8curl -fsSL https://vite.plus | bash
9```
10​
11On Windows, in PowerShell:
12​
13```powershell
14irm https://vite.plus/ps1 | iex
15```
16​
17`vp` provides the Node version pinned in `package.json` (Node 26), so no separate Node install is
18needed. From the repository root:
19​
20```sh
21vp i
22vp run dev
23```
24​
25Open the pairing URL printed by the dev runner. The bare origin does not authenticate
26a new browser.
27​
28Prefer a container? See [Dev container](../internals/devcontainer.md) for VS Code and Codespaces setup.
29​
30## Forge mirror (optional)
31​
32GitHub is the source of truth. [source.tanstack.com/@pingdotgg/t3code](https://source.tanstack.com/@pingdotgg/t3code)
33is a two-way synced mirror built for many agents working at once. You never need it to contribute.
34​
35```sh
36git remote set-url origin https://source.tanstack.com/@pingdotgg/t3code.git
37git remote add github https://github.com/pingdotgg/t3code.git
38src auth login --web && src auth setup-git
39```
40​
41With `origin` on the mirror, use `src` (it takes `gh`'s commands and flags) and its `source` agent
42skill for PRs, issues, reviews, and CI, and leave GitHub's API alone. Pull requests into `main`
43merge on GitHub, so GitHub's required checks and reviews still apply. Numbers can differ between
44the two forges; in text that reaches GitHub, use the GitHub number.
45​
46## Choosing a dev process
47​
48Use `vp run dev` for server and web, or `vp run dev:desktop` for the Electron client.
49`dev:server` and `dev:web` start those processes separately.
50See the [mobile README](../../apps/mobile/README.md) for native builds and Metro.
51​
52Flags go directly after the task name, for example `vp run dev --home-dir /tmp/t3code-dev`.
53Add `--browser` to open a browser automatically.
54​
55To stop a worktree's dev stack when its thread settles, import the **Dev** action from
56`t3.json` in Settings > Projects > Actions and start it through the action menu or its
57keybinding. Its `stopOnSettle` option lets T3 stop the terminal and its child processes.
58Running `vp run dev` through an agent's own shell tool does not attach it to this lifecycle.
59​
60### State and ports
61​
62Linked worktrees default to their own `.t3/userdata`, even when `T3CODE_HOME` is set.
63The main checkout defaults to `~/.t3/dev`. An explicit `--home-dir` wins in both cases.
64The first start in a worktree seeds its empty database from a read-only copy of
65`~/.t3/userdata/statev2.sqlite` if the source is at most 500 MiB. Larger sources warn
66and dev starts with an empty database. `--no-seed` or `T3CODE_DEV_SEED=0` skips seeding.
67To copy a larger source, run `vp run migrate-dev-db` with dev stopped. It may use
68substantial disk space before pruning.
69Never run a development server against the live `~/.t3/userdata`.
70See [test data](../../AGENTS.md#test-data) for copying a consistent database snapshot.
71​
72Read ports from the `[dev-runner]` output. Worktrees derive stable preferences from their paths,
73but occupied ports can shift them. `T3CODE_PORT_OFFSET` or `T3CODE_DEV_INSTANCE` can select a
74different preference when needed.
75​
76### Replay provider
77​
78`vp run dev` registers `Replay (dev)`, a local ACP agent at
79[dev-replay-agent.ts](../../apps/server/scripts/dev-replay-agent.ts). It plays recorded
80ACP transcripts with simulated streaming, so the real adapter, orchestration, and UI run
81without a paid model. Each scenario is a model, and each prompt plays the next recorded turn.
82Bundled servers, including desktop dev, skip it because they have no agent script.
83Dev servers add it back on start, so turn it off in Settings instead of removing it.
84To add a scenario, point a `SCENARIOS` entry at a generic ACP replay fixture
85(`registry_transcript.ndjson`) or a file in [dev-replay](../../apps/server/scripts/dev-replay).
86The agent replays session updates, permission requests, and questions. It drops session setup
87and vendor frames, so Grok and fx recordings lose their vendor-only behavior.
88​
89### Profiling dev startup
90​
91Run `vp run profile:startup` to capture the first 20 seconds of inherited Node processes under
92`.t3/startup-profiles/<timestamp>`. Workers that execute the preload get separate profiles.
93It starts the normal dev runner and accepts its flags.
94Stop it with Ctrl+C after the capture. Set `T3CODE_STARTUP_PROFILE_DIR` to choose another output
95directory. Profiling adds overhead, so compare readiness with an ordinary `vp run dev` run.
96​
97Open a `.cpuprofile` in Chrome DevTools' Performance panel for sampled function stacks and source
98lines. Each `.modules.json` is a Chrome trace containing timed module resolution, loading,
99synchronous subprocess calls, and startup measures. Per-file records include importer URLs,
100source loading, preparation, recursive dependency linking, and top-level evaluation. Preparation
101uses synchronous loader boundaries; linking includes dependencies, and CommonJS evaluation can
102include nested `require` calls. Some ESM/CommonJS interop reports a combined stage. `.process.json` records the process, Node version, and capture window.
103The server's nested Effect and SQL spans remain in `.t3/userdata/logs/server.trace.ndjson`.
104​
105The preload temporarily adds module-body timing markers and uses one automatic debugger pause
106to instrument the live Node loader. The launcher adds `--no-node-snapshot` to inherited
107`NODE_OPTIONS` because Node 26's snapshotted loader cannot accept that breakpoint. Include the
108same flag when using the preload directly. This disables Node's built-in startup snapshot,
109can change compilation-cache hits, and adds overhead.
110Treat per-file captures as diagnostic runs; use uninstrumented runs for baseline startup latency.
111The first entry module can compile before loader instrumentation is installed.
112​
113A fresh process still uses filesystem, Node compilation, provider, and Vite dependency caches.
114Measure a missing Vite dependency cache separately from a restart with an existing database.
115Neither the Vite ready message nor the HTTP listener proves that a browser has rendered the app.
116​
117### Sharing and remote debugging
118​
119`vp run dev --share` publishes the web port over the machine's tailnet and prints a pairing URL
120for that origin. Give the tester the complete URL, including its token. The dev runner removes
121its mapping on exit.
122​
123Leave `VITE_HTTP_URL` and `VITE_WS_URL` unset. Vite proxies the backend through the browser's
124origin so the same build works over localhost and remote connections.
125​
126Shared runs enable bundled dev to avoid a network round trip for each import level.
127`T3CODE_BUNDLED_DEV=0` opts out when debugging bundler differences. These traps matter
128when changing this setup:
129​
130- The web entry must dynamically import the app so React refresh initializes before application
131 chunks. Static imports can work on first load and fail after a route split.
132- Bundled dev rebuilds Tailwind through watched files. Its ordinary Vite hot-update hook expects
133 a server/module graph that Rolldown does not provide.
134- The app and its routes are lazy chunks, built on first request. A plugin pre-bundles the
135 startup chain when the server starts. Renaming or moving a startup route or the app entry
136 silently drops it from that list.
137- Bundled dev and `--built` hand Rolldown its own watcher, which skips Vite's default
138 `node_modules` ignore. Those files are pnpm-store hardlinks, so another worktree's install or
139 removal rebuilds every long-running server and leaks memory. Keep `node_modules` excluded.
140​
141The workarounds live in the [web entry](../../apps/web/src/bootstrap.ts),
142[Tailwind plugin](../../apps/web/vite/tailwind.ts), and
143[startup warmup](../../apps/web/vite/warmBundledDev.ts).
144​
145`vp run dev --share --built` skips the Vite dev server. It runs `vp build --watch` for the web
146app, and the backend serves `apps/web/dist` on the same tailnet URL and pairing flow. Use it when
147someone tests over a slow link or only needs to use the app: loads are production size, and the
148first load does not wait for dev bundling. There is no HMR. After an edit, wait for the
149`built in` line (about 6 s), then reload. Until the first build exists, the URL shows a page that
150reloads by itself. The license manifest is not generated in this mode. `--built` requires
151`--share` and the `dev` mode.
152​
153#### Reusable dev credential
154​
155Use this only on a hostname where you trust every service. Browsers send cookies to all ports
156on that hostname. Any service you visit there can receive the reusable admin credential,
157including services unrelated to T3 Code. If you run untrusted services on that hostname, keep
158normal per-environment pairing instead.
159​
160To use one browser profile across web dev worktrees on the same hostname, generate one fixed
161value once:
162​
163```sh
164openssl rand -hex 32
165```
166​
167Put that value in the main checkout's gitignored `.env`:
168​
169```dotenv
170T3CODE_DEV_AUTH_TOKEN=<the value generated above>
171```
172​
173The `t3.json` Setup Worktree action links that file to each worktree's `.env`. The dev runner reads repository env files at startup. `.env.local` and inherited process
174environment values override `.env`, so no per-worktree export is needed after setup.
175​
176For a manual worktree or launcher without that link, export the same fixed value instead:
177​
178```sh
179export T3CODE_DEV_AUTH_TOKEN="<the value generated above>"
180```
181​
182Do not generate a new value at startup. Start or restart `vp run dev --share` after configuration,
183then open its printed startup pairing URL once per browser profile on that hostname. Later web dev
184servers on the same hostname accept the shared cookie across ports. The cookie expires after 30
185days. Reload an old tab if its URL now serves a replacement environment.
186​
187The token and startup pairing URLs are reusable administrative secrets. Never put them in a
188commit, pull request, or public output. Every server still seeds its own auth database record at
189startup and keeps its own SQLite data, signing key, and revocation state. Desktop and non-dev
190servers ignore the value. See [environment authentication](../internals/environment-auth.md#reusable-dev-credential)
191for the security model.
192​
193## Checks
194​
195`vp run check:changed` runs CI's fast checks for your branch's changes: format and lint of the
196changed files, `knip:check` when code or a manifest changes, the generated GraphQL checks, typecheck
197of the affected packages and their dependents, and the test files beside the changed files. It also
198runs the checks that read other files: the agent context budget when the server is affected, and
199React Compiler coverage for only the changed web and mobile files. It maps
200files to packages with [ci-changes.cjs](../../.github/scripts/ci-changes.cjs), as CI does. It
201compares against the closest merge base with any remote's `main`, so a fork whose `main` lags
202upstream still compares against upstream, or with a ref you pass. CI still runs the full
203test suites and the jobs the summary lists, so a local pass does not promise a green CI run.
204​
205The steps run side by side, as many at once as the machine allows: one per two cores and per 6 GiB
206of available memory. On a large machine a run takes about as long as its slowest step, usually
207knip's exports pass at about 10 s. Pass `--concurrency <n>` or set `CHECK_CHANGED_CONCURRENCY` to
208change the limit (`1` runs the steps one by one). A step that runs past `--timeout <seconds>` or
209`CHECK_CHANGED_TIMEOUT` (default 900) is killed with every process it started and reported as
210`TIMEOUT`, not as a failing check. When the files are exactly those of the last passing run, it prints that
211run's summary instead of running again. Any change to a file, the install, or a generated file runs
212the checks again.
213​
214While you iterate, run single checks:
215​
216```sh
217vp test run <files>
218vp lint <files>
219vp run --filter <package> typecheck
220```
221​
222In `apps/web`, a test that renders React or touches the DOM is named `*.browser.test.{ts,tsx}`
223and runs in headless Chromium with the React Compiler on, like the app. There is no jsdom; other
224`*.test.ts` files run in Node.
225​
226TypeScript workspaces typecheck with `effect-tsgo tsrs --noEmit` from `@effect/tsgo`: the
227[tsrs](https://github.com/oxc-project/tsrs) checker with Effect diagnostics, quick fixes, and
228refactors built in. Only `apps/marketing` installs `typescript`, for Astro. VS Code's TypeScript 7
229extension starts the same checker through `.vscode/tsdk/tsgo` (`js/ts.tsdk.path`), a POSIX
230script, so on Windows open the workspace through WSL.
231​
232Use `vp run lint:mobile` for native mobile changes. CI owns the full suite; see
233[ci.yml](../../.github/workflows/ci.yml) for its current jobs.
234The [manual Windows lane](../../.github/workflows/windows-tests.yml) is available for focused
235Windows investigation while that suite is not a required gate.
236​
237### Unused code
238​
239`vp run knip:check` checks unused files and dependencies across the repo and, at the same time,
240unused runtime exports in `apps/server`, `apps/desktop`, `apps/web`, and every internal package under
241`packages/`. CI enforces both checks.
242Exported types and Effect schemas are allowed without consumers. The schema preprocessor
243recognizes schema types, including aliases and schema classes; functions that create or decode
244schemas remain checked. Canonical Effect service construction APIs stay exported with an explicit
245`@public` annotation, which Knip recognizes. Completely unused files remain checked too.
246Named exports in web UI component modules are kept as complete component sets. Knip ignores
247unused exports in `apps/web/src/components/ui/*.tsx`, while still reporting an entire unused file.
248Use `vp run knip --workspace apps/web` to audit one workspace, including exports,
249or `vp run knip:production --workspace apps/web` to find code kept alive only by tests.
250The full export audit still has findings and is not a repo-wide CI gate. Extend the
251export check's workspace selectors as more workspaces become clean. Review callers before
252deleting code; production mode can also report development scripts and test fixtures.
253Runtime-discovered entrypoints and dependency exceptions belong in [knip.jsonc](../../knip.jsonc).
254​
255## Desktop artifacts
256​
257Local artifact builds are unsigned by default and write to `release/`:
258​
259```sh
260vp run dist:desktop:dmg
261vp run dist:desktop:linux
262vp run dist:desktop:win
263```
264​
265`dist:desktop:linux` targets x64. For a native Linux ARM64 build, run:
266​
267```sh
268vp run dist:desktop:artifact --platform linux --target AppImage --arch arm64
269```
270​
271DMGs default to the host architecture. Use `--arch` to choose another target and `--keep-stage`
272to retain packaging files for inspection. Run `vp run dist:desktop:artifact --help` for other
273options.
274​
275### Prepared server bytecode
276​
277Native macOS and Windows desktop artifact builds, and native Linux x64 and ARM64 builds, generate server
278bytecode with the packaged Electron executable, after ASAR creation and before signing. Generation imports the CLI and server
279module declarations without launching server services; worker-only modules compile normally.
280AppImage backends use temporary links for each mount and remove them on normal exit. Later launches
281reclaim abandoned cache directories whose links point to gone mounts; AppImage main-process caching
282stays disabled. macOS x64 builds on ARM64 hosts run the packaged x64 Electron
283under Rosetta to generate the cache. Other cross-platform and cross-architecture builds skip
284generation explicitly. CLI, SEA and WSL runtimes keep their existing behavior.
285​
286Electron's Node 24 cache has no read-only option and includes the producer UID in its directory
287name. The backend links individual prepared files into its writable cache with keys for the
288installed module paths. Node replaces files atomically, so regeneration cannot write through those
289links into signed resources. Windows uses hard links and falls back to normal caching when volumes
290differ or resource permissions prevent linking. Protected installations therefore may not reuse
291prepared bytecode. No cache bytes are copied during startup.
292​
293The key calculation follows Node's internal portable ESM format. Generation checks that the
294packaged engine produced the expected CLI and server entries and fails the build if that format changes.
295The manifest also checks Electron, Node, OS, architecture and V8's flags-sensitive version tag.
296Missing or rejected entries compile from the shipped JavaScript. Prepared backends skip the
297explicit startup flush; Node can persist missing entries on normal exit. The writable fallback
298keeps the explicit flush needed when Windows force-kills the backend.
299​
300### Linux AppImage prerequisites
301​
302Build on Linux because the browser-secret helper links against the host's libsecret. Install
303C/C++ build tools, libsecret development headers, pkg-config, and ImageMagick.
304Bytecode preparation also runs packaged Electron and requires GTK, NSS, ALSA, and GBM runtime libraries.
305Install Rust 1.95 or newer through [rustup](https://rust-lang.org/install), using the stable
306toolchain as release CI does.
307​
308Ubuntu 24.04 and Debian 13:
309​
310```sh
311sudo apt-get update
312sudo apt-get install build-essential libsecret-1-dev pkg-config imagemagick libgtk-3-0t64 libnss3 libasound2t64 libgbm1
313```
314​
315On Ubuntu 22.04 and Debian 12, use `libgtk-3-0` and `libasound2` instead of the `t64` packages.
316​
317Fedora:
318​
319```sh
320sudo dnf install gcc gcc-c++ make libsecret-devel pkgconf-pkg-config ImageMagick gtk3 nss alsa-lib mesa-libgbm
321```
322​
323Arch Linux:
324​
325```sh
326sudo pacman -S base-devel libsecret pkgconf imagemagick gtk3 nss alsa-lib mesa
327```
328​
329The C toolchain, pkg-config, and libsecret headers are also needed for Linux desktop development.
330​
331### macOS DMG prerequisites
332​
333Install the Xcode Command Line Tools with `xcode-select --install` and install Rust.
334For a cross-architecture or universal build, add the requested Rust targets:
335​
336```sh
337rustup target add aarch64-apple-darwin x86_64-apple-darwin
338```
339​
340### Windows installer prerequisites
341​
342Install Rust, Python 3, and Visual Studio Build Tools with **Desktop development with C++**.
343Include the Windows SDK and the MSVC build tools and Spectre-mitigated libraries for the target
344architecture. Add its Rust target:
345​
346```powershell
347rustup target add x86_64-pc-windows-msvc
348# For an ARM64 installer:
349rustup target add aarch64-pc-windows-msvc
350```
351​
352NSIS is downloaded by electron-builder. WSL support additionally needs the Linux CLI archive
353passed as `--wsl-runtime`; see the
354[release runbook](./release.md#windows-payload-topology-and-update-validation).
355​
356### Signing and passkeys
357​
358Add `--signed` after configuring the platform credentials in the
359[release runbook](./release.md). macOS passkeys need a signed, provisioned app; follow the
360[Connect setup](./connect-setup.md#desktop-passkeys) for local signing and renderer HMR.