Developing tcm-ts — tcm-ts

40. Developing tcm-ts

40.1 The codebase

tcm is developed in tcm: all of it is in its own codebase, tcmts, and nowhere else. The repository holds only how to get it (scripts/codebase.sh, package.json's scripts), the instructions agents read (AGENTS.md), and the README. By namespace:

tcmts.core      extraction, canonicalization (the IR), hashing, runtime schemas from types, compiler options
tcmts.store     the SQLite codebase store
tcmts.loader    rendering stored definitions to .tcm/cache modules, running them, the #codebase shim
tcmts.cli       the tcm command, placement inference, sync, merge, web (Vite plugin, dev/build/start), the
                #tcm/host runtime, the wire codec; its tests in tcmts.cli.test (end-to-end: tcmts.cli.test.e2e)
tcmts.vscode    the VS Code extension (a thin client for `tcm lsp`), a package of its own (tcm-ts-vscode): its code,
                and the files it ships beside it (its package.json, icon, TypeScript plugin) in tcmts.vscode.files
tcmts.examples  polls, notes, userinput, reminders, geometry: example projects, kept as written (files of the
                codebase), built by their setup scripts in the end-to-end tests
tcmts.docs      user documentation, one page per feature area
tcmts.site      the docs site: its pages, how they render, and the app serving them (tcmts.site.Docs)
tcmts.share     tcm serve's web UI (tcmts.share.App), shipped in the package as a codebase of its own
tcmts.atproto   the AT Protocol client tcm and its registry use (identity, OAuth sign-in as a loopback client, signed
                records, guarded fetches)
tcmts.react     React's routes, links and data (the router over the page's globals, its hooks, Pages, Stylesheet):
                the tcm library `react` (host protocol 1), which tcm ships (`libraries` in its manifest)
                and apps mount with `tcm lib add tcm react`; tcm's own apps use it by name
tcmts.records   the AT Protocol records of tcm's libraries (codes.tcm.release, the attestations, codes.tcm.star):
                their lexicons, keys and readers; the tcm library `records`. TCM_LIBRARIES lists tcm's libraries,
                which `pnpm verify` checks hold everything they use (each added to a fresh codebase as the branch
                has it), and releases on main once they've landed
tcmts.server    the server API (tables, transactions, jobs, sessions, cookies, the request) on #tcm/host: the tcm
                library `server` (host protocol 1, `table` and `every` in their roles)
tcmts.registry  the registry `tcm registry` runs (tcmts.registry.App), shipped in the package as a codebase of its own:
                Jetstream ingest of codes.tcm.* records, the mirror job checking each release's signature before
                fetching its code by hash, and its pages (tcmts.registry.pages), with the forge's for code
tcmts.dev       verifying, building the docs site, deploying the server and the registry, comparing compilers
tcmts.fixtures  what tests take from the codebase as files (the examples, a docs page, the editor plugin)
tcmts.manifest  the tcm-ts package's manifest
tcmts.deploy    tcm's server and registry, as deployed to Railway (below)

tcm at <file>:<line> names the definition at a line a stack trace shows; tcm find, the editor's Search view and tcm view do the rest of the finding.

Definitions that refer to each other are hashed as one cycle, so a change to one re-hashes all of them, and editing one brings the rest along. Code that tcmts.cli.codebase.Codebase itself uses (views, mirrors, provided packages, kept values, placement) takes CodebaseCore, an interface the class has, rather than the class: naming the class in their types made it one cycle of 29 definitions. Keep it so: code Codebase calls takes CodebaseCore, adding to it what it needs.

Code reading a definition's IR reads it through tcmts.core.ir (defParts, declarationOf, initializerOf, childOf), never by position. The IR is ["Def", tag, ...], then a lineage salt (["Salt", path]) only if the definition has one, then its JSX runtime (["Jsx", runtime]) only if it has JSX, then its syntax. Types aren't salted, and neither is code analyzed with no saltFor (as tests analyze it), so ir[3] is the declaration in some definitions and not in others. tcmts.cli.test.shapes checks that the code reading IR reads analyzed and stored definitions alike.

