@spunto/build
The parts of Spunto's Build pillar that are the same whoever runs the control plane — shared by Spunto Cloud and Spunto Lite.
@spunto/build holds the parts of Spunto's Build pillar that do not
depend on who runs the control plane. Spunto Cloud and
Spunto Lite both ship them, from the same published
version on npm.
Build is the pillar that turns a project (an image, some devcontainer features, some VS Code extensions, repositories, lifecycle commands) into a running worker: an isolated development container used by a human or an AI agent. Two products do this. They differ in almost everything around it, such as tenancy, database, Docker topology and UI. But a lot of what happens between a project and a worker has to come out identical on both sides:
- the shell script that bakes the image and the one that sets up the container,
- the markers that script prints so a UI can follow the build,
- the names of the containers, volumes and images it creates,
- the file a worker writes its progress to,
- the JSON file a project is exported to.
Before this package, each of those existed twice, typed by hand on each side, and the two copies had already started to drift in both directions. Now there is one copy, and it lives here.
"Looks" vs. "agrees on"
This package follows the same idea as
@spunto/design-system, one layer lower:
| Package | Owns | Its types are |
|---|---|---|
@spunto/design-system | what the two products look like: tokens, primitives, components | structural and lax: everything optional, so a card degrades instead of throwing on a payload it has never seen |
@spunto/build | what the two products agree on: protocols, names, file formats, generated scripts | canonical and strict: a shell-script generator must refuse a shape it does not know |
The two rules are opposite on purpose, and that is why this package cannot be folded into the design system. See Design rules.
A quick tour
Each entry point is a subpath import, and each one does a single job:
steps
The build-log protocol: ::spunto:step: markers that turn one opaque docker build into a list of live blocks.
extensions
Which extension registry, and how to ask it: Open VSX by default, or an operator-configured gallery.
naming
The container, volume, network and image names, and the paths a worker and its control plane share.
spec
A project as a portable file (kind: "spunto/project") you can carry between Spuntos.
script
The generators that turn a project into the shell a worker runs: image, setup, start, skills.
catalogs
What a project picker offers: images, features, suggested extensions.
types
ProjectFeature, Repository, SetupStatus and the worker status vocabulary that both ORMs point at.
agent-stream
Reading an agent session as it runs: one adapter per harness dialect, one vocabulary out.
terminal
The string half of a persistent terminal: dtach commands, session names, list parsing.
Several of these modules define wire formats: something one program writes and another reads, sometimes minutes later, sometimes in another language. Those formats are documented byte by byte in the Formats tab, because changing one of them is a migration, not an edit.
Quick start
npm install @spunto/buildThe package ships TypeScript source, so your toolchain has to transpile it (Next.js needs one line of config). See Installation.
Generate an image build script, then follow its progress in the log:
import { buildImageScript } from "@spunto/build/script"
import { BuildStepTracker } from "@spunto/build/steps"
import { projectImageRef } from "@spunto/build/naming"
const { script, steps } = buildImageScript({
features: [{ id: "node" }],
vscodeExtensions: ["esbenp.prettier-vscode"],
})
const ref = projectImageRef("my-app", 7) // "mp-proj-my-app:v7"
// …run `script` inside a docker build tagged `ref`, and for every chunk of its log:
const tracker = new BuildStepTracker(steps)
const { text, changed } = tracker.ingest(chunk) // markers become banners, steps light upExport a project as a file, and read one back:
import { buildProjectSpec, parseProjectSpec, projectSpecFilename } from "@spunto/build/spec"
const file = JSON.stringify(buildProjectSpec(project), null, 2) // kind: "spunto/project"
const name = projectSpecFilename(project.name) // "my-app.spunto-project.json"
const spec = parseProjectSpec(await uploaded.text()) // throws a toast-safe messageNothing in this package reads a database, opens a socket, or renders anything. It produces strings and parses strings. The I/O (running the script, forwarding the log, storing the file) stays with the product that owns the Docker socket and the database.