CLI reference — tcm-ts

34. CLI reference

tcm help prints the same list in short form; tcm <command> --help (or tcm help <command>) shows a command's arguments and options, with their defaults. A mistyped command gets a suggestion, and an argument or option a command doesn't take is an error naming it, followed by what the command does take (its usage, arguments and options, as --help shows them), with the option meant for a misspelt one (unexpected '--olds' (did you mean --old?)).

  • Arguments for tcm run that start with - go after --: tcm run lib.main -- --verbose.
  • Missing arguments are asked for at a terminal: tcm view, run, edit, docs, deps, mv, rm, dev and others offer the codebase's names to search, tcm switch and merge offer branches, tcm resolve the conflicts and their definitions, tcm reset the recent commands to undo, and tcm upgrade the packages. tcm rm of a name others use asks instead of refusing. Elsewhere (scripts, CI, editors), a missing argument is a usage error.
  • Package installs show as a line each, replaced by what was installed and how long it took on a terminal.
  • tcm -C <dir> <command> runs as if tcm were started in dir, as git -C does: on the codebase there, not one TCM_CODEBASE or a session's TCM_BRANCH names (they're the caller's). With tcm run --bin tcm, it's how a change to tcm is tried on a codebase made for it: tcm run --bin tcm -- -C /tmp/repro status (an agent: tcm_cli with live and cwd).
  • tcm --build <build> <command> runs the command with another tcm: a build pnpm verify kept (known-good, previous, or an id tcm builds lists) or a tcm.js. -C <dir> after it runs it in dir, on the codebase there, whichever build it is. It's how to show a bug is in an older tcm (developing); an agent: tcm_cli with build.
  • --branch <b> reads another branch: the commands that only read (view, search, find, ls, deps, dependents, docs, todo, status, history, diff, at) take it, and read that branch as it is: its code, how it reads (its comment and doc changes), its history and todo (status lists its session's unsaved scratch files by name). Nothing changes it, or any branch. In a session, it's how to look at another session's work without moving: the session's own branch and #codebase files stay as they are. A command that changes things isn't given it, and says so, rather than acting on that branch as TCM_BRANCH would. (tcm mcp --branch is another thing: the branch a session works on.)
  • -m <message> says what a history entry is for (--message): the commands that make one (add, update, patch, mv, alias, rm, resolve, import, upgrade, fmt, merge, revert, reset) take it, and tcm history calls the entry by it (messages).
  • A hash is # and its first letters (#abc), as tcm shows it; one given with another # before it (##abc) is the same hash.
  • Shell completion: source <(tcm complete zsh) (or bash, fish, powershell); see install.

34.1 Codebase

tcm init
set up a codebase here: .tcm/codebase.sqlite, package.json #codebase imports, tsconfig.json, scratch.ts
tcm / tcm watch [--into ns] [--fresh]
watch *.ts scratch files (and files kept as written that are there to save: a page saying where it goes, or, with --into, one the namespace holds, as tcm edit writes a Dockerfile): typecheck, hash, report, evaluate watch expressions and tests. Their results are cached by hash; --fresh re-runs them. Names a file uses and doesn't import are said (not imported: …), not written: the file is open in an editor
tcm lsp [--stdio]
language server for editors, over stdio (editors)
tcm mcp [--branch b | --direct]
tools for AI agents to work in the codebase: a Model Context Protocol server, over stdio (agents); a session on a branch of its own (a new one, or the one the last session from the same directory left holding its work; --branch b: that one, if no other server is on it), landed by tcm_merge when you say so, and moved onto another with tcm_cli ["switch", "<branch>"]; --direct, no session: on the codebase's own branch (--session is accepted, and the default)
tcm complete <shell>
a completion script for zsh, bash, fish or powershell, completing names from the codebase too
tcm check [files...] [--fresh]
one-shot version of watch. Reports each definition as new / identical / update / alias / resolves (saved at a conflicted name, it resolves the conflict) (more than a few identical ones as one line). Names the code uses and doesn't import are imported first, written into the file, as saving would (the lines added are said); an import nothing uses is left, and said (saving takes it out). Code that doesn't parse is said by its first syntax error alone, with the lines around it: no type errors that follow from reading it wrong, and nothing imported
tcm add [files...] [--into ns]
save new definitions from scratch files (default: the codebase's scratch.ts, wherever tcm runs from); names the code uses and doesn't import are imported first, and imports nothing in the file uses are taken out, as tcm patch does (the lines added, and the names taken out, are said; scratch files)
tcm update [files...] [--into ns] [--clear]
save, re-point changed names, and re-hash every dependent. --clear deletes each file once it's saved (what it held is in the codebase); a change to comments only (or to the name something from outside the codebase is imported as), the code the same, is saved too (~ text). Imports what the code uses and doesn't import, and takes out imports nothing uses, as tcm add does. What the file imports that uses what it changes is re-derived against it and saved with it ((dependent)), so a class and what's typed with it change in one save (scratch files). Code that doesn't parse is said as check says it. Dependents that stop typechecking keep their old hash and go to todo. Also checks stored table rows against changed row types (data) (many dependents are listed by namespace; tcm diff lists them all)
tcm export <namespace> <dir>
write a namespace's files (pages, stylesheets, images, a Dockerfile, a .gitignore with its dot), or one file, to a directory, links and names as they are now
tcm import <dir> [--into ns] [--update] [--exclude a,b] [--tests] [--mirror] [--verbose]
add an existing project, one namespace per file; --mirror keeps its files in step with the codebase, and makes the releases recorded with them again (importing)
tcm mirror [--check] [--unlink]
the directory changes are written back to; --check re-analyzes its files, which must hash to what the codebase holds (mirrors)
tcm edit <name>... [--file f] [--force]
write definitions to a scratch file for editing (scratch.ts, or scratch.tsx for JSX), with references following any updates; added to the file if it holds unsaved work (--force replaces it); never over the codebase's own package.json or tsconfig.json. A conflicted name is written to a file of its own, named after it (lib.k.ts), as its two sides merged from the merge base's, with conflict markers where both changed the same lines, saying what each side changed and whether the merge typechecks; conflicted names given together (one using another) share one file. Saving it, once the markers are out, resolves the conflict, named resolve <name> (merged) in history (merging)
tcm patch <name> --old <text> --new <text> [--all] [--imports <json>]
change a definition by find-and-replace on its code as tcm view shows it, then save it as update does. Each old text must be there once (or --all), as written or spaced and wrapped otherwise (the code is formatted); nothing is saved if one isn't, or if the result doesn't typecheck. --old and --new are taken as written: a \n in them (as a shell's '…' leaves it) is a backslash and an n, as code has it in "a\nb", not a line break. Write a line break as one (inside the quotes, or $'…\n…' in a shell), or give --edits as JSON, where \n is one; a \n in --new that isn't in --old is noted, and an --old with one that isn't found says this. Inside a template literal, the text is the source's, escapes and all (\n a backslash and an n, \`, \${): an old text found only with its escapes taken the other way (a line break for \n, a bare backtick for \`, ${ for \${, as JSON makes easy to write) is matched so when that finds it once, the new text written the same way, and a note says so; a new text that would end or split a template literal it's inside (a bare backtick, or ${ without its }) changes nothing, and says which and how to escape it. Code that doesn't parse is said as check says it. --edits '[{"old": …, "new": …}]' makes several edits at once; --patches '[{"name": …, "edits": […]}, …]' changes several definitions, and pages (files kept as written), saved together as one update and one history entry (a signature and its callers, or code and the page saying what it does); each edit is matched against its definition as tcm view shows it, however they're written together to be checked (two functions declared alike, which one file can't hold, go in a file each, saved one after the other). An old text that isn't there is reported with the most similar line. Imports follow the code (a name the new code uses is imported from the codebase, a package the codebase's code imports it from, tcm's own modules, or the one Node builtin that exports it; one it stops using goes); one that can't be found that way (an alias: isImportDeclaration as ts_isImportDeclaration) is given with --imports '[{"name": "isImportDeclaration", "from": "typescript", "as": "ts_isImportDeclaration"}]' (name default or * with as; "type": true for a type only), or as imports in a patch of --patches; it goes too if the code doesn't use it. The change is shown as lines removed and added; problems the check finds, and the line most like an old text that isn't there, name the definition and its line as tcm view numbers it, the edits made (lib.f:8:36; imports added aren't counted)
tcm at <file>[:<line>]
which definition is at a line of a file (a scratch file, a definition's document in its branch's views, .tcm/src or a session's .tcm/sessions/<branch>/src, a generated module in .tcm/cache as stack traces name them, a mirrored file), or every definition in it with its lines. A view reads as its branch has it now; another branch's is that branch's to say (--branch <it>). A module named by its component's hash (<hash>.ts in the cache, modules/<hash>.js in a packed package), even one that isn't here, names its component's definitions
tcm view <name|#hash>... [--lines a-b] [--grep text]
show definitions, rendered with current names, with their placement for app code; --lines or --grep (the lines with the text, as written, and two either side; or matching a regular expression written /…/, flags after it: --grep '/get|set/i') shows only some of each, numbered. --lines takes a line (120), a range (120-180), from a line on (120-) or several (1-20,90-). A name that isn't there is said first, with the names meant most likely (or, for a namespace, what it holds), and the rest are shown; the exit code is then 1. edit, rm, patch and the rest say what was meant the same way
tcm ls [namespace]
list names
tcm find <query>
the names with every word of query in the name, signature text or doc (finding code)
tcm search <text> [--regex] [--case] [--word] [--under ns] [--limit n]
find text in the code and pages, as find in files does: each line with it, by definition, numbered as tcm view --lines counts. The text is as written, or a regular expression written /…/, as --grep takes it (--regex, the old way, takes all the text as one); --under keeps to a namespace or a definition. It stops at 500 definitions or 5000 matches, and says so (finding code). Text that starts with - goes after --: tcm search -- --lines
tcm find ':<type>'
search by type: ':(xs: T[]) => T'; unknown names are type variables (search)
tcm deps <name> / tcm dependents <name>
what a definition uses / what uses it now (the named definitions that refer to it)
tcm docs <name>
show a definition's documentation, rendered: links and embeds with current names, examples evaluated (docs)
tcm run <name> [args...]
run a definition. Functions are called with the args; the result is awaited and printed. What it starts finds the codebase's commands in <COMMAND>_BIN (TCM_BIN), unless the caller set one: TCM_BIN=<tcm.js> tcm run <e2e test> runs an end-to-end test against that tcm (and it's said). One a tcm run further out set (the codebase's build as it was then: one of its runners in .tcm/cache) is replaced. tcm run --bin <command> -- [args...] runs a command of the codebase's packages (a manifest's bin) instead
tcm mv <from> <to>
rename a definition or namespace (no code changes)
tcm alias <name> <new>
add another name for a definition
tcm rm <name>... [--force]
remove names, each in turn. If it's the definition's last name and named definitions use it, refuses unless --force (they keep working either way); ones being removed in the same command don't count
tcm todo [--all]
conflicts, plus dependents that need fixing after an update (the first 20, the rest counted: fixing those often fixes them; --all lists every one); and code using a version nothing names (left behind by a change it couldn't follow, or a name removed with --force)
tcm status
where things stand: the branch, which tcm this is (its build, the hash of its code), the codebase by namespace, scratch files holding unsaved work and what saving them would do, todo, other branches with work main doesn't have, and what this one has (history entries, and the names they changed), session directories whose branch is gone that hold unsaved work, whether a mirror's files are in step
tcm fmt [namespace]
reformat stored definitions with oxfmt; no hash changes (formatting)
tcm lint [namespace] [--fresh]
lint definitions with oxlint, cached by hash (linting)
tcm packages
the npm packages the codebase runs: range, locked version, what uses them (packages)
tcm upgrade <pkg> [--force]
newest versions within the ranges in use (and of what's built against it); no hashes change, everything using them is re-checked
tcm upgrade <pkg>@<range|latest> [namespace]
re-pin code to another range: re-hashed, dependents follow
tcm lib
the libraries this branch holds: where each is mounted, what it is, where it came from (libraries)
tcm lib add <remote|url|dir|tcm> <package>[@<version>]|#<hash> [--as <name>] [--at <name>]
fetch a release's namespace (<package>: its newest), or a namespace by its whole hash, with what it uses, and mount it read-only at <root>.lib.<name> (or --at), recorded beside it; it's called after its package (@alice/geo-tools: geo_tools) unless --as says otherwise, which a hash needs. tcm is the libraries this tcm ships (tcm lib add tcm react, offline)
tcm lib add @<handle>/<package>[@<version>] [--as <name>] [--at <name>]
the same for a release its author announced on the AT Protocol (default: the highest version they announced), fetched by hash from the servers its announcement names, and recorded with their DID (announced releases)
tcm lib upgrade <name|mount> [<package>@<version>|#<hash>] [--source s] [--check]
move a library to its package's newest release (or the one given; for an announced one, the highest its author's DID announced), from the first of its sources that has it (--source: tried first), as one update: its API's changes first (--check: only those), then code using it follows; what no longer typechecks goes to todo. Refused while code uses what the new one doesn't have
tcm lib sources <name|mount> [--add <s>] [--rm <s>]
where a library is fetched from, tried in order (each answer is checked by hash): add a mirror (a remote, URL or codebase directory), or stop fetching from one (not the last) (sources)
tcm lib attest <lib|@who/pkg@ver|pkg@ver|#hash> review|advisory|yank [-m text] [--severity s] [--fixed-in v] [--as handle]
say something about a release, signed in your AT Protocol repository: a review, an advisory (--severity low|moderate|high|critical), or, its author's alone, a yank (what people say)
tcm lib search <query> [--hub <url>]
find libraries on a hub: what its libraries' newest versions hold that matches, by name, words in docs or :a type, each with what matched and its tcm lib add line; the hub is kept for next time, and without one it's tcm's, https://tcm.codes/hub (finding one)
tcm lib trust [--add <handle>] [--rm <handle>]
whose reviews and advisories tcm lib, add and upgrade show, besides each library's author's: kept in the codebase's history
tcm lib rm <name|mount>
remove a library's names and its record entry; refused while code outside it uses what only it names
tcm pack <package>[@version] [--out dir]
an npm package of a package (a manifest definition's namespace): a release of it, or the branch as it is (publishing)
tcm pack [[subpath=]<namespace|name>...] --name <pkg>[@version] [--bin cmd=<name>,...] [--with pkg,...] [--files a,b] [--out dir]
an npm package of the definitions given (publishing)
tcm release [<package>][@version] [--draft] [--notes file] [--remote r] [--force] [--announce [--as <handle>] [--source <url>]…]
release a package at the version its API's changes call for (or a further one), with notes whose list of changes tcm writes; a mirrored codebase's are recorded with its files, for git (releases); --announce writes a codes.tcm.release record to your AT Protocol repository, signed in in the browser once (an existing version: announced as it is), fetched from --source (kept) or origin (releases)
tcm release check [package]
what the next release would be, and each semver finding behind it; and what would stop it (work left, a tcm library using what isn't its own), exiting 1
tcm release view <package>@<version>
a release's notes
tcm releases [package]
the packages and their releases
tcm changelog [package] [--out file]
a package's changelog, from its releases
tcm resolve <name> <#hash> | --ours | --theirs
pick one definition for a conflicted name, by hash or by its side of the merge (--ours: this branch's, --theirs: the merged branch's, as tcm todo labels them, followed through what re-derived it since); code that used the others (each side's, after a merge) is re-derived against it; history names it resolve <name> (ours) (or (theirs)). To combine them instead, tcm edit the name (merging)
tcm test [names...] [--fresh] [--definitions | --suites] [--bin cmd=<path>,...]
run tests (all of them, or the ones named and the ones under the namespaces named), cached by hash, then the suites packages declare, cached by the code they ran; --definitions skips the suites, --suites runs only them, --bin runs them against another build of a command, like a package packed from the codebase, or an older tcm.js (for tcm, a kept build's name too: --bin tcm=known-good) to see a new end-to-end test fail before a fix (testing). Only passes are reused: a test that failed runs again. Suites, and test definitions' Vitest projects, run as many at once as the machine's budget allows, shared by every tcm on it with its builds, checks and verifies: TCM_SLOTS (how many; by default from the machine's cores and memory; TCM_SUITE_SLOTS, its name before, still works), TCM_SLOTS_DIR (where the slots are) (testing)
tcm migrate [--if-needed]
move a codebase made with an older hash format to this tcm's, history and all (hashing); the database as it was is kept beside it (.tcm/codebase.v<n>.sqlite), and the migrated one used only if it reads back as itself. --if-needed says nothing when there's nothing to do
tcm verify [--fresh]
re-render and re-hash every live definition; must be the identity. What this same tcm verified already, named as it is now, isn't checked again; --fresh checks everything (hashing)
tcm builds
the tcm builds kept (where TCM_BUILDS says: tcm's repository's dist/): known-good, previous and older ones, each with the history entry it was made from, when, what changed in it, and its tcm.js (developing)
tcm builds make <entry>
a build of the codebase's tcm as it was at a history entry, packed from the codebase then and kept with the others, to run with --build

34.2 History and branches

See history, branches and merging.

tcm diff
what the last command changed: edits, dependents re-hashed by them, renames
tcm diff <n|#hash> | <branch> | <a> <b>
changes since a history entry, to another branch, or between two branches or entries
tcm diff <#def> <#def> / tcm diff <conflicted name>
two definitions' code, line by line, as tcm view shows it; a conflicted name's: the merge base's against each side's
tcm history [n] [--all]
this branch's history, newest first: an entry per command, with its hash, how many back it is, and what it changed by name, the definitions only re-hashed because a dependency changed counted (--all lists them); the latest 30 unless n (or -n <n>, --limit <n>) says. An entry is named by its command and what it changed (update lib.f, lib.g (+3)), and by its message where it has one (its first line; the rest, and the command, under it)
tcm describe [n|#hash] [-m <message>]
say what a history entry is for (default: the latest): its message, which no hash has, so nothing else changes; -m "" takes it away, and without -m it's shown. A codebase the entry was pushed to keeps the message it had (messages)
tcm reset <n|#hash>
bring back the names as of an entry (tcm reset 1 undoes the last command), as a new entry
tcm branch [name] [--delete] / tcm switch <name>
list, create or delete branches (not main, the one you're on, nor one a session's server is running on; its session's directory goes too, unless it holds unsaved work) / switch (not in a session: TCM_BRANCH keeps a process on its own branch)
tcm merge <branch> [--no-fast-forward] [--squash] [--abort]
merge into this branch: nothing if it has the other's history, a fast-forward if the other has its (or, with --no-fast-forward, a merge entry all the same, so what came in is one entry), otherwise three-way from their common ancestor (--squash: one entry whose only parent is this branch's, so the other's entries stay its own and pushing sends only what this branch names; how sessions land): a definition both sides edited is merged as text where the edits are apart and it typechecks, and every definition is re-derived against what the merge names. A merge that leaves a conflict, or code that stops typechecking, waits apart, this branch untouched, with scratch files holding what's to settle; commands on the branch work on it, and once nothing's left it's made (--abort drops it) (waits apart). On main, or with --squash, the branch lands: main merged into it first, there, then its tcm todo and tests, and only then the branch on main (landing)
tcm revert <n|#hash>
undo one history entry (a landing, say) and nothing after it, as a new entry: what it changed goes back, three-way, so changes made since stay; one made since where it changed things is a conflict (tcm resolve)

34.3 Sync and remote execution

See sync.

tcm remote [add <name> <url|dir> [--token t] [--fingerprint sha256] | rm <name>]
manage remotes: a tcm serve URL or another codebase directory
tcm push [remote] [--as branch] [--force]
send this branch's history (what the remote lacks) and move the remote branch to it (a contributor's, under @<handle>/). Refuses if the remote has history you don't, unless --force
tcm pull [remote] [branch]
fetch the remote branch's history into <remote>/<branch>; fast-forward this branch if it can, merge from the common ancestor otherwise (one that leaves something to settle waits apart, as tcm merge's does; on main, it isn't made)
tcm run <name> [args] --on <remote|url|dir>
send what the remote lacks, run there, stream stdout/stderr back
tcm contribute [remote] [--to main] [--as name] [--title t] [--description d]
push this branch (a contributor's as @<handle>/<branch>) and open a contribution of it on the remote (pushing again updates it) (contributions)
tcm contribution [--remote origin] [--all]
the remote's contributions
tcm contribution view|comment|merge|close|reopen <id>
a contribution's changes, whether it merges, and comments; comment (--about <name>); merge it on the remote by history (refused on conflicts); close or reopen it
tcm serve [--port 7737] [--host 127.0.0.1] [--token t] [--no-run] [--public] [--tls [--tls-cert f --tls-key f]] [--insecure] [--site dir] [--moved /path] [--proxy /path=url[|url],…]
serve this codebase over HTTP(S), and a static site (--site) and apps beside it (--proxy) to anyone, the site's old addresses redirected to where its pages moved (--moved); --public lets anyone pull and read contributions without a token (sync)
tcm serve's web UI
at /forge/ (and / without --site): the codebase's pages, search, history, and contributions reviewed in the browser (web UI)
tcm user [--remote origin]
who you are on a remote: its maintainer (and its contributors), a contributor, or a reader with no token
tcm hub [--port 8080] [--host 127.0.0.1] [--dir tcm-hub] [--base /path]
run a hub (under a path of a site, with --base): it finds releases announced on the AT Protocol, checks each against its author's signed record, mirrors its code by hash and shows it; visitors sign in with their AT Protocol account to star libraries and see what people they follow, and the libraries they use, are up to (hub)
tcm user add|rm <handle> [--remote origin]
the maintainer's: add a contributor (printing their token, once; again, a new one), who pushes branches under @<handle>/ and proposes them; or remove one, stopping their token (contributors)

34.4 Apps, data and jobs

See browser apps, production, data and jobs.

tcm dev <app> [--port 5173] [--http <namespace>[=/prefix]]
serve a browser app from the codebase with hot reload; <app> is a function run on load, or a React component rendered into the page
tcm build <app> [--force] [--static <dir> [--base /path]]
immutable production build into .tcm/apps/<hash>/ (reused if it exists); with an app's pwa, its manifest, icons and service worker (PWAs). --static writes it out as a static site, for any static host, for the site's root or --base: each page the app links to rendered as it's built, server components included; an app that calls server functions is refused (static sites)
tcm start <app> [--port 8080] [--host] [--tls] [--keep N] [--http <namespace>[=/prefix]] [--base /path]
serve in production, following the app's name; --base serves it under a path of a site (deploying)
tcm apps [prune [--keep 3]]
list builds (* = current); prune keeps the current build plus the newest N others
tcm data [table [n]]
typed tables: row counts, or a table's newest rows
tcm data migrate <table>
upgrade every stored row to the current row type now, through migrate
tcm jobs [n] | retry <id> | cancel <id>
list background jobs, or act on one

built from c482a14e