The command line is a few definitions per command, so that a change to one command reaches only the suites that ran it (a suite is cached by the definitions it ran: Testing). For tcm find:

  • tcmts.cli.commands.routeFind: what parsing reads, its arguments and options as a @bomb.sh/router route tree, parsed into a typed model. It has no descriptions.
  • tcmts.cli.commands.helpFind: its description and its parameters' (commonHelp has those many commands share), read only by tcm help, --help, usage errors and shell completion (@bomb.sh/tab).
  • tcmts.cli.run.runFind: what runs it, given what every command is (Context: its arguments, the codebase opened). tcmts.cli.main.dispatch does what every command needs (-C, --build, the session, --branch, opening the codebase) and calls the command's handler from tcmts.cli.run.handlers(); it runs tcm mcp itself.

routes(), helps() and handlers() list them by name. They're functions making the table, not constants: a constant table of functions would count every function in it, for every suite. A new command needs all three (handlers()'s type and testHelpCoversEveryCommand say when one is missing) and a place in SECTIONS, the order tcm help lists them in. Prompts at a terminal use @clack/prompts (tcmts.cli.ui).

TypeScript is TypeScript 7, through tcmts.core.ts: its syntax trees (typescript/ast), its compiler process (typescript/sync), and the few helpers TypeScript 6 had that tcm uses. Checker answers cross a process boundary, so canonicalization asks for symbols in batches (prefetched in tcmts.core.analyze).

