dist.town

@graham.systems /dist-town

cli

beta 0.5.1latest 0.7.0

Latest

dist-town/cli 0.7.0

The command is now just disttown publish. disttown.jsonc A repo can commit a manifest describing what it publishes: the project, the artifact globs, the storage choice, and the account expected to do the publishing. It's JSON with comments (the .jsonc extension is the honest…

Sep 3, 2026

dist-town/cli 0.7.0

The command is now just disttown publish.

disttown.jsonc

A repo can commit a manifest describing what it publishes: the project, the artifact globs, the storage choice, and the account expected to do the publishing. It's JSON with comments (the .jsonc extension is the honest label — editors just work), it resolves by walking up from wherever you are (monorepo sub-projects get their own), and a published JSON Schema at dist.town/disttown.schema.json gives you completion and validation as you write it.

disttown init writes a commented scaffold, proposing the project name and detecting dist/.

Inference, shown

With a manifest committed and an annotated tag at HEAD, publish infers everything: the tag proposes the version (a leading v before a digit is stripped), the manifest supplies the files, the storage choice routes the bytes. Nothing inferred is ever silent — interactive publish renders the whole plan first (every value with its source, every file with its digest and destination, every pointer move) and asks. --yes skips the question; non-interactive runs behave exactly as before, so no CI pipeline changes.

Explicit arguments override the manifest everywhere. No manifest means the CLI you already know, unchanged.

Storage profiles

The machine defines buckets, repos pick them by name: disttown storage setup --profile cdn records the definition, a manifest says "storage": "cdn", and per-entry overrides route individual globs elsewhere. "blobs" is reserved for bytes-on-your-own-PDS. Upload commands and credentials never live in the repo, so a cloned manifest carries no secrets. DISTTOWN_STORAGE_BASE_URL / _UPLOAD_CMD serve CI without any files.

The publisher guard

The manifest records which DID it expects to publish. Logged in as someone else, publish stops before a byte moves and says so — the multi-account save. The other resolution is adoption: the manifest is rewritten to expect you, and the original project reference moves into derivedFrom — fork lineage, captured at the only moment the CLI can know it, preserved comment-for-comment. The claim reaches the project record and renders on the fork's page as exactly what it is: a claim.

Also

  • Every command that takes a project now infers it from the manifest when you don't say — release list bare, inside a governed repo, means that repo's project, and the output says so.
  • Manifest globs are containment-checked: never absolute, never escaping the repo, symlinks resolved — a hostile manifest can't publish your dotfiles.
  • storage doctor --profile <name> checks a named profile.
  • Nix builds of the CLI now stamp their version (0.7.0+nix) and update source instead of reporting dev.

Sep 3, 2026 dist-town/cli@0.7.0

dist-town/cli 0.5.0

One new command, and it's the point of the whole thing.

disttown update

The CLI now updates itself — and it does it without touching a single dist.town service. It resolves its publisher's identity, follows the latest pointer in their repo, fetches the release record, downloads the artifact for your platform, and verifies it against the digest in the signed record. If dist.town went dark tomorrow, this command would not notice.

Before replacing anything, it tells you where the update comes from — publisher handle and DID, version, publication time, digest — and asks. --yes consents for automation; --check reports without acting (exit 2 when an update is available, for scripts); --channel beta follows a channel instead of latest.

Some behavior worth knowing:

  • A yanked release is never installed. If latest points at one, the updater reports no update and stays put.
  • If the publisher rolls latest back to an earlier release, the updater says so plainly — a rollback is followed with your consent, never silently.
  • Package-managed installs (nix store, system paths) are left alone, with a pointer to the right tool.

Also

  • publish and the lifecycle verbs no longer write a pointer update when the pointer already targets the release — a no-op record update turned out to be something firehose consumers can choke on.

Sep 1, 2026 dist-town/cli@0.5.0

dist-town/cli 0.4.0

This release gives the CLI read capabilities, and reorganizes some commands in the name of consistency. There are breaking changes as a result.

The read surface

  • disttown release list [project] — what you've published, newest first, with channel and lifecycle state.
  • disttown release show <project> <version-or-pointer> — one release in full: digests, download locations with verification state, pointers, source claim, changelog. release show mytool latest works.
  • disttown project list and disttown pointer list <project> — your projects (aliases included) and where every pointer aims.
  • disttown whoami — who this machine publishes as, and whether your cached session's scopes still cover everything. The stale-session surprise now surfaces here instead of mid-publish.
  • disttown logout — clears the local session, but does not delete the session server-side.

Reads consult dist.town's index by default — the same public surface the pages use. --pds reads straight from your own repo instead, and --compare reports exactly where the two disagree (useful right after publishing, before the index catches up). Every read says which source answered, and everything takes --json.

