Tutorial: your first codebase — tcm-ts

Tutorial: your first codebase

You'll write a few functions, save them to a codebase, change one, rename, undo, and add a test and a doc. The output shown is what tcm prints.

1. Make a codebase

With tcm installed, in an empty directory:

mkdir tutorial && cd tutorial
tcm init

This makes a codebase and a scratch.ts to write in.

2. Write something

scratch.ts is ordinary TypeScript. Below its opening comment it has:

/** Square a number. */
export const square = (x: number): number => x * x;

square(12);
$ tcm check
scratch.ts
  + new       square : (x: number) => number

    7 │ square(12)
      ⧩ 144

  run `tcm add` to save 1 definition

square is new: the codebase doesn't have it yet. square(12) is a watch expression: tcm ran it and showed its value. Watch expressions are never stored.

To check on every save instead, leave tcm (no arguments) running in a terminal. The VS Code extension shows the same results as you type.

3. Add it

$ tcm add
  + new       square : (x: number) => number

square is in the codebase now. Delete it from scratch.ts and it's still there: files are where you write, the codebase is where code lives.

4. Use a namespace

Replace scratch.ts with:

// tcm: namespace geo

/** A point on a plane. */
export type Point = { x: number; y: number };

/** The distance between two points. */
export const distance = (a: Point, b: Point): number => Math.hypot(a.x - b.x, a.y - b.y);

distance({ x: 0, y: 0 }, { x: 3, y: 4 });
$ tcm add
  + new       geo.Point type
  + new       geo.distance : (a: Point, b: Point) => number

$ tcm ls
geo.Point     #no6urvx6  type
geo.distance  #vra42ccj  : (a: Point, b: Point) => number
square        #kor774ja  : (x: number) => number

The first line put the file's definitions in the geo namespace. The #… after each name is its hash, short for the hash of its code. A name points at a hash; the hash is the definition.

5. Use what you saved

Import from #codebase (the root namespace) or #codebase/<ns>:

// tcm: namespace geo
import { distance, type Point } from "#codebase/geo";

/** How far you travel visiting the points in order. */
export const pathLength = (ps: Point[]): number => ps.slice(1).reduce((n, p, i) => n + distance(ps[i]!, p), 0);

pathLength([{ x: 0, y: 0 }, { x: 3, y: 4 }, { x: 3, y: 0 }]);
$ tcm add
  + new       geo.pathLength : (ps: Point[]) => number

6. Change something

Round distances to two decimal places. The scratch file only needs what you're changing:

// tcm: namespace geo
import type { Point } from "#codebase/geo";

/** The distance between two points, to two decimal places. */
export const distance = (a: Point, b: Point): number => Math.round(Math.hypot(a.x - b.x, a.y - b.y) * 100) / 100;
$ tcm update
  ~ update    geo.distance : (a: Point, b: Point) => number  (was #vra42ccj)
  ~ updated   geo.pathLength (dependent)

pathLength uses distance, so tcm checked it against the new version and updated it too. Had it stopped typechecking, it would have kept its old version and gone on your todo list (tcm todo) with the error. The codebase is never left half-updated.

7. See what changed

$ tcm diff
changes from `update geo.distance`

  edited
    ~ geo.distance  #vra42ccj → #vwvxl53m
        /**
      -  * The distance between two points.
      +  * The distance between two points, to two decimal places.
         */
      - export const distance = (a: Point, b: Point): number => Math.hypot(a.x - b.x, a.y - b.y)
      + export const distance = (a: Point, b: Point): number => Math.round(Math.hypot(a.x - b.x, a.y - b.y) * 100) / 100

  updated because a dependency changed (code unchanged)
    ~ geo.pathLength  #kuir3xdg → #3ah7ke6j  via geo.distance

8. Rename something

$ tcm mv geo.distance geo.dist
moved 1 name
  note: geo.dist is still named `distance` at runtime (`.name` is part of its hash); changing that means editing it

$ tcm view geo.pathLength
// geo.pathLength #3ah7ke6j : (ps: Point[]) => number
// runs anywhere
import { dist } from "#codebase/geo";
import type { Point } from "#codebase/geo";

/**
 * How far you travel visiting the points in order.
 */
export const pathLength = (ps: Point[]): number => ps.slice(1).reduce((n, p, i) => n + dist(ps[i]!, p), 0)

pathLength shows the new name, and its hash (#3ah7ke6j) didn't change: a rename moves a label, with nothing to find and replace. The note is because a function's .name is visible at runtime, so tcm mv leaves it alone. "Runs anywhere" means this code needs neither a server nor a browser.

9. Undo

$ tcm history 2
#upoceguk   0  2026-10-28 06:21:30  mv geo.distance geo.dist
    geo.dist                     + #vwvxl53m
    geo.distance                 - #vwvxl53m
#eizpcqxs   1  2026-10-28 06:21:30  update geo.distance
    geo.distance                 #vra42ccj → #vwvxl53m
    and 1 updated because a dependency changed (`tcm history --all` lists them)

Each command is one history entry. Go back one entry, to before the rename:

$ tcm reset 1
names as of #eizpcqxs (1 added, 1 removed)

distance is called distance again. The reset is a new entry and code is never deleted, so you can always go back.

10. Test it

A test is a definition whose name starts with test. It passes if it returns without throwing:

// tcm: namespace geo
import assert from "node:assert/strict";
import { pathLength } from "#codebase/geo";

export const testPathLength = () => {
  assert.equal(pathLength([{ x: 0, y: 0 }, { x: 3, y: 4 }]), 5);
  assert.equal(pathLength([]), 0);
};
$ tcm add
  + new       geo.testPathLength : () => void

$ tcm test
  ✓ geo.testPathLength  2ms

1 passed, 0 failed

$ tcm test
  ✓ geo.testPathLength  (cached)

1 passed, 0 failed (1 cached)

The second run ran nothing. A test's hash covers everything it calls, so its result is cached until that code changes. Change distance again and the test runs again.

11. Document it

Doc comments can link to definitions and hold examples, which tcm runs:

// tcm: namespace geo
import { distance, type Point } from "#codebase/geo";

/**
 * How far you travel visiting the points in order: the sum of the {@link distance}s between neighbours.
 *
 * ```ts
 * pathLength([{ x: 0, y: 0 }, { x: 3, y: 4 }, { x: 3, y: 0 }])
 * ```
 */
export const pathLength = (ps: Point[]): number => ps.slice(1).reduce((n, p, i) => n + distance(ps[i]!, p), 0);
$ tcm update
  ~ doc       geo.pathLength : (ps: Point[]) => number  (doc changed)

$ tcm docs geo.pathLength
How far you travel visiting the points in order: the sum of the `distance`s between neighbours.

```ts
pathLength([{ x: 0, y: 0 }, { x: 3, y: 4 }, { x: 3, y: 0 }])
```

```text
⧩ 9
```

The code matches what's stored, so only the doc changed. Links and example names are stored by hash, like code: rename distance and the doc shows the new name; update it and the example runs the new version. If an update breaks an example, tcm update says so.

12. Find it again

Search by name, or by type:

$ tcm find ':(ps: Point[]) => number'
geo.pathLength  #3ah7ke6j  (ps: Point[]) => number

Next

That's the daily loop: write in a scratch file, check, add, update, mv, history and reset, test, docs.

Built from c482a14e