A package's manifest can give its definitions roles (tcmts.cli.roles: ROLES, and rolesIn reads them, by hash, libraries' included; codebaseRoles keeps them until names change), which the build asks about where it would otherwise look for one module's export: so a library's component is known wherever it's used from. Analysis asks too (AnalyzeHost.roleOf): a top-level call of a table is a table, its schema worked out from the call. Code analyzed without names to read roles from (dependents re-derived, code received) stays a table if its record was one (AnalyzeHost.wasTable, CheckRequest.tables for checker processes). Schedules are found where apps are built and started (isSchedule, by the every role). HOST_PROTOCOLS are the versions of the runtime's interface this tcm serves (host in a manifest; hostRefusal is why an app isn't built).

The server runtime is #tcm/host (tcmts.cli.server_api.createServerHost, host protocol 1): the call being answered, the app's SQLite files, the store's lock and transactions, the job queue, its worker and schedules. The runtime (serverApiHost) calls it, and so does the server API, the server library (tcmts.server, ordinary definitions, which tcm's apps use by name and tcm ships for others). Its own source is written to .tcm/cache/tcm-host.js, so it must stay self-contained: everything it uses comes in through deps. (#tcm/server, the module tcm generated before, is refused with TCM_SERVER_GONE.) The runtime knows a deliberate error by its mark (isHttpError: Symbol.for("tcm.httpError")), so any copy's HttpError is one.

40.2 Tests

pnpm verify
everything a change needs (below); names failing tests. When all pass, the package tested becomes the known-good tcm
pnpm test
tcm test tcmts: the tests a change reaches (hashing invariants, frozen golden hashes, and end-to-end CLI scenarios: sync, merge, web apps in real Chrome, production, jobs, data, import), the rest cached
pnpm examples:check [name…] [--tests]
the example apps' files as they are now, checked in seconds (tcmts.dev.checkExamples): each has a codebase kept for checking (dist/examples/<name>-check, made once by its setup script), and the files that changed since the last check are saved into it in its setup's order (what uses them is checked with them); --tests runs its tests too. The end-to-end tests still build each example from scratch. The large one, Effect, isn't in either (tcm's server builds it, as it builds every example, minutes the first time after its files or tcm change): pnpm examples:check effect --tests checks it, after a change to importing or hashing (its first check imports it, minutes)
pnpm docs:dev
serve the docs site as a tcm app (tcm dev tcmts.site.Docs): each page rendered on the server from the codebase's pages, the browser reloading when one is updated
pnpm docs:site
build the docs site (tcmts.docs and the examples page) into dist/site: the docs app built as a static site (tcm build tcmts.site.Docs --static, every page rendered as it's built), with its code browser (code/: tcm's codebase, every definition a page, and a big namespace's grouped by what they're for as the editor's Codebase view groups them, and an index linking each example's forge, code/<name>/, which tcm's server shows; tcmts.dev.buildCodeBrowser), the examples' files to download, and the examples that are static sites (reminders) built into it, from codebases their setup scripts build, kept in dist/examples; it fails on any broken link or anchor, or a page no link reaches. pnpm server:deploy builds it into what tcm's server serves
pnpm tcm run tcmts.dev.forgeTimings <codebase dir>
how long what the web UI's pages work out takes on a codebase (a copy of tcm's own, say: cp -c clones it): the warm-up a server starts with and the longest it holds up other work, each step cold, and the latest 60 entries' history summaries checked against diffNamespaces
pnpm tcm run tcmts.dev.atprotoTrial [tcm.js] [dir]
tcm's AT Protocol flows against Bluesky's own software, locally and offline, before a release: Bluesky's dev-env's PDS (its OAuth server and consent pages, driven in Chrome) and PLC directory, installed in dir (default in the system's temp directory, once; not as tcm's dependency), with a stand-in for Jetstream and a relay fed by the PDS's own firehose. It checks announcing (loopback OAuth and its granular scopes), tcm lib add and upgrade by announcement, attestations, a kept sign-in refreshed and its scope enforced, the registry (getRecord proofs, a visitor signing in and starring, "Libraries I use", live events, tcm lib search), and publishing tcm's lexicons (from tcm's codebase, on the branch it runs on, as a dev-env account standing in for tcm.codes), with the tcm given (default the known-good one). Its scripts are tcmts.dev.devenv; what each service said is in dir/*.log
pnpm compare-compilers
every definition rendered and hashed again with tsrs must hash as with TypeScript 7 (tcm verify under tsrs), and the golden tests must pass with it (tcmts.dev.compareCompilers)

tcm's tests are test definitions, in its own codebase like the code they test. Those that call code directly (the core and store tests, the CLI's server-component and highlighting tests) are cached by hash: a change re-runs only the tests that reach it. The end-to-end tests, which run tcm as a program, are in tcmts.cli.test.e2e (helpers in tcmts.cli.test.e2e.harness): they're the tcm-ts package's suites, each run as a program of its own against the codebase's own tcm and cached by the code it ran. What they need as files (an example project, a docs page) they import from the codebase (tcmts.fixtures) and write where they need it.

40.3 Working on tcm in tcm

pnpm tcm <command> runs tcm on tcm's own codebase, in codebase/ (not in git), from wherever you run it, so paths you give it are yours (TCM_CODEBASE names the codebase; tcm leaves it out of what tests run). The first run gets the codebase from tcm's server, which is public, with its history and releases (and sets origin up with the token in TCM_TOKEN, if there is one), and, if there's no known-good tcm here yet, the tcm the server runs. Proposing a change from outside is contributing.

pnpm tcm patch tcmts.core.semver.compatRange --old "…" --new "…"   # a small change, checked and saved
pnpm tcm edit tcmts.core.semver.compatRange                         # a bigger one, into codebase/scratch.ts
pnpm tcm update                                                     # saved; its dependents re-hashed
pnpm tcm mv tcmts.core.semver.meetsComparator tcmts.core.semver.matchesComparator
pnpm tcm test tcmts                                                 # the tests the change reaches
pnpm tcm push                                                       # to the server: the codebase's only other copy

Agents work on tcm the same way: the repository's .mcp.json runs tcm mcp --session on this codebase, so an MCP client opened in the repository gets tcm's tools and instructions, in a session of its own, landed on main by tcm_merge when you say so.

