@loomcli/loom
v0.18.0
Published
The Loom CLI toolchain.
Readme
@loomcli/loom
The Loom CLI toolchain. It exports no module. Its loom bin acts on one package: the directory of the nearest package.json at or above the working directory. loom build does its work under Bun, so Bun must be on the PATH.
Install
pnpm add -D @loomcli/loomInit
loom init scaffolds a new application in an empty directory, or adds the pieces an existing package lacks.
mkdir notes && cd notes
bunx @loomcli/loom init # package.json, src/, its test, the lint configuration, tsconfig.json, .changes/README.md, and the changelog skill
pnpm install
pnpm checkIn an empty directory nothing is installed yet, so the first run goes through the package runner. Inside a package that already depends on @loomcli/loom, the bin is loom.
- Paths. Each path below is a convention. A
loom.config.jsonbesidepackage.jsonmoves any of them through itspathskey, such as"source": "lib"or"installer": "../../install.sh", and holds the installer'spostInstallstep. See the configuration file. - Scaffold files.
src/application.ts,src/main.ts,tsconfig.json,src/application.test.ts, the lint file (.oxlintrc.jsonoreslint.config.js), and thepackage.jsonkeysbin,scripts.build,scripts.check, the Loom dependencies, and the tool packages at the ranges@loomcli/loomdeclares are written once, only when missing. Init never overwrites them, because they are your code from then on. - Managed files.
.changes/README.md, the fragment guide,.agents/skills/loom-changelog/SKILL.md, the changelog skill, andinstall.sh, the installer script, carry a header with a checksum. Init re-renders an unedited one, so it follows the installed toolchain. An edited one draws a warning and is left alone, and--forcerestores it. Delete the header to keep the file as your own. - The installer script. When
package.jsonrepositorynames a GitHub repository, such asowner/name, init writesinstall.sh, executable. Under POSIXshon Linux and macOS it downloads the platform's binary from the repository's GitHub Release, verifies it against the release'sSHA256SUMS, installs it into~/.local/binor the--dirit is given, and records the installation. A package whoseloom.config.jsondeclarespostInstallgets a script that then runs its post-install command, which--skip-post-installskips, and prints its post-install message. Without a GitHub repository, init writes no script and prints a warning. See the installer script for its arguments and the release shape your release workflow publishes. - The check script.
scripts.checkrunstsc --noEmit, the linter, and the test runner. The test passes the Application to the assembly check of@loomcli/testing, so a fault only the assembled application reveals fails there. - Options.
--only <piece>limits init toapplication,entry,tsconfig,test,lint,package,changes,skill, orinstall, and may repeat.--linter oxlint|eslintand--test-runner bun|vitestchoose the tools, by default oxlint and Bun. A lint configuration or avitestdependency the package holds selects its tool, and at a terminal init asks when nothing does. Init installs nothing, so run your package manager afterward.
See loom init for the contract.
Changelog
A package keeps its pending changes as fragments in .changes/, or the folder paths.changes names in loom.config.json, and cuts its package.json version from them. CHANGELOG.md is likewise the default of paths.changelog.
mkdir -p .changes
printf -- '- Add the `--tag` option to `notes list`.\n' > .changes/feature.list-by-tag.md
loom changelog check
git add .changes && git commit -m 'Add list-by-tag fragment'
loom changelog write --dry-run
loom changelog write --date 2026-10-07
loom changelog notes 1.5.0 # the 1.5.0 section's body, for a release page
loom changelog write --target 2.0.0-rc.1 # sets the version and keeps the fragments pending
loom changelog notes 2.0.0-rc.1 # prints the pending fragments
loom changelog write # finalizes 2.0.0 and consumes them- Fragments.
breaking.<slug>.mdis a breaking change and carries one### Migrationsection with the five labels,feature.<slug>.mdis a feature, and<slug>.mdis a fix. A body is Markdown bullets with no frontmatter or headings..changes/README.mdis the guide, not a fragment. loom changelog checkvalidates every fragment and names each invalid one. It needs no git history, so CI runs it.loom changelog writecomputes the next version from the highest kind present, renders one section with### Breaking Changes,### Features, and### Fixes, prepends it toCHANGELOG.md, setsversioninpackage.json, and deletes the consumed fragments. From1.0.0, breaking advances the major, feature the minor, and fix the patch; below it, breaking advances the minor and anything else the patch. It orders fragments by the commit that added them, so it needs full git history.--date YYYY-MM-DDdates the release,--narrative FILEcopies prose above the entries, and--dry-runprints the section and the next version and writes nothing.loom changelog write --target <version>cuts the version it names,MAJOR.MINOR.PATCHwith an optional-prerelease part, instead of the computed one. It must rank above the current version, and its release part must meet the version the pending fragments need, so a fragment keeps its kind whatever version ships it. A prerelease target sets the version, writes no changelog section, and keeps every fragment pending; from a prerelease,writewithout--targetcuts the release part. A prerelease cut from a stable version other than0.0.0needs that version's section in the changelog.sharedVersioninloom.config.jsonlistspackage.jsonfiles that carry the package's version, such as["../hub/package.json"], which the same cut sets. A value that is not a list of nonempty strings, or a listed file that is missing or holds another version, fails the cut before any file changes.loom changelog notes <version>prints a cut version's changelog section, or the pending fragments when the version is the package's current prerelease, and changes nothing. A version with no section fails.
write refreshes no lockfile; run your package manager after the cut. See The package changelog for the contract.
Build
loom build bundles the package's application for one target and bakes its release facts into the __LOOM_RELEASE__ identifier, which core reads into host.release.
loom build --target node # dist/main.js and dist/application.js, { build: 'distributed' }
loom build --target bun-linux-x64 --release # dist/<name>, a binary with its version, repository, and asset
loom build --watch --target node # rebuilds on change, { build: 'development' }- Targets.
--targettakesnode,bun, or a Bun compile target such asbun-linux-x64, by default the host's compile target. One call builds one target. - The modules. The entry is
src/main.ts, orpaths.entryinloom.config.json, or--entry. A bundle also carries the application module,src/application.ts,paths.application, or--application, as a file named for the module, such asapplication.js, so a test imports the built Application with its baked facts. When the conventional application module does not exist, the bundle carries the entry alone. - The release.
--releasereadsversionandrepositoryfrompackage.json, and--repository <owner/name>overrides the field. A binary's path and asset name read the application's name frombin, or--name. - A failed build changes nothing. The build writes into a temporary directory and moves its files into place only once it succeeds.
A custom build composes the same facts into its own bundler call:
bun build src/main.ts --outdir dist --target node --define "$(loom build --target node --define)"--facts prints the facts JSON instead, for a bundler's define option. See loom build and Release facts for the contracts.
