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 hub 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.hub the hub `tcm hub` runs (tcmts.hub.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.hub.pages), with the forge's for code
tcmts.dev verifying, building the docs site, deploying the server and the hub, 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 hub, 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' (commonHelphas those many commands share), read only bytcm 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.dispatchdoes what every command needs (-C,--build, the session,--branch, opening the codebase) and calls the command's handler fromtcmts.cli.run.handlers(); it runstcm mcpitself.
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 testtcm 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 cachedpnpm 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);--testsruns its tests too. The end-to-end tests still build each example from scratch. The large one, Effect, isn't in either, nor in the code browserpnpm verifybuilds (pnpm docs:siteandpnpm server:deploybuild it, minutes the first time after its files change):pnpm examples:check effect --testschecks 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.docsand the examples page) intodist/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 and each example's, built by its setup script and kept indist/examples, every definition a page, and a big namespace's grouped by what they're for as the editor's Codebase view groups them;tcmts.dev.buildCodeBrowser), the examples' files to download, and the examples that are static sites (reminders) built into it; it fails on any broken link or anchor, or a page no link reaches.pnpm server:deploybuilds 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 -cclones 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 againstdiffNamespaces 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 addandupgradeby announcement, attestations, a kept sign-in refreshed and its scope enforced, the hub (getRecordproofs, 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 aretcmts.dev.devenv; what each service said is indir/*.log pnpm compare-compilers- every definition rendered and hashed again with tsrs must hash as with TypeScript 7 (
tcm verifyunder 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 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, 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 idtcm buildslists (a prefix will do), or a path to atcm.js. On tcm's codebase, aspnpm tcmis; with-C <dir>after it, on the codebase indir(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 historycounts, or a hash), when none kept is: packed from the codebase as it was then, by the tcm running, and installed intodist/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:
- codebase: nothing needs fixing (
tcm todo), and every definition renders and re-hashes to itself (tcm verify); - tests:
tcm test tcmts --definitions, the test definitions; - docs: the docs site builds from the codebase's pages, every link and anchor resolving;
- package: the
tcm-tspackage packed from the codebase installs (intodist/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 (whichpnpm testuses 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.jsonindist/known-good), there's nothing to check, and verify says so at once; tcm verifychecks 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 tcmand 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/(whattcm packmade),install/(the tcm it tests, which becomesknown-good/formain), andverify.lockwhile it runs. A branch's goes once it passes; a failed one's stays a day (to run itsinstall/…/tcm.jsagain), 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 example codebases the docs' code browser is built from, 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 andpnpm 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'spnpm tcminstalls) /docs/- the docs site, its code browser (
/docs/code/), and the examples (/docs/examples/<name>/: their downloads, and the apps among them running) /forge/- the codebase's web UI
/hub/- tcm's hub, a service of its own (below)
- POSTs to
/<route> - the codebase itself: its history, branches, contributions and releases, as
tcm pull,pushandcontributionask for them (originishttps://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, /hub 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.serveris 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, and--proxyfor the example apps and the hub),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.EXAMPLESwith anapp, listed in the docs'examples/apps.txt) run beside it:start.shbuilds 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), runstcm start <app> --base /docs/examples/<name>on one of two ports of its own, andtcm serve --proxypasses/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. 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 thetcm-serverservice of the Railway project the repository is linked to (railway link, totcm-ts). The docs it serves are as of the last deploy.- Anyone can pull from it: the first
pnpm tcmmakescodebase/from it with no token. The maintainer's token is the service'sTCM_TOKEN(railway variables list --service tcm-server): with it inTCM_TOKEN, the firstpnpm tcmsetsoriginup with it, sopnpm tcm push,contribution mergeanduser addwork against it. Contributors have tokens of their own, made withpnpm tcm user add <handle>(contributors), which push only their own branches. An existingcodebase/connects withpnpm tcm remote add origin https://tcm.codes --token <token>.
40.6 tcm's hub
tcm's own hub is at https://tcm.codes/hub, the one tcm lib search asks unless it's told another. It's a
Railway service of its own, tcm-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
/hub, over Railway's private network (--proxy /hub=http://tcm-hub.railway.internal:8080).
tcmts.deploy.hubis what's deployed: a Dockerfile that installs the known-good tcm,start.shrunstcm hub --host :: --port $PORT --base /hub --dir /data/hub(::, as Railway's private network may be IPv6 only; it won't start withTCM_ATPROTO_LOCALset), andrailway.jsonbuilds that image and counts a deploy only once the hub's page (/hub/) answers.pnpm hub:deploy(tcmts.dev.deployHub) uploads them and the known-good tcm's package to thetcm-hubservice of the project the repository is linked to. A newer tcm updates the hub'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 hub adds its base: its OAuth client document ishttps://tcm.codes/hub/api/oauth/client, coming back tohttps://tcm.codes/hub/. Its base isTCM_HUB_BASE(default/hub), which must match the path the server proxies (/hubin itsstart.sh).
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 hub --base: pnpm tcm serve --help lists --moved.
- The hub's service: in the
tcm-tsproject, a new empty service namedtcm-hub, with no public domain (if one was made forhub.tcm.codes, remove it, and itshubrecord at Marque if it was added). - Its volume, mounted at
/data(the service's Volumes, orrailway volume add --mount-path /datawith the service linked):start.shkeeps the hub in/data/hub(TCM_HUB_DIR). The mirrored codebase grows with what's announced. - Its variables:
PORT=8080, the port tcm's server asks it on.JETSTREAM_API_KEYif there's one (a new hub 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_HUB_EVERYonly 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); neverTCM_ATPROTO_LOCAL. - The server's variables: none new.
TCM_HUB_PROXYis where it finds the hub (defaulthttp://tcm-hub.railway.internal:8080, the service's private name andPORT): set it only if those differ, or empty for no hub. - Deploy, the hub first:
env -u TCM_BRANCH pnpm hub:deploy, thenenv -u TCM_BRANCH pnpm server:deploy, from a checkout linked to the project (railway link). Either order works: until the hub answers,/hub/says it's starting and reloads by itself. The hub's first start installs its packages and builds it (under a minute; the health check waits five). - The domain: on the
tcm-serverservice, Settings → Networking → Custom Domain,tcm.codes, on the service'sPORT(8080 on Railway, which sets it;start.sh's 7737 is only for when nothing does). Add it in the dashboard: the Railway CLI answersrailway domain tcm.codeswith "Unauthorized". Railway shows two records to make, both at Marque (marque.at), where tcm.codes's DNS is, beside its_atprotoand_lexiconTXT records, which stay as they are (the Bluesky handle needs nothing served: no/.well-known/atproto-did):- the apex, a
CNAMEnamed@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
TXTnamed_railway-verify, its valuerailway-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).
- the apex, a
- Check it:
https://tcm.codes/is the home page,/docs/the docs,/forge/the web UI;/docsgoes 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/hub/(Activity) and/hub/librariesload;curl https://tcm.codes/hub/api/oauth/clientnames"client_id":"https://tcm.codes/hub/api/oauth/client", andredirect_urisis["https://tcm.codes/hub/"].railway logs --service tcm-hubsaysjobs recurring: ingest (1m), thenhub: read … record(s) from the accounts the relay listsonce, andjob ingest doneevery 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--hub(and none kept), endsfrom https://tcm.codes/hub.- Sign in, at the top of a hub page, with an account (a throwaway one), comes back signed in.
- Then the repository:
pnpm repository, and commit: the README,AGENTS.md,CONTRIBUTING.mdandscripts/codebase.shnamehttps://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.codesfirst, 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 asnew,changedorunchanged. - When a lexicon changes (
tcmts.records), the tests intcmts.dev.lexicons.testcheck every record tcm builds (a release astcm release --announcemakes 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 withTCM_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.