Worktrees, env files, databases and dev servers for working on ToolJet. TypeScript run straight from source on the .nvmrc Node: no build step, no runtime dependencies.
tools/tj/bin/tj help # all commands; `tj <command> --help` for oneTo type just tj, put this launcher on your PATH. It runs the copy in whichever checkout or worktree you're in, so each branch uses its own version, and it works in non-interactive shells too:
mkdir -p ~/.local/bin && cat > ~/.local/bin/tj <<'EOF'
#!/bin/sh
root=$(git rev-parse --show-superproject-working-tree --show-toplevel 2>/dev/null | head -1)
[ -x "$root/tools/tj/bin/tj" ] || { echo "tj: not inside a ToolJet checkout" >&2; exit 2; }
exec "$root/tools/tj/bin/tj" "$@"
EOF
chmod +x ~/.local/bin/tj # ~/.local/bin must be on PATHDon't add tools/tj/bin itself to PATH. That pins one checkout's copy, and inside a worktree you'd silently run the wrong version. Scripts and coding agents call tools/tj/bin/tj from the repo root, which needs no setup.
tj doctor # prerequisites, with a fix hint for each failure
tj wt add feat/x --app # worktree + submodules + deps + test & dev DBs + free ports
cd "$(tj wt path feat/x)"
tj start # server + frontend in the background, waits for health
tj status && tj logs server -f
tj stop
tj wt rm feat/x --yes # stop, drop its DBs, remove the worktree (refuses on unsaved work)
tj db migrate --test # migrate the test DB (NODE_ENV=test alone is a no-op)- stdout is data. With
--jsonit's one document,{ "schemaVersion": 1, "ok": true, ... }. Errors haveok: falseanderror: { message, hint, exitCode }. - stderr is progress.
- On a terminal:
› step,✔ ok,⚠ warn,✖ fail,· info,→ hint. - When piped, in CI, or under a coding agent:
step:,ok:,warn:,fail:,info:,hint:lines with no colour. NO_COLORis respected.
- On a terminal:
- Exit codes:
0ok,1failed,2usage,3not ready (health timeout). - Never prompts without a terminal. Destructive commands need
--yesthere. - Subprocess output goes to
.tj/logs/<name>.log, or stderr with--verbose. On failure the log tail is shown.
Per checkout, in .tj/ (gitignored): state.json (ports, DB names, dependency hashes), run/<svc>.json (pid, port), logs/.
- Worktree layout:
- Worktrees live in
.worktrees/<slug>_<hash>. - DB names are
tooljet_<slug>_<hash>[_test]. They are unique per branch and stay under Postgres' 63-char limit, even with the e2e runner's shard suffix.
- Worktrees live in
- Submodules: a worktree clones them from the main checkout's local module repos, so unpushed EE branches are visible.
- Dependencies: before running
npm ci, setup looks for another checkout whose installednode_modulesmatches this lockfile (npm'snode_modules/.package-lock.json) and copies it copy-on-write (APFS clonefile, or reflink on Linux). A plugins build is copied the same way from a checkout that recorded one for the samepluginstree.--forcealways reinstalls. - Env files: a worktree gets its own
.env.testand, with--app, its own.env. Test-DB commands move.envaside while they run, because the server's config loader merges.envover the environment.
- Add
src/commands/<name>.tsexporting aCommand:name,summary,usage,options,run. - Register it in
src/main.ts. Help andtj help --jsoncome from that table. - Report results through
emit(data, render)and progress throughui.*. Never write progress to stdout. - Throw
TjError(message, { hint, code })for failures.
Keep the TypeScript strip-safe: no enums, namespaces or parameter properties; .ts import extensions; import type.
npm run tj:check type-checks and runs the tests. Pre-commit runs it when tools/tj/ changes.