Breaking: lifecycle verbs moved, and the vocabulary is complete

disttown yank is now disttown release yank <project> <version> — and it brought company:

  • release deprecate / release undeprecate — "still works, stop building on it." Deprecated releases keep resolving, with a warning.
  • release unyank — every verb has a named inverse now; --undo is gone.
  • project archive / project unarchive — wind a project down without breaking anything it ever shipped.

All of them take --kind and --reason — the inverses too, because "why it came back" is as much history as "why it left."

Breaking: --channel changes what latest means

disttown publish 2.0.0-rc1 --channel beta now moves the beta pointer and leaves latest alone. Previously it moved latest — which meant a release candidate could silently become what every updater and install script sees. The rule is one sentence: the default pointer follows the channel. Want a channel release on latest? Say so: --pointer latest --pointer beta. Naming any pointer replaces the default entirely. Channelless publishes are unchanged.

Fixed: publish could hang forever under OAuth

A logging deadlock could freeze the CLI — silently, permanently — on the first library log line emitted after login, which in practice meant the first blob upload of a publish. It could also invalidate your cached session on the way down, forcing a re-login.

Publish is harder to hurt yourself with

  • Scope problems now fail before anything uploads.
  • publish --dry-run prints the whole plan — digests, records, which pointers move — touching nothing. CI can gate on its exit status.
  • If a publish fails partway, the CLI names every file already uploaded (content-addressed, so retrying reclaims them; nothing is deleted).
  • disttown doctor [project] checks the whole pipeline in one go: session, scopes, PDS, storage, and whether dist.town's index can see you — with "publishable but not yet visible" clearly told apart from anything actually wrong.

Sep 1, 2026 dist-town/cli@0.4.0

0.3.0 — releases that say where they came from

Releases can now say where they came from — and this one does.

Source linking

A release can pin the git commit and annotated tag it was cut from, right in the immutable record. publish --tag v0.3.0 reads both hashes from your local checkout; since git is content-addressed, the tag object hash transitively pins the entire source tree. Names are labels, hashes are truth: force-moving a tag never moves a published claim.

Set your repository once with the new disttown project set --source, and every release inherits it. When the source is a record in your own atproto repo — a Tangled repository — the association is signed by the same key as the release itself, and release pages badge it same-repo. This is honest verification: it proves association, not build provenance.

Tangled artifacts are first-class

If your CI uploads artifacts to Tangled, dist.town now treats them as locations rather than rivals:

  • At publish time, tag-matched Tangled artifacts are offered for attachment (--attach-tangled for CI). The CLI downloads and hashes every blob locally — each digest in your release is your own measurement, never the forge's claim.
  • Afterward, any Tangled upload whose bytes match a declared digest becomes an additional verified download location, including for releases published before the upload existed.
  • Uploads that match nothing appear under an explicit "not in the record" caveat — visible, and honestly unverified.

Windows builds

This is the first release with Windows artifacts (amd64 and arm64, as zip archives), alongside Linux and macOS. The CLI is pure Go, so the whole matrix cross-compiles from one machine — tools/dist.sh in the repo is the two-line recipe. disttown --version now exists, stamped at build time.

Also

  • Release pages show the source claim in the record rail: repo, tag, and commit, with copy-to-clipboard hashes.
  • project set updates a project's display name, description, source, and website — only the flags you pass change.

Aug 19, 2026 dist-town/cli@0.3.0

dist-town/cli 0.2.0

The release that renamed itself: disttown now lives at dist-town/cli, published under the nested project name this version introduces. The old name remains a permanent alias — every existing URL keeps resolving.

Nested projects

  • Project names are now paths: dist-town/cli on the command line and in URLs, dist-town:cli in the record key. Flat names remain valid — nesting is never required.
  • Hierarchy lives in the name itself: no new lexicon, no parent field, and parentage derives by prefix. An umbrella page lists its children automatically.
  • disttown project create writes a bare project record (umbrellas), and disttown project rename performs the alias dance atomically: successor created, old record marked renamedTo, history staying exactly where it happened.

Changelogs

  • disttown changelog set <version> attaches release notes to any published release — including ones published before this command existed. Notes are plain site.standard.document records carrying at.markpub.markdown content with a derived plaintext fallback, so they render in any standard.site reader.
  • Re-running the command edits the existing document in place rather than shadowing it.
  • disttown publish --notes <file> lands the notes in the same atomic commit as the release. These very notes shipped that way.

Housekeeping

  • OAuth sessions gain the site.standard.document scope; cached sessions need one disttown login re-run before authoring changelogs.
  • --project accepts both the URL form (dist-town/cli) and the record-key form (dist-town:cli) everywhere.

Aug 15, 2026 dist-town/cli@0.2.0

Report this page