The files the repository keeps (README, AGENTS.md, package.json, scripts/codebase.sh, …) are files of the codebase too: change them in the codebase, then pnpm repository (tcmts.dev.writeRepository) writes them out to commit.

40.3.1 The known-good tcm

The tcm doing the work isn't the one being changed. pnpm tcm (and so the MCP server) runs the build the last passing pnpm verify tested, kept in dist/known-good (VERIFIED there says which history entry it was made from). A change that breaks tcm can't break the tcm making it. pnpm verify replaces it when everything passes, and a running tcm mcp picks the new one up by itself (agents). To keep working while verify runs, work in a session (TCM_BRANCH=agent/x pnpm tcm …, or an agent's tcm mcp): verify reads main. TCM_BRANCH=<b> pnpm verify verifies a session's branch instead, everything but making it the known-good tcm, which is only ever main's. Each verify builds in a place of its own (below), so checkouts sharing dist/ (worktrees linking it) verify their branches side by side; only making a tcm the known-good one waits its turn.

A change to the hash format or the table layout (tcmts.store.SCHEMA) would leave the two unable to read each other's codebases, tcm's own included: the known-good tcm can't open a codebase in the new format or layout, and the new tcm can't open one in the old. So pnpm verify moves them together. When everything passes, the new tcm migrates tcm's codebase (tcm migrate: history and all, the old database kept as .tcm/codebase.v<n>.sqlite, the migrated one used only if it reads back as itself), and only then becomes the known-good one; the one it replaces is kept as dist/known-good-previous. If the migration fails, neither changes. Until then, TCM_LIVE=1 can't run the new tcm on tcm's own codebase (it can't open it); it runs it anywhere else (a test codebase), and the tests do.

The code being changed is what runs everywhere else: tcm test tcmts runs the codebase's own test definitions, and its end-to-end tests against the codebase's own tcm, each only if code it ran changed. And TCM_LIVE=1 pnpm tcm <command> runs the codebase's own tcm as it is now, to try a change before verifying it: the known-good one runs it from the codebase (tcm run tcmts.cli.main.runCli -- <command>), as it would any program there, on main (or the branch TCM_BRANCH names: a session's). To try it on another codebase (one made for a repro), -C comes first: TCM_LIVE=1 pnpm tcm -C /tmp/repro status runs it there, on that codebase's own branch.

40.3.2 Older builds

pnpm verify keeps the builds it replaces, to run again: to show a bug was already in an older tcm, or that a new test's case fails without the fix (testing). The one the known-good replaced is dist/known-good-previous; the one before that goes to dist/builds/<id>, named by the history entry it was made from (verified.json in each says which, VERIFIED when), and the newest four there are kept (each an install of about 80 MB, most of it shared with the next, as APFS clones files).

pnpm tcm builds
each build: its name, the history entry it was made from (how many entries back on this branch, and its command), when it was verified, what changed in it since the build before it, and its tcm.js
pnpm tcm --build <name> <command>
the command run by that build: known-good (main's last verified), previous (the one before it), an id tcm builds lists (a prefix will do), or a path to a tcm.js. On tcm's codebase, as pnpm tcm is; with -C <dir> after it, on the codebase in dir (a repro's), whichever build it is
pnpm tcm test <e2e tests> --bin tcm=<name>
the end-to-end tests against it
pnpm tcm builds make <entry>
a build of tcm as it was at any history entry (a number back, as tcm history counts, or a hash), when none kept is: packed from the codebase as it was then, by the tcm running, and installed into dist/builds/<id> beside a clone of the known-good install's dependencies (by npm, when they differ), in about fifteen seconds

pnpm tcm says where they are (TCM_BUILDS, its dist/), and a tcm running from a kept build finds the others beside it. A build from before a change to the hash format can't open a codebase in the new one, tcm's own included: run it on a codebase it makes (--build <it> -C /tmp/repro init).

40.4 Verifying a change

pnpm verify is pnpm tcm run tcmts.dev.verify, run with the codebase's own tcm, which tcm run gives it as TCM_BIN. It checks these side by side, printing each stage's result and time as it finishes, and, while tests run, each failure as it happens and every 30 seconds how many have finished:

  1. codebase: nothing needs fixing (tcm todo), and every definition renders and re-hashes to itself (tcm verify);
  2. tests: tcm test tcmts --definitions, the test definitions;
  3. docs: the docs site builds from the codebase's pages, every link and anchor resolving;
  4. package: the tcm-ts package packed from the codebase installs (into dist/candidates/<branch>/install), works far from the repository (it makes a codebase in a temporary directory), and passes the end-to-end tests (tcm test --bin tcm=<it>). They run against it, the tcm that becomes the known-good one and that's released, and only against it: once, not again against the codebase's own build (which pnpm test uses while you work).

What passed is kept, like a test's result, so each stage does only what a change reaches:

  • with nothing changed since the known-good tcm was verified (the codebase's history head, on the same Node and platform; verified.json in dist/known-good), there's nothing to check, and verify says so at once;
  • tcm verify checks again only what this same tcm hasn't verified with the names it has now;
  • tests run again only where code they ran changed;
  • a package packed the same as the known-good one, file for file, is the known-good one: it isn't installed and tried far away again. One that differs only in its pages (the docs it ships) is the known-good install with the new pages; one whose dependencies are the known-good's is unpacked from its tarball beside them (a copy of the known-good install, which APFS clones) rather than installed by npm from scratch. Trying it far away and its tests run side by side.

So with nothing changed, verify takes under a second; a change few tests reach (a page, a command), a few seconds, most of them building the docs site; one that every end-to-end test runs (the store, the Codebase class) about two and a half minutes. pnpm verify --fresh checks everything again, tests included. When everything passes, the package becomes the known-good tcm.

40.4.1 Side by side: what's in dist/

As each worktree has its own files, each branch's verify has its own place to build, and only what's shared waits its turn: verifies of different branches (agents' sessions, each with TCM_BRANCH) run at once and never wait for each other; a second verify of the same branch waits for the first; and making a tcm the known-good one, which every checkout runs, is main's alone, one at a time, holding dist/verify.lock only while it does (and while it migrates tcm's codebase to a new hash format). The tests they run share the machine's suite budget.

known-good/
the tcm pnpm tcm and the MCP server run: main's last verified (VERIFIED, verified.json), with its tarball
known-good-previous/, builds/<id>/
the ones it replaced (older builds)
candidates/<branch>/
a verify's own (main, or the branch, as a directory's name: session-2026-11-02-1db6a0-<hash>): package/ (what tcm pack made), install/ (the tcm it tests, which becomes known-good/ for main), and verify.lock while it runs. A branch's goes once it passes; a failed one's stays a day (to run its install/…/tcm.js again), and the next verify takes away what's left of a branch that's gone, or older
verify.lock
held by main's verify while it makes its tcm the known-good one
examples/
the codebases of the examples the docs site builds in (static sites), and their downloads, by what their files hash to: built one at a time each (<name>-<hash>.lock), and kept a day after a verify last used one, as other branches' may have other files
package/, site/
pnpm build's package and pnpm docs:site's docs site

40.5 tcm's server

tcm's codebase is served by tcm serve on Railway (the tcm-server service of the tcm-ts project), at https://tcm.codes. Its Railway address, https://tcm-server-production.up.railway.app, is the same server. With the codebase only in tcm, the server is its copy away from your machine: pnpm tcm push keeps it there. One domain holds everything, by path:

/
the home page (tcmts.deploy.server.site.index, a placeholder for now), and the tcm it runs (/tcm-ts.tgz, which a new checkout's pnpm tcm installs)
/docs/
the docs site, its code browser (/docs/code/: tcm's codebase as pages, and each example's codebase in a forge of its own, /docs/code/<name>/), and the examples (/docs/examples/<name>/: their downloads, and the apps among them running)
/forge/
the codebase's web UI
/registry/
tcm's registry, a service of its own (below)
POSTs to /<route>
the codebase itself: its history, branches, contributions and releases, as tcm pull, push and contribution ask for them (origin is https://tcm.codes)

They can't collide: the codebase's routes are POSTs to fixed names at the top (get, put, branch, run, …), the site answers only GET and HEAD, and /forge, /registry, /docs/code/<name> and /docs/examples/<name> are matched first, by whole path segments. (So a path proxied to an app mustn't be one of those names.)

Links from before: the docs were at the root until November 2026. --moved /docs redirects (a 301) a GET for a path the site doesn't have at the root but has under /docs, or that an example app serves there: /cli.html goes to /docs/cli.html, /examples/notes/ to /docs/examples/notes/, on either address. Pulls and pushes are where they were, so an origin at the Railway address needs no change.

  • tcmts.deploy.server is what's deployed: a Dockerfile that installs the known-good tcm, start.sh (it makes the codebase on the service's volume, /data, the first time, and serves it with --public, --no-run, --site, --moved /docs, --proxy for the example apps and the registry, and --forge for the examples' codebases), railway.json, and the home page (site/index.html). Railway terminates TLS, so inside it's plain HTTP (--insecure).
  • The examples that are apps (tcmts.dev.EXAMPLES with an app, listed in the docs' examples/apps.txt) run beside it: start.sh builds each from the site's download of it, in the background (NO_TESTS=1 setup.sh, into /data/examples/<name>-<hash>, once per version of it and of tcm), runs tcm start <app> --base /docs/examples/<name> on one of two ports of its own, and tcm serve --proxy passes /docs/examples/<name>/ to whichever answers. A new instance starts on the other port, and the one serving stops only once it answers, so nothing is dropped: while a deploy's new version is built (minutes) the version before goes on serving, and notes and polls start over every six hours (TCM_EXAMPLES_RESET, seconds) as a fresh instance without their data. What's left is the deploy itself: Railway restarts the service (one with a volume can't run two deployments at once), so the site and the apps are away for that restart; then the versions before start first. With none before (the first deploy), an example's page says it's starting and reloads by itself.
  • Every example's codebase is shown in a forge of its own: start.sh builds the ones that aren't apps too (every download the site has, examples/*.tar.gz), in the background, the same way, and links /data/examples/<name> to each one's latest build; tcm serve --forge /docs/code/<name>=/data/examples/<name> shows it (web UI), and starts again on a new build once the link moves to it. While one is built the build before it is shown; with none before, its page says there's none yet.
  • pnpm server:deploy (tcmts.dev.deployServer) builds the docs site from the codebase, for /docs (buildSite(…, { base: "/docs" }): the docs app's navigation and the static examples are built for it; its links are relative), and uploads it, the server's files and the known-good tcm's package to the tcm-server service of the Railway project the repository is linked to (railway link, to tcm-ts). The docs it serves are as of the last deploy.
  • Anyone can pull from it: the first pnpm tcm makes codebase/ from it with no token. The maintainer's token is the service's TCM_TOKEN (railway variables list --service tcm-server): with it in TCM_TOKEN, the first pnpm tcm sets origin up with it, so pnpm tcm push, contribution merge and user add work against it. Contributors have tokens of their own, made with pnpm tcm user add <handle> (contributors), which push only their own branches. An existing codebase/ connects with pnpm tcm remote add origin https://tcm.codes --token <token>.

40.6 tcm's registry

tcm's own registry is at https://tcm.codes/registry, the one tcm lib search asks unless it's told another. It's a Railway service of its own, tcm-hub (its name from when the registry was called the hub), in the same project, so its jobs and storage are apart from tcm serve's, and a deploy of one doesn't restart the other. It has no domain of its own: tcm's server passes it what's asked under /registry, over Railway's private network (--proxy /registry=http://tcm-hub.railway.internal:8080).

  • tcmts.deploy.registry is what's deployed: a Dockerfile that installs the known-good tcm, start.sh runs tcm registry --host :: --port $PORT --base /registry --dir /data/hub (::, as Railway's private network may be IPv6 only; it won't start with TCM_ATPROTO_LOCAL set), and railway.json builds that image and counts a deploy only once the registry's page (/registry/) answers.
  • pnpm registry:deploy (tcmts.dev.deployRegistry) uploads them and the known-good tcm's package to the tcm-hub service of the project the repository is linked to. A newer tcm updates the registry's app in its directory and keeps the rest: its tables, jobs, where it got to in Jetstream, and the codebase it mirrors into.
  • Its address takes nothing to set. Railway's proxy says what visitors asked (Host: tcm.codes, x-forwarded-proto: https), tcm's server passes that on, and the registry adds its base: its OAuth client document is https://tcm.codes/registry/api/oauth/client, coming back to https://tcm.codes/registry/. Its base is TCM_REGISTRY_BASE (default /registry), which must match the path the server proxies (/registry in its start.sh).
  • Its old names stay where renaming them would cost: the service, tcm-hub (renaming it changes its private address: rename it in the dashboard, then set the server's TCM_REGISTRY_PROXY to http://<new name>.railway.internal:8080, or change the default in the server's start.sh), and its directory on the volume, /data/hub (moving it is mv /data/hub /data/registry with the service stopped, then TCM_REGISTRY_DIR=/data/registry).

40.7 Setting up tcm.codes

Once, by hand: nothing here is done by the deploy commands. The tcm deployed (the known-good one) must have tcm serve --moved and tcm registry --base: pnpm tcm serve --help lists --moved.

  1. The registry's service: in the tcm-ts project, a new empty service named tcm-hub (the name the server's start.sh reaches it by), with no public domain (if one was made for hub.tcm.codes, remove it, and its hub record at Marque if it was added).
  2. Its volume, mounted at /data (the service's Volumes, or railway volume add --mount-path /data with the service linked): start.sh keeps the registry in /data/hub (TCM_REGISTRY_DIR). The mirrored codebase grows with what's announced.
  3. Its variables: PORT=8080, the port tcm's server asks it on. JETSTREAM_API_KEY if there's one (a new registry then replays Jetstream's archive; without one it catches up from the relay's accounts, which is enough for tcm's own: everything announced so far is still in its authors' repositories). TCM_REGISTRY_EVERY only to read Jetstream other than every minute. None of the settings that point it elsewhere (TCM_JETSTREAM, TCM_RELAY, TCM_APPVIEW, TCM_ATPROTO_DIRECTORY: their defaults are Bluesky's network); never TCM_ATPROTO_LOCAL.
  4. The server's variables: none new. TCM_REGISTRY_PROXY is where it finds the registry (default http://tcm-hub.railway.internal:8080, the service's private name and PORT): set it only if those differ, or empty for no registry.
  5. Deploy, the registry first: env -u TCM_BRANCH pnpm registry:deploy, then env -u TCM_BRANCH pnpm server:deploy, from a checkout linked to the project (railway link). Either order works: until the registry answers, /registry/ says it's starting and reloads by itself. The registry's first start installs its packages and builds it (under a minute; the health check waits five).
  6. The domain: on the tcm-server service, Settings → Networking → Custom Domain, tcm.codes, on the service's PORT (8080 on Railway, which sets it; start.sh's 7737 is only for when nothing does). Add it in the dashboard: the Railway CLI answers railway domain tcm.codes with "Unauthorized". Railway shows two records to make, both at Marque (marque.at), where tcm.codes's DNS is, beside its _atproto and _lexicon TXT records, which stay as they are (the Bluesky handle needs nothing served: no /.well-known/atproto-did):
    • the apex, a CNAME named @ to the target Railway shows (….up.railway.app). A CNAME can't be at the apex as it is; Marque flattens it, answering with the target's address (an A record);
    • a TXT named _railway-verify, its value railway-verify=… as Railway shows it. It's needed here: as Marque answers the apex with an A record, Railway can't see the CNAME, and the domain stays "Waiting for DNS update" until this record is there. Then Railway checks it and issues the certificate (about 3 minutes).
  7. Check it:
    • https://tcm.codes/ is the home page, /docs/ the docs, /forge/ the web UI; /docs goes to /docs/.
    • curl -sI https://tcm.codes/cli.html (and the Railway address's) is a 301 to /docs/cli.html; /examples/notes/ goes to /docs/examples/notes/, which runs.
    • https://tcm.codes/registry/ (Activity) and /registry/libraries load; curl https://tcm.codes/registry/api/oauth/client names "client_id":"https://tcm.codes/registry/api/oauth/client", and redirect_uris is ["https://tcm.codes/registry/"].
    • railway logs --service tcm-hub says jobs recurring: ingest (1m), then registry: read … record(s) from the accounts the relay lists once, and job ingest done every minute.
    • tcm init && tcm remote add origin https://tcm.codes && tcm pull, in an empty directory, gets tcm's codebase.
    • tcm lib search <query>, with no --registry (and none kept), ends from https://tcm.codes/registry.
    • Sign in, at the top of a registry page, with an account (a throwaway one), comes back signed in.
  8. Then the repository: pnpm repository, and commit: the README, AGENTS.md, CONTRIBUTING.md and scripts/codebase.sh name https://tcm.codes (a new checkout gets tcm and the codebase from it), so only once it resolves.

40.8 Publishing tcm's lexicons

For tcm's maintainers. The records tcm writes on the AT Protocol (codes.tcm.release, codes.tcm.review, codes.tcm.advisory, codes.tcm.yank, codes.tcm.star: releases, libraries) have Lexicon schemas, kept in the records library (tcmts.records.lexicons). They're published as the Lexicon spec says: each a com.atproto.lexicon.schema record, keyed by its NSID, in the repository of the DID that _lexicon.tcm.codes (a DNS TXT record, did=<did>) names: tcm.codes's own account.

pnpm tcm run tcmts.dev.publishLexicons -- --dry-run      # what it would write, signing in to nothing
pnpm tcm run tcmts.dev.publishLexicons [-- --as tcm.codes]
  • Only as the authority. It looks up _lexicon.tcm.codes first, and signs in as that DID (--as, if given, must be it): signed in as anyone else, it refuses, writing nothing.
  • The sign-in is tcm's loopback OAuth (releases), asking only for repo:com.atproto.lexicon.schema, and kept as any is; the account you sign in as by default stays the one it was.
  • Idempotent: each document is checked against the Lexicon spec (with @atcute/lexicon-doc), then compared with what's published: a schema published as it is now isn't written again, a changed one is replaced (putRecord, swapping the record it read) and a new one created. It lists each as new, changed or unchanged.
  • When a lexicon changes (tcmts.records), the tests in tcmts.dev.lexicons.test check every record tcm builds (a release as tcm release --announce makes it, each attestation, a star) against the schemas, keys and all; publish again once the change has landed on main.
  • The AT Protocol trial runs it against dev-env's PDS, with TCM_LEXICON_DID (honoured only with TCM_ATPROTO_LOCAL=1) standing in for the DNS record.

40.9 Packaging

tcm-ts, the npm package, is a package of tcm's own codebase: its manifest is tcmts.manifest (the tcm and tcm-ts bins run tcmts.cli.main.runCli, tcm-ts/core exports the analyzer). pnpm build packs it into dist/package, with dependencies pinned at the codebase's locked versions. At a release it's that release; otherwise it's the code as it is, as 0.0.0.

Releasing is tcm release, in tcm's codebase:

pnpm tcm release check tcm-ts      # the next version, from the semver checks on tcm-ts's API
pnpm tcm release tcm-ts --draft    # codebase/release-tcm-ts.md: write a summary above the marker
pnpm tcm release tcm-ts            # recorded in the codebase (and pushed with it)
pnpm tcm changelog tcm-ts --out CHANGELOG.md
pnpm build && (cd dist/package && npm publish)

Publishing is done by hand.

built from c482a14e