@smoothbricks/cli
v0.13.12
Published
SmoothBricks monorepo automation CLI
Readme
@smoothbricks/cli
@smoothbricks/cli provides smoo, the SmoothBricks monorepo automation CLI. It is the control plane for shared CI,
release, Git hook, package metadata, and publish validation conventions across SmoothBricks-style repositories.
The tool is intentionally convention-over-configuration. SmoothBricks repos use [Nx], [Bun], [Nix], and [devenv]
(activated natively, or through [direnv] in repos that have not migrated), so smoo assumes those pieces exist instead
of adding another local config file. Repos should be made correct by running the mutating initialization path, then kept
correct by the read-only validation path.
Install
Add the CLI to the root workspace:
bun add -d @smoothbricks/cliThe package exposes a Bun-native executable:
smoo --helpSmoothBricks' PATH entry tooling/smoo and the published bin/smoo both import built JavaScript from dist. Shell
entry builds the CLI before it can be called; the launcher loads no source transformer and has no source fallback.
Core Commands
smoo monorepo init [--runtime-only] [--sync-runtime]
smoo monorepo validate [--fail-fast] [--only-if-new-workspace-package] [--projects <projects>] [--repairs-pending]
smoo monorepo update
smoo monorepo check
smoo monorepo diff
smoo monorepo validate-commit-msg <commit-msg-file>
smoo monorepo check-public-denylist [--remote <name>] [revisions...]
smoo monorepo check-public-denylist --staged
smoo monorepo check-public-denylist --commit-message <commit-msg-file>
smoo monorepo record-sibling-projects
smoo monorepo sync-bun-lockfile-versions [--stage]
smoo monorepo list-release-packages [--fail-empty] [--github-output <path>]
smoo monorepo validate-public-tags
smoo monorepo setup-test-tracing (--all | --projects <projects>) [--dry-run]
smoo release npm-status
smoo release repair-pending [--dry-run [true|false]]
smoo release version --bump <auto|patch|minor|major|prerelease> [--projects <projects|all>] [--dry-run [true|false]] [--github-output <path>]
smoo release publish --bump <auto|patch|minor|major|prerelease> [--dry-run [true|false]]
smoo release retag-unpublished <tag...> [--to <ref>] [--push] [--dispatch] [--remote <remote>] [--branch <branch>] [--dry-run [true|false]]
smoo release bootstrap-npm-packages [--dry-run [true|false]] [--skip-login] [--package <name...>]
smoo release trust-publisher [--bootstrap] [--dry-run [true|false]] [--skip-login] [--package <name...>]
smoo secrets status [-R <owner/name|remote>] [--env <environment>] [--json]
smoo secrets set [name] [-R <owner/name|remote>] [--env <environment>]
smoo secrets sync [-R <owner/name|remote>] [--env <environment>]
smoo secrets run <group> <command...>
smoo github-ci nx-smart --target <target> [--name <check-name>] [--step <number>] [--mode <auto|affected|run-many>] [--stage <stage>]
smoo github-ci nx-run-many --targets <targets> [--projects <projects>] [--configuration <configuration>] [--collect-outputs <directory>]
smoo github-ci nx-deploy [--stage <stage>] [--mode <auto|affected|run-many>] [--select-tag <tag>] [--verify]
smoo github-ci apply-outputs <directories...> --source-sha <sha>
smoo github-ci dispatch-workflow --workflow <workflow> --ref <ref>
smoo github-ci ensure-pull-request --head <branch> --base <branch> --title <title> --body <body>
smoo playwright ensure chromium
smoo playwright run <command> [args...]
smoo nx refresh-test-timings <project>smoo nx-<tool> [args...] runs the smoo-nx-<tool> bin of a SmoothBricks tool package the CLI depends on
(@smoothbricks/failure-summary, @smoothbricks/nextest-extract, @smoothbricks/build-inputs, and the plugin's own
smoo-nx-* bins) with the remaining arguments, under smoo's runtime; the tool's exit status is smoo's. Those packages'
bin fields are the list: smoo nx-absent exits 64 naming every tool.
An option value smoo cannot honour is refused before the command runs, as one line naming the option, the value given
and what it accepts, followed by the command's help; the exit status is 1. That covers --pr (an integer from 1 through
999999999, in decimal), --stage on wrangler deploy-stage, wrangler deployed-version and github-ci nx-deploy
(staging, production or prN), --bump, --mode, and a --dry-run value other than true or false: read as
"not a dry run", a misspelled one would publish.
Initialization
smoo monorepo init is the fix-first command. It mutates the repository toward the SmoothBricks convention instead of
only reporting drift.
It currently:
- Updates managed CI, release, hook, and formatting files. The publish workflow is only written when the repo has owned release packages.
- Ensures the local
tooling/smoolauncher is executable when present. - Synchronizes root runtime versions inside devenv, or when
--sync-runtimeis passed. - Applies safe publish metadata defaults to
npm:publicpackages without inferring package ownership. - Normalizes internal workspace dependency ranges to
workspace:*. - Rewrites safe package scripts in packages with workspace dependencies so developer commands like
bun run testandbun run devstay available while delegating through Nx targets. smoo monorepo validate --fixcreates/updatestooling/package.json, keeps@smoothbricks/clithere instead of the root workspace package, and fills required workspace/devenv tool declarations.- Runs [
sherif --fix --select highest][sherif] for broad monorepo package hygiene. - Normalizes conditional export ordering so
typescomes first anddefaultcomes last. - Adds
srcto packagefileswhen development-only exports intentionally point at source files.
The workspace dependency rule is generic. smoo does not know about individual package names such as eslint-stdout.
For every root or workspace package.json, if a dependency name matches an actual package in the same workspace, smoo
rewrites that range to workspace:*.
Packages with internal workspace dependencies also need Nx-aware scripts so dependent builds run before local commands.
For safe build, test, typecheck, benchmark, dev, and preview commands, smoo monorepo validate --fix moves the real
command into package.json nx.targets.<target>.options.command, sets cwd to {projectRoot}, and replaces the
script with an nx run <project>:<target> alias. Continuous commands such as astro dev, vite dev, and previews get
an explicit output style on the alias and continuous: true on the Nx target. Astro/Vite dev servers use
--outputStyle=dynamic-legacy; other continuous targets use --outputStyle=stream. Simple leading environment
assignments are moved into nx.targets.<target>.options.env so commands such as
NODE_OPTIONS='--import=extensionless/register' astro dev remain shell-independent.
The rewrite is intentionally conservative. smoo does not rewrite deploy, database, release, sync, subtree, publish, or
pack scripts, and it rejects Nx target commands that recurse through package scripts such as bun run test. The reason
is dependency correctness without hiding unsafe operational commands behind generated Nx targets: workspace-dependent
packages should get ^build ordering for ordinary development commands, while publishing and deployment stay explicit.
smoo monorepo init --runtime-only only synchronizes root runtime versions. It is used from direnv setup so
packageManager, engines.node, and @types/node stay aligned with the active devenv shell without duplicating that
policy in the direnv script. The versions are those of the bun running smoo and the node on the shell's PATH,
the same binaries shell entry probes for drift; no checkout path is consulted, because the checkout's
tooling/direnv/.devenv/profile link is re-pointed by every activation of that checkout and once wrote another
evaluation's Bun over the shell's.
Bun types are also a root runtime policy. The root @types/bun version follows the exact packageManager Bun version,
while package manifests are not forced to depend on Bun just because a test tsconfig opts into Bun globals. If a package
does explicitly declare @types/bun, it names the root's catalog entry for it (below), and smoo owns the semantic
root [email protected] to @types/bun x.y.z relationship.
A root dependency these policies check (the tool devDependencies, @types/bun, @types/node, ttsc) may be a Bun
catalog reference, catalog: or catalog:<name>. smoo then judges the catalog entry Bun installs, names that entry in
a failure (package.json workspaces.catalog.nx must be ...), and update, validate --fix and init --runtime-only
repair the entry and keep the reference. Bun reads the catalogs from the object form of workspaces once it holds
catalog or catalogs, and otherwise from the top level of the root manifest; smoo reads them from the same place.
init --runtime-only writes the root manifest alone and runs no package hygiene. A member that declares a runtime type
the root pins (@types/node, @types/bun) therefore names it catalog:, the root names it catalog: too, and the
version lives once, in the root's catalog, where the runtime sync writes it. A range of the member's own stays behind
when the runtime moves, and validate's package hygiene (sherif --fail-on-warnings, multiple-dependency-versions)
then refuses the release, as it refused cowshed's ^26.6.5 beside the root's ^26.6.6 after Bun 1.4.4.
LMAO Test Tracing
smoo monorepo setup-test-tracing configures LMAO-backed Bun test tracing for workspace packages. It is a bulk wrapper
around the @smoothbricks/nx-plugin:bun-test-tracing generator, so the Nx plugin remains the single source of truth for
the files written.
Configure every workspace package:
smoo monorepo setup-test-tracing --allConfigure selected packages by Nx project name, package name, or package root:
smoo monorepo setup-test-tracing --projects cli,@smoothbricks/lmao,packages/nx-pluginThe command infers the op context module from each package's package.json name, assumes an opContext named export,
and imports defineTestTracer from @smoothbricks/lmao/testing/bun. Override those defaults when a repository uses a
different convention:
smoo monorepo setup-test-tracing --projects my-lib --op-context-export myOpContext
smoo monorepo setup-test-tracing --projects my-lib --tracer-module @scope/testing/bunUse --dry-run to print the nx g @smoothbricks/nx-plugin:bun-test-tracing ... invocations without writing files.
After setup, run smoo monorepo validate --fix to apply the broader SmoothBricks monorepo policy.
Validation
smoo monorepo validate is the read-only gate. It should pass in local shells and CI after packages have been built.
It checks:
- Managed file drift.
- Root package policy.
- Root Bun type version matches the Bun package manager version.
- Tooling policy: root
package.jsonowns workspace-level tools likenx,tooling/package.jsonownssmoo, andtooling/direnv/devenv.nixowns shell-provided tools likebun,git-format-staged, andfmt. The check holdstooling/package.json's@smoothbricks/clirange to^<the version bun.lock resolved>and never asks the registry, so a release published after a commit cannot refuse it;smoo monorepo updatemoves the pin to the latest stable release and installs it. Every@smoothbricks/*package the root declares must resolve inbun.lockto the version the installed CLI uses through its dependency closure (cli → nx-plugin → cowshed …): Nx loads the root's copy, sonx syncwould render the managed files with another nx-plugin thansmoodid. After its install,smoo monorepo updatepins each such package to the version the installed CLI uses and installs again when one moved. - Nx release policy, including project package release tags, project-level GitHub Release changelogs, and the temporary Bun lockfile versionActions hook.
bun.lockworkspace versions match package manifests.- Public package tag policy.
- Public package metadata.
- Workspace dependency ranges.
- Workspace-dependent package scripts delegate safe commands through Nx targets without recursive script runners.
- Nx target conventions and inferred-task setup.
- GitHub Actions workflows, composite actions, and Dependabot configuration with [
jactionlint]. - [
sherif] package hygiene, with warnings treated as validation failures. - Packed publishable package artifacts with [
publint]. - Packed publishable package type resolution with the [
attw][are-the-types-wrong] CLI.
The packed-package checks validate what [npm] users will install, not only the source tree. smoo packs each
publishable package (npm:public or npm:private) with [Bun], runs [publint] on the tarball, then runs
[attw][are-the-types-wrong] on the same tarball.
Packed dependency freshness uses the same runtime closure as release packing: dependencies, optionalDependencies,
and peerDependencies. Development workspace dependencies retain their workspace-range conversion and packed-manifest
checks, but do not require publication of private build or test helpers that consumers never install.
The [attw][are-the-types-wrong] check uses the node16 profile and ignores the CJS-to-ESM warning. SmoothBricks
packages are [ESM]-first, so [CommonJS] consumers can use dynamic import. Node 10-only subpath failures are
intentionally ignored because [Node.js] 10 is not part of the supported package contract.
The github-actions pre-build check runs jactionlint --no-online from the repository root using the managed shell/CI
PATH. It uses the default correctness, security, and policy profile unless the repository config chooses otherwise.
The managed shell pins shellcheck and pyflakes; smoo requires both and passes their resolved paths to jactionlint,
rather than quietly skipping those checks. Checker failures and missing tools count as one failed check with
diagnostics. Without --fix, --fail-fast stops before the build; fix mode builds before checking. --projects scopes
builds and packed-package checks, not this repository-wide check.
Config is always explicit: the first existing .github/jactionlint.yaml, .github/jactionlint.yml,
.github/actionlint.yaml, or .github/actionlint.yml, in that order, or /dev/null for empty/default settings.
User-global dotfiles therefore cannot weaken the gate. Repository config may choose a profile, configure individual
rules, or enable an existing baseline. Smoo never requests fixes, baseline creation, or ignore flags, including in
smoo monorepo validate --fix mode; --no-online also disables network checks requested by repository config.
Git decides the inputs, never a filesystem walk. Smoo lists the YAML files directly in .github/workflows (only those
are workflows), action.yml and action.yaml at any depth, and .github/dependabot.yml or .yaml with
git ls-files --cached --others --exclude-standard, and passes the ones the working tree holds to jactionlint as
explicit files. Tracked files and new files not yet added are checked; a file the repository ignores, such as a
dependency install or a tool cache inside the checkout (a Go module cache holds third-party actions), is never an input,
while a tracked action is checked wherever it lives, test fixtures included. Smoo never runs jactionlint's repository
mode, which walks the filesystem into ignored directories and lints what it finds there; given explicit files,
jactionlint still detects the project and the local workflows that call each action. A repository with no applicable
workflow, action metadata, or Dependabot configuration is skipped, with the reason printed under --verbose; applicable
files are never treated as a successful empty check, and a failed git ls-files fails the check.
smoo monorepo validate --only-if-new-workspace-package first checks the staged git diff for newly added workspace
package manifests. If none are staged, it exits successfully without running the full validator. The generated
pre-commit hook uses this mode so adding a package rechecks conditional managed files, including whether the publish
workflow is now required, without making every commit pay for full validation.
Browser Tests
Browser-test targets can use smoo playwright run bun test --timeout=0 browser. The command prepares Chromium through
the shared browser policy, then starts the child with the selected browser location in its environment before Playwright
is imported. PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH takes precedence: managed Linux shells provide lock-pinned Nix
Chromium with its complete runtime closure, rather than an Ubuntu download missing NixOS libraries. Without a configured
executable, host runners use the configured Playwright cache or their XDG cache; ephemeral GitHub runners use their
preinstalled browser without downloading. Developer machines use their configured or default cache. Targets using this
command must depend on the CLI build as well as their application build.
Publishable Packages
Publishability is declared with an [Nx] tag:
{
"nx": {
"tags": ["npm:public"]
}
}This tag is the source of truth for public npm metadata and publish artifact validation. Release selection adds one more
convention: a package is released by the current repository only when its repository.url exactly matches the root
package.json repository.url. Equivalent-but-different spellings, such as git+https vs SSH for the same GitHub
repo, fail validation because ownership should be explicit and visually obvious. This lets a workspace mirror public
packages from another repository without publishing them from the mirror.
Rules:
npm:publicpackages must not beprivate: true.private: truepackages must not havenpm:public.- Public packages must define license metadata.
- Public packages must publish with
publishConfig.access = "public". - Public packages must define
repository.type,repository.url, andrepository.directory. - Public packages must define
files. - Public library packages must define
types. - Public packages must define either
exportsorbin.
Owned public packages may inherit the root license when the root license is not UNLICENSED. Mirrored public packages
must carry their own license. smoo monorepo init does not copy the root repository.url into packages; a missing
package repository.url is a validation failure so new packages must consciously choose whether they are owned by the
current repository or mirrored from another one. Init still sets publishConfig.access = "public", repository type,
repository directory, export ordering, and source-file publish entries when those can be derived safely.
smoo monorepo list-release-packages prints the comma-separated Nx project names for packages that are both
npm:public and owned by the current repository. Release commands, trusted-publisher setup, and the managed publish
workflow use this owned release package list instead of every public package in the workspace. smoo keeps both names in
release metadata: Nx commands, GitHub Release tags, and git release tags use projectName, while npm publish checks and
tarball validation use the real package name.
For [GitHub Actions], smoo monorepo list-release-packages --fail-empty --github-output "$GITHUB_OUTPUT" appends the
projects=<nx-project-list> output expected by the managed publish workflow and fails with a clear error when no owned
release packages exist.
smoo release npm-status shows whether each owned release package's current name@version already exists on npm. It is
an npm registry check, not a full release workflow status check.
smoo release version --bump auto first selects direct release candidates, then delegates versioning to [Nx]. A
candidate with no release tag is a first release: Nx has no commits since a release tag to derive its bump from, so smoo
versions it itself (see Releases). Direct candidates are owned public packages with package-local changes
that can affect published users: files matched by the package's resolved Nx build/production inputs, packaged assets
listed in package.json files, package metadata docs such as README/LICENSE/CHANGELOG, or user-visible package.json
fields such as exports, bin, types, dependencies, peerDependencies, and publishConfig. Test-only and local
automation changes such as scripts, nx, devDependencies, and tsconfig.test.json do not select a package by
themselves.
Downstream dependency bumps are intentionally left to Nx release. If package A is selected and bumped, Nx may also bump
public package B when B depends on A, even when B has no direct file changes. smoo should not pre-expand direct
candidates to downstream dependents because that would duplicate Nx's dependency graph and can over-select packages.
The dry run asks Nx in-process; candidate selection and the real run spawn nx. All of them are bound to the repository
root git reports, never to an inherited NX_WORKSPACE_ROOT_PATH. Nx reads each project's commits through
git log --relative from that root, so a shell naming the same checkout by another path (CI's devenv bind mount) would
otherwise hide every commit's files from the real run and version nothing the dry run planned; with the daemon on, a
daemon started under one spelling also refuses clients that use the other.
Spawned Nx release commands always pass --verbose, so a failed project graph prints Nx's underlying plugin error and
stack trace before smoo reports the child exit status. Diagnostics never retry versioning or change its mutation flags.
Nx Conventions
smoo keeps Nx target names predictable and separates tool work from aggregate workflows.
@smoothbricks/nx-plugin owns transformer-aware TypeScript targets because this workspace compiles through ttsc.
Configuring @nx/js/typescript would run tsc/tsgo directly and bypass the Typia and LMAO transformers. A package
tsconfig.lib.json therefore produces the concrete tsc-js target from the SmoothBricks plugin. The same plugin also
infers Bun test typechecking, Cargo workspace targets, and aggregate targets.
Concrete targets use {tool}-{output} names and describe the tool that runs and the artifact or purpose it produces:
tsc-jscomes from@smoothbricks/nx-pluginand runsttscfor package JavaScript/declaration output.- Packages that run
bun testmust havetsconfig.test.json. Bun executes tests without typechecking, so smoo creates a no-emittypecheck-teststarget from that config and wires it into validation. Other test runners may own their own typecheck path. - Test tsconfigs are validation configs, not TypeScript build-mode projects. They must use
noEmit, must not setcomposite: true, and package roottsconfig.jsonmust not reference./tsconfig.test.json. The inferredtypecheck-teststarget runs"$SMOO_TYPECHECK_TSC" -p tsconfig.test.json --noEmitafterbuildinstead. - A neighboring workspace-root
Cargo.tomlprovides Cargo test, lint, mutation, and benchmark targets. Workspaces withcdylibmember crates also receive the cacheablecargo-wasmoutput target. buildis an aggregate. It exists only when there is at least one concrete build target such astsc-js,tsdown-js, or another tool-output target, and it depends on output-family wildcards such as*-js,*-web,*-html,*-css,*-ios,*-android,*-native,*-napi,*-bun, and*-wasminstead of duplicating commands.lintis an aggregate validation target. It is not a formatting target.
This CLI's cli:test target divides every Bun test file across five native --shard targets, partitioned by the
measured per-file durations in test-timings.json (--timings=test-timings.json --shard=N/5) instead of by file count,
so one slow file no longer decides a shard's length. All five bounded targets must pass, and normal discovery still runs
every test file under src/ exactly once, including files the manifest does not name yet. The shell-entry integration
files (src/monorepo/setup-environment*.test.ts, one per subject over the shared fixture in
src/monorepo/__tests__/helpers/shell-entry.ts) are ordinary shard members; the logic they used to reach only through a
whole shell entry is tested by direct calls (installed-bun-links.test.ts, uv-relocation.test.ts,
developer-links-declared.test.ts).
smoo nx refresh-test-timings <project> rewrites a project's --timings manifest. It runs the project's bun test
shard command once without --shard, plus --update-timings, in the shards' cwd, so Bun writes every test file's
measured duration into the first --timings manifest. Bun merges into that manifest, so entries for deleted files stay
and are harmless. If the suite fails, the command exits non-zero without checking coverage; Bun still writes that run's
durations, so fix the suite and run the command again. Otherwise the command fails listing any test file the manifest
still does not name. The nx-plugin's inferred shard-timings check, which lint depends on, prints this command when a
test file is missing from the manifest.
Release fixtures enable Nx performance and verbose daemon logging in their child environment. A body failure or a test ending before its fixture is released prints the fixture's daemon log into the retained test output before cleanup. This records graph, hashing, and daemon-request phases without changing deadlines, retries, or daemon selection; it does not cancel a timed-out test body or claim to repair a stall.
The fixture-Git process-group regression keeps its 100 ms command deadline and exact timeout oracle; that deadline is the subject under test. The case has no guard of its own: the descendant sleeps for a day, so a broken group kill leaves the pipes open and the case never returns, and the leg's no-progress bound fails the run. Natural exit cannot make a broken cleanup pass.
Receipt-admission defects run as one local walk and one CI walk over five labelled rows each. Every row keeps its healthy entry, exact refusal and two-install oracles; only the repository and command shims are shared. The fixture's prepare script rebuilds its dependency topology between rows without deleting the unrelated TypeScript API pin.
Abandoned-fixture regressions run their nested bun test from the package directory, preserving the checkout's socket
namespace. An explicit empty config omits the unrelated package preload, and the shared per-test bound governs the
nested case and its retirement hook instead of Bun's implicit 5 s default. The nested runner explicitly inherits the
outer runner's already-isolated environment.
Nx-backed CLI fixtures declare cacheDirectory: ".nx/cache" in their own nx.json, keeping the task cache and native
workspace-data database under the temporary workspace even when caller environment overrides are cleared. Their teardown
stops the fixture daemon and removes that workspace; it must not leave one ~/.nx/<repoKey> per fixture run.
The root @typescript/native dependency follows TypeScript's documented side-by-side pattern: it aliases TypeScript 7
and supplies the native compiler used by ttsc. Because ttsc resolves only the unscoped package by default, the
managed devenv shell selects node_modules/@typescript/native/bin/tsc by default. A repository declaring
tooling/direnv/typescript-go.nix instead requires its own tooling/direnv/.devenv/profile/bin/tsgo; a missing
executable is a named refusal, never a stock-compiler fallback. The GitHub setup action persists that absolute path
through GITHUB_ENV. Nx and other JavaScript tooling still require the full TypeScript compiler API, so workspace
typescript stays on TypeScript 6. After install, setup-environment forces Bun's shared .bun/node_modules/typescript
hoist onto that API package so Nx does not load @typescript/native (TS7) via the store
(oven-sh/bun#33834). Both dependencies and the environment binding are
required: TypeScript 7 under the unscoped name breaks Nx API calls such as readConfigFile.
For [email protected], smoo temporarily distributes patches/[email protected] and registers it in the root
patchedDependencies before smoo monorepo update runs Bun installation; it removes every other ttsc@ registration.
The patch repairs import-extension handling during the compiler's internal forced-emission diagnostic pass; user
--noEmit still writes no output. It also makes a source-plugin build lock waiter inspect the holder before it reports
a published binary: a process that finds the binary still takes the cache key's lock to register as its reader, so a
holder killed while it held that lock is proven gone and retired instead of outwaited for ttsc's 600 s admission budget,
and a live or unprovable holder is never retired. A lock inspection whose read of current/generation straddles a
handoff of the key reports the key released instead of throwing "ttsc plugin build lock has no valid generation": after
a publication every reader takes and frees the key in turn, so the read can land after one reader retired current/ and
before the next published its own. Only the same current/ directory, by device and inode, before and after a read that
found no valid generation is refused as malformed. ttsc's own go invocations run in workspace mode (GOWORK=auto),
where go accepts only -mod=readonly or -mod=vendor, so the patch drops a caller's -mod flag from their GOFLAGS
and keeps the rest: a host shell exporting GOFLAGS=-mod=mod failed its entry rebuilding a tool, "selecting Go packages
for plugin typia failed". A plugin source's link at or below a name the go tool passes over (./_-prefixed, or
testdata) is skipped like one in a pruned directory instead of refusing the module: go neither compiles nor embeds it,
and cowshed's .cache/lmao build-state link inside a package that is also a Go plugin module cost that module its
recorded source identity, so every load copied it and ran go 21 more times. One load walks the Go SDK once: its
package selections and build environments share that observation and its witness entries while the load runs
synchronously, and a watch delivery ends the batch. Each TtscCompiler.prepare() walked the bundled SDK three times
before, about 25k path lookups a call. An emit whose caller sets TTSC_WRITE_IF_CHANGED=1 writes an output only when
its bytes differ (driver/rewrite.go DefaultWriteFile), as a watch child's (TTSC_WATCH_WRITE_IF_CHANGED) already
does: an emit after one source edit rewrote every .js, .d.ts and map in place, file churn every copy-on-write clone
of the checkout pays for. It is an opt-in because a caller that finds stale outputs by which files the emit did not
touch (the typescript-emit executor through nx-plugin 0.7.9, and any checkout whose plugin dist predates the patch,
which bun install applies first) would remove every unchanged output. The executor sets it and prunes outDir by
ttsc's --verbose emitted-file list instead. The bootstrap emits that build the plugin before Nx can run (the shell
entry and tooling/ttsc-emit.ts) set it too: they prune nothing, and one that rewrote the plugin's dist moved every
stamp Nx recorded for it, so the plugin's tsc-js restored its dist from the cache, removing the directory while other
tasks resolved their executors from it. ttsc's utility-plugin host, which links a plugin's Go library (lmao-ttsc's, a
consumer's lint plugins), printed only // ttsc utility: plugins=N emit=true and a count under --verbose, so the
executor refused its emits; the patch makes it print the build lane's header (tsconfig=… cwd=… emit=…) and one
+ <path> line per emitted file (utility/host.go RunBuildWithIO), and it writes through the same
DefaultWriteFile. A linked plugin that writes a file beside the compiler's outputs (a census in outDir) reports it
with PluginContext.ReportHostOutput(file string); each emit then hands the file's bytes to the host's writer after the
compiler's own outputs (driver/plugins.go, driver/rewrite.go, driver/emit_plugin.go), so ttsc's build lane, its
utility host and a plugin's own host such as typia's list it, and the executor keeps it. An unreported one is pruned as
a file this emit did not produce. The packaged managed asset is the source, so clean managed installs receive the same
repairs without a compiler fork or edits to the dependency cache. The registration applies only to that exact compiler
version; the repository's lockfile continues to own its compiler pin.
Explicit Nx target names must not contain :. Nx already uses colon syntax at the CLI boundary:
project:target:configuration. Allowing target names like build:wasm makes command parsing and package-script aliases
look like configurations, and it prevents a clean split between concrete tool-output targets and aggregate targets.
Use tool-output names for concrete targets, such as tsc-js, tsdown-js, and cargo-wasm. Use build and lint only
as aggregate targets. Package scripts may still use developer-friendly colon names, for example build:wasm, but those
scripts should delegate to unambiguous Nx targets such as nx run pkg:cargo-wasm.
Package Structure
Workspace packages keep all TypeScript sources — and especially all test files — under a single root: src/.
packages/<name>/
src/
foo.ts
foo.test.ts # unit tests: colocated with the module they defend
__tests__/ # cross-module/integration TS tests, still under src/
crates/*/tests/ # Rust integration tests (cargo-owned convention, unaffected)
scripts/ # non-shipped tooling; never *.test.ts
dist/, target/ # generated output, never scannedThe test-location rule is not tidiness; it is what makes the generated tooling provably cover every test. The convention
is one root, not one path: tsconfig.test.json includes are generated as src/** patterns, and the bounded
bun test targets run with cwd <package>/src — both because bun test <arg> treats the argument as a filter over a
scan rooted at the cwd (scanning from the package root would walk a Rust package's entire cargo target/ tree, tens of
seconds per run), and because a second test root would have to be threaded through every one of those consumers forever.
A test file outside src/ is therefore neither typechecked nor executed, silently. smoo monorepo validate fails on
any *.test.ts / *.spec.ts(x) outside src/ so that gap cannot reappear.
Within the single root, separation still exists where it matters: unit tests sit next to their modules, integration
tests live in src/__tests__/, and Rust crates keep cargo's own tests/ directories, which the rule deliberately
ignores.
Managed Files
smoo monorepo update writes the managed files into a repository.
Managed files include:
- [
.git-format-staged.yml][git-format-staged] - Git hook scripts under
tooling/git-hooks - [direnv]/[GitHub Actions] bootstrap scripts under
tooling/direnv - [GitHub Actions] workflows under
.github/workflows - Local composite [GitHub Actions] under
.github/actions
Matching in-workspace source symlinks are preserved. Broken, escaping, or drifted symlinks are reported instead of
followed. SmoothBricks uses symlinks back to packages/nx-plugin/managed so changes to the CLI package are tested
immediately. Downstream repos receive ordinary committed copies.
The publish workflow is conditional. Repositories with no owned release packages skip .github/workflows/publish.yml in
init, check, and diff; adding a new owned package makes the workflow required on the next validation run.
Use:
smoo monorepo update
smoo monorepo check
smoo monorepo diffcheck fails when a managed file is missing or stale, and when a package policy fails (the devenv profile's Go against
the Go SDK ttsc vendors, root package, the root's @smoothbricks/* packages against the versions the installed CLI
uses, Nx names and release config, public tags and metadata, test locations, workspace dependencies, the devenv import
and release lock, and the Cargo cache and toolchain policies). check --warn reports managed-file drift as warnings
instead — CI runs it on every event and, on a default-branch push, dispatches the managed-files workflow to heal the
drift — while a policy failure still fails it, because nothing heals one. diff reports drift without writing files.
The Cargo toolchain policy resolves each cached target's named inputs through its project definitions first, then
workspace definitions. A cached target must reach cargoToolchain, the toolchain identity (the devenv pin and the Nix
store objects of the programs the shell declares), directly or through a named input; restating the lock alone is
refused. A local definition of cargoToolchain that drops the lock or the programs runtime input is refused as well.
The generated publish workflow is canonical Prettier YAML. Running the repository formatter over
.github/workflows/publish.yml is byte-stable and does not create managed-file drift.
Shell Entry
tooling/direnv/devenv.nix selects coreutils-tee for this repository's shell. The separate package applies the
upstream tee fix without overriding nixpkgs' coreutils or stdenv. The lockfile and
compiler/SDK inputs stay unchanged; remove the patch when the pinned nixpkgs includes the fix. Evaluate shell edits
through
canonical workspace activation.
The coreutils requirement is satisfied by either complete package, coreutils or coreutils-tee, so no second package is added. Repositories declaring neither receive stock coreutils by default.
Every direnv load runs the managed devenv prologue (tooling/direnv/devenv.smoo.nix), and it is built to cost almost
nothing when nothing changed:
setup-environment.tsrunsbun installonly when its inputs changed since the last successful install: the root and every workspace memberpackage.json,bun.lock,bunfig.toml, the patch filespatchedDependenciesnames, and the Bun version, or when a required installed package is missing or an external developer link is dangling. The record lives innode_modules/.smoo-install, so deletingnode_modulesreinstalls; a failed install records nothing and is retried on the next entry. A CI runner writes it too, after its install: devenv'ssmoo:installtask lists the file among its inputs, and a runner's devenv state outlives its checkout (a host runner keeps it on a shared bind), so without the record a fresh checkout of an unchanged lockfile found the task current, installed nothing, and the shell hook failed on a missingttsc. A developer entry installs with--frozen-lockfilewheneverbun.lockexists, so shell entry never rewrites the lockfile: a manifest that needs a lockfile change degrades the shell with Bun's reason, andbun installrun on purpose writes the change. A CI runner always installs with--frozen-lockfile. A CI runner is one that setsGITHUB_ACTIONS=true(GitHub Actions, and Forgejo Actions, which mirrors its variables asGITHUB_*), neverCIalone: agent harnesses exportCI=trueon every command they run. When that frozen install misses, the entry installs again without the flag, prints thegit diffof what the install changed (thebun.lockthe pull request should have committed) and fails. The diff goes to stderr, whole: devenv reports a failed shell entry as its stderr alone, so anything written to stdout would be lost. The same receipt records each presentbun.lock/bun.lockbSHA-256 after success. Missing or mismatching lock receipts require the normal installer to run;cowshed doctorreportsnode-modules-install-missingornode-modules-lock-driftwithout installing or rewriting anything. Before a successful managed setup install records this receipt, an isolated-store metadata pass computes the live closure from the root, workspace-member and hoist aliases, following each alias's canonical endpoint transitively, and checks the installed store links and package aliases of the entries that closure reaches — plus any entry that is the sole variant of its package and version, which has no replacement to point at. It skips only unreferenced sibling variants: superseded peer contexts nothing in the live tree resolves through, with no same-version substitution ever standing in for a live link. Every live or sole alias must reach a directory with a readable package manifest. A failure names the alias, its expected target and the metadata cause; it records no successful install and uses the existing degraded-local/strict-CI reporting. The check writes no new receipt on failure and performs no cache repair or automatic retry. It does not certify every published payload file, protect against later shared-cache changes, or run on the matching-stamp/status path. Extending that path remains an open contract question requiring measured unchanged-entry cost and coverage. The CLI update command installs without writing this receipt and is outside this receipt-admission check. Directdevenv shellsources.cowshed/envin the managed prologue before project hooks, just as direnv does, so both entry paths reuse cowshed's CI and pinned compiler-cache environment.A physically missing registry-package payload has one explicit repair route:
bun tooling/direnv/setup-environment.ts --repair-missing-payload <installed-path>. Name the missing file or directory through this checkout's installed alias. Under the same install lock, the installer binds its name/version and tar integrity tobun.lock, verifies that published tar and compares only that package. Existing payload mismatches refuse; only missing published entries are added in a sibling staging directory, preserving existing files, links and metadata, then published with a native atomic directory exchange. Published-complete and unchanged lock bytes are proved before the existing successful-install receipt. Repeating a healthy request installs nothing. A failure before that receipt restores the original package; after it commits, reclamation failure names the retained staging path and explicitly says the repair committed. This does not change normal-entry/status validation, certify all shared-cache payload, repair external developer targets, or add a retry/purge route. Seespecs/cowshed/03_caches.md→ "Explicit missing-payload repair".The explicit repair preserves the prior successful-install witness and its locked hashes; it never upgrades that witness's input digest from a one-package proof. A stale witness stays stale, and no valid prior witness means refusal.
A repository whose
devenv.nixenableslanguages.python.uvgets its uv workspace synced the same way (uv sync --all-packages --all-groups --all-extras,--lockedon a CI runner) into devenv'sUV_PROJECT_ENVIRONMENT, with the interpreter devenv provides, and activated after the sync. Its inputs are the rootpyproject.toml,uv.lockand every workspace member'spyproject.toml. The environment names no path of its checkout, so a copy-on-write clone of an installed checkout enters installed: uv creates it relocatable, each workspace member's editable.pthline is rewritten relative to site-packages after every sync, currentness and replacement share one witness that the environment is still the relocatable one that sync would leave (no launcher or interpreter link naming an environment or a checkout, no absolute.pthline), and the managed module clearslanguages.python.libraries, whose default would put the checkout's.devenv/profileinto the interpreter's store path. devenv's ownlanguages.python.venvanduv.syncmust stay off; the module refuses them.Bun/uv installs, installed-package repairs, alias/link writes and repository git configuration/hooks in one checkout share a flock(2) on
node_modules/.smoo-install.lock. A second mutating entry waits (and says so), then rechecks the first entry's stamps and links. The kernel releases the lock when its holder exits, so a killed shell entry leaves nothing behind. An existing lock file is opened read-only (flock needs no write access), so an entry whose installs are current writes nothing in the checkout and enters a read-only view of it, such as a read-only cowshed job. Thesmoo:developer-linkstask plans its declared links before it takes that lock, and takes it only when it has something to write: a dangling store link, a stale developer link or a declared link to change. Entries whose links are current, or whose declarations this machine cannot honour, say so without waiting on each other's probe of a provider checkout. Cargo fetch never takes this checkout lock: it changesCARGO_HOMEunder Cargo's native package-cache locks and records successful fetches in smoo's private XDG cache. Local and CI fetches can therefore proceed while another entry holds the Bun/uv install or alias/link lock; competing Cargo operations remain serialized by Cargo itself.The managed
.envrcloads no parent directory's.envrc: the shell is built from the checkout's own inputs, so the same commit enters the same shell wherever it is checked out. An environment wanted from outside the checkout belongs in the gitignored.envrc-local.The installs are devenv tasks ordered before
devenv:enterShell:smoo:install(bun, uv) runs only when a manifest, lockfile,bunfig.toml, patch or the install scripts changed, ornode_moduleswas deleted (devenv'sexecIfModified), andsmoo:cargo-fetchrunscargo fetch --lockedonly when CARGO_HOME lacks a workspace's locked packages (itsstatus), which the Nx plugin's offlinecargo metadataneeds. A failed task loads the shell anyway; CI runs both withdevenv tasks run, which fails. devenv also evaluates theenterShellscript once BEFORE it runs any task, only to capture the environment (it setsDEVENV_SKIP_TASKSfor that pass), so a repository's ownenterShellhook that needs what the tasks install, such as thettscinnode_modules, must skip the pass: on a fresh checkout or a runner that restored another lockfile'snode_modulesthe hook would abort the capture before the install could run. This repository'stooling/direnv/enter-shell.tsdoes, and so does the managed prologue's question about the declared links (Declared Links, below). Managed toolchain cache maintenance (toolchain-stamp.ts) skips that capture pass too: it exports no variables, and compiler probes and invalidation belong after tasks and project hooks have settled the toolchain, not once before them and again afterward. The managed.envrcwatchespackage.json, the lockfiles and the scripts shell entry runs, so a shell direnv keeps loaded re-enters after a pull or a script change. Local nvfetcher overlays also watchnixpkgs-overlay/flake.nixand_sources/generated.nix. Import the existing local flake outputs directly indevenv.nix(((import ./nixpkgs-overlay/flake.nix).outputs {}).overlays.default), rather than declaring the overlay as apath:flake input: the live import makes generated pins part of devenv's evaluation dependencies as well as direnv's watches. Ordinary activation then selects changed pins while an unchanged shell stays cached; no forced refresh or second version witness is needed.This repository's entry proper bootstraps the CLI and Nx plugin plus their transitive
workspace:*runtime dependencies, once each and dependency first, usingttsc -p tsconfig.lib.json --emitwithout calling Nx or smoo. The existingnode_modules/.cache/smoo/dist-verifiedmarkers gate emit, and absent or emptydistrebuilds even with a marker. Bootstrap finishes before runtime-pin sync can invoke smoo. Install/postinstall and developer-link tasks run their raw scripts and do not invoke the CLI. CLI-only rebuilds leave the plugin's Nx daemon and graph untouched. See Self-hosted shell bootstrap.smoo:cargo-fetchbounds eachcargo fetch --locked, which holds Cargo's package-cache lock while it runs and so stops every cargo that sharesCARGO_HOMEwhile it stalls: the fetch runs in a process group of its own under a 300 s deadline that kills the group, and git's low-speed limits end a stalled HTTP(S) transfer after about a minute. A fetch that is killed fails the task like any failed fetch (the Nx plugin README, "Cargo closure input", has the mechanism).Shell secrets resolve only for an entry that installs; see below.
tooling/direnv/ttsc-env.sh, sourced by shell entry and by the git hooks, names the checkout's own native compiler inTTSC_TSGO_BINARY, caches ttsc's plugin binaries in the checkout's.cache/ttsc-plugins(the output a repository's plugin build target declares) and its Go build cache in.cache/ttsc/go-build.GOFLAGScarries-trimpath. A cache the caller already exported wins, so a host or a sandbox that shares these caches across checkouts sets them itself, unless the caller's environment was bound for another workspace (itsNX_WORKSPACE_ROOT_PATHnames another directory): that workspace's caches are dropped. Go's own caches keep Go's defaults. Sourced from a directory that is not a workspace root, it refuses rather than cache beside that directory.SMOO_TYPECHECK_TSCnames the native compiler the inferredtypecheckandtypecheck-teststargets run.devenv.smoo.nixbuilds it from one pinned TypeScript source revision withgo_1_27from thenixpkgs-goinput. The targets run it directly rather than through ttsc, whose auto-loaded transform plugins (typia) check inside their own host with ttsc's compiler. Transform diagnostics stay enforced where the transform runs:tsc-jsfor the lib program, the test runner's transform for executed test files. The variable is a hashed input, so a revision bump misses the cache. Emit, editors and ttsc plugins keep the npm@typescript/nativecompiler. A target run outside the devenv shell fails on the unset variable rather than checking with another compiler under the same key. The module publishes that onenixpkgs-gopackage set as the module argumentgoPkgs. A repository'sdevenv.nixtakes Go from it instead of importing the same Go inputs again, so both producers use one evaluated package set.tooling/direnv/nx-socket-dir.shexportsNX_WORKSPACE_ROOT_PATHandNX_SOCKET_DIR. A checkout gets its own socket dir underDEVENV_RUNTIME; an inherited one that resolves into another checkout is dropped. A cowshed workspace (.cowshed/workspace.json) uses<link>/nx, where<link>is theCOWSHED_RUNTIME_LINKits.cowshed/envnames: the literal path cowshed gives its sandboxed jobs. The script creates that link if no job has yet. Host shells and sandboxed jobs then share one Nx daemon, and a sandboxed daemon's plugin workers can bind their sockets whichever side connected. A workspace whose file names no link (cowshed has not served it since its mint) or whose link leads to another checkout is refused, never given a socket dir of the shell's own.tooling/direnv/repo-pathruns first and serves an unserved workspace withcowshed exec <ws> -- true, refusing by name only when that fails (cowshedspecs/cowshed/02_workspaces.md, "Workspace environment").On Darwin,
tooling/direnv/apple-developer.shhands the Apple toolchain to Xcode: it unsetsCC/CXX, drops a/nix/storeSDKROOTorDEVELOPER_DIR, and removes every/nix/storePATHentry that carries nixpkgs xcbuild'sxcrun, which answersunable to find sdk: 'macosx'once the nix developer directory is gone.xcrunis then Apple's/usr/bin/xcrun, soxcrun --sdk macosx --show-sdk-pathnames the selected Xcode or Command Line Tools SDK in host shells and sandboxed jobs alike. Each drop is announced; values outside the store stay as given.tooling/direnv/compiler-variables.shmakes cc-rs's compiler variables the shell's own:CC_<target>,CXX_<target>,AR_<target>andRANLIB_<target>, theirHOST_andTARGET_forms, andCFLAGS,CXXFLAGS,ARFLAGSandRANLIBFLAGSin all of those forms and generic. It keeps the ones the shell'senvdeclares, whichdevenv.smoo.nixpasses it, and drops and announces every other one the shell inherits. cc-rs build scripts rerun on each name they look a compiler up through, so one a caller carried in from another checkout's shell rebuilds those packages and everything that depends on them against a target built without it (cowshedspecs/cowshed/16_build_volumes.md, "One Cargo environment on every path").
The repository's local git config includes
tooling/workspace.gitconfigby a path relative to the config file (../tooling/workspace.gitconfigin.git/config), so a copied checkout reads its own copy. An absolute include would name the checkout that wrote it, and git refuses to run when an include exists but cannot be read — the original checkout, from inside a sandboxed clone. Shell entry rewrites every such include without reading through git first.A shell entry that may not write the git hooks or the repository config — a cowshed workspace granted
--deny-write .git/hooks, a read-only checkout — loads the shell anyway, the way a failed install does. It prints aWARNINGnaming the path and errno it hit, thenGit hooks and repository config were not applied; the next entry that may write them wires them.applyWorkspaceGitConfigfrom@smoothbricks/cli/monorepo/git-config, which shell entries managed by older releases call, degrades the same way.Starting the shell contacts no network when the checkout is installed. The managed module turns devenv's Cachix integration off (
cachix.enable = false): it only adds devenv'sdevenvbinary cache, whose signing key devenv would otherwise ask the Cachix API for, and the shell's packages come fromcache.nixos.orgor are built locally. A host that wants that cache names it in its ownnix.conf. The shell exportsNX_USE_LOCAL=true, so an Nx daemon starting (andnx configure-ai-agents,nx init) answers from the installednxinstead of installingnx@latestfrom the registry;nx migratethen also runs with the installed CLI, andNX_USE_LOCAL=falserestores the newest one for that command. A daemon still asks GitHub fornrwl/nx-ai-agents-configwhen an agent has both rules and Nx's MCP configured, to tell whether that configuration is outdated; a cowshed sandbox runs Nx without a daemon. The shell also exportsNX_ISOLATE_PLUGINS=false: Nx loads the project-graph plugins in the daemon's own process instead of one worker process each. On a 56-project consumer's shed that left the project graph and everylinttask hash unchanged and cut the daemon's process tree from 8 processes and about 1.75 GB RSS to 1 process and about 0.65 GB.
Linking Local Packages
To run a repository against a local build of a package it depends on, point that package's node_modules entry at the
local checkout. Register the checkout once with bun link, run from inside it. Then link the entry: from the repository
root, bun link <name> links the root's node_modules/<name>. In a workspace member, bun link <name> fails with
<sibling>@workspace:* failed to resolve, because it installs the member as its own root and finds none of its
workspace:* siblings ([oven-sh/bun#24190][bun-link-workspace]). There, link the entry by hand:
(cd ~/src/smoothbricks/packages/cowshed && bun link)
ln -sfn ~/src/smoothbricks/packages/cowshed packages/app/node_modules/@smoothbricks/cowshedEvery install smoo runs keeps such a link, except a link to Nx, which those installs replace (see Nx below): shell
entry's install, a CI runner's frozen-lockfile install and smoo monorepo update. A developer link is any symlink
directly in the root's or a workspace member's node_modules (scoped names included) whose target lies outside the
repository. The install records these links first, then puts back every one except Nx. A link whose target no longer
exists is removed with a warning, and that install puts the lockfile's version in its place. To unlink, remove the link:
the next install puts the lockfile's version there. A missing external target, a live link to Nx, or a link into Bun's
install cache (node_modules/.bun/<entry> under globalStore) that the installed tree reaches and whose cache entry is
gone also makes the install stamp stale: the next managed install replaces the dangling or Nx link and relinks the store
entry, while every other live external link is preserved. A dangling store entry nothing installed reaches is a
superseded variant no install changes, so it never makes the stamp stale. An ordinary local setup invocation runs
bun install unless the install stamp matches, the TypeScript API package resolves and no developer or store link is
stale; a successful install records the stamp, so a later ordinary local setup invocation with current bindings runs no
bun install.
Only an observed missing target makes a link dangling; an unreadable checkout is not a deleted checkout. A developer link whose target this process may not read (EPERM in a cowshed sandbox that grants no read of it, EACCES under a directory's mode) is kept as made, named on its own line, and left out of the install's admission, which cannot read what it names; it is never taken for a dangling link and never fails the entry. Any other metadata error aborts the install without removing captured links.
Declared smoo.developerLinks directories resolve against each checkout's own root, never a root borrowed from a
cowshed marker. In-repository aliases are relative and travel with that checkout. An explicit declaration naming a
checkout present on this machine is authoritative: it repoints an existing external binding, including a developer's
previous choice. A declaration whose checkout is absent, or whose directory has no package.json, is reported as
unavailable and leaves the installed version and the existing binding bytes unchanged. A fork carries existing external
developer links as absolute sibling bindings.
A job's terminal cleanup applies the same boundary to the developer links it displaced. A registry replacement is
restored to its recorded link. A different live developer binding is kept as the developer left it, with no new grant or
attestation. A link that is absent with no observed install is reported as developer-link-ambiguous and stays absent.
A covered install restores its snapshot exactly before it returns, so a relink made through a covered bun, npm or
pnpm install is reverted. A relink made outside covered installs, by an absolute bypass or a command that is not an
install, keeps its intent. Neither puts back a link smoo.developerLinks made once the root manifest names its package
no more, even when the job captured it while the manifest still did (the workspace's kept Nx daemon, say): the install
that followed the dropped declaration keeps the version it installed.
While such links exist, every shell entry and every install smoo runs prints one line naming them, such as
linked to local checkouts: @smoothbricks/lmao -> ~/src/smoothbricks/packages/lmao (12 links). Entries that install
nothing print it too, because devenv's direnv integration hides the output of the run that installs.
A plain host bun install, bun add or bun remove re-points dependencies at their lockfile versions before root
preinstall, so a lifecycle script cannot keep developer links. In a cowshed job the managed command-name Bun/npm/pnpm
install entrypoints restore live developer package and executable aliases immediately before the next command; the
supervisor also restores them when the job ends. Explicit absolute host/devenv executables bypass immediate wrapping and
receive terminal cleanup only, so absolute-bun install && nx ... is not covered. Outside cowshed, a link made by hand
is gone after a plain install and has to be made again. A declared link (below) comes back by itself.
An install that exists to replace links, such as a script that runs bun link <name> against providers registered anew,
names the packages it replaces so that smoo does not undo it:
keepDeveloperLinks(root, install, { relink: ['@scope/name'] }) from the repository's
tooling/direnv/developer-links.ts. A name is a package name, scoped or not, and selects that package's link in the
root's node_modules and in every workspace member's. When install resolves, what it left at those names stays and
every other captured developer link is put back as for any install. When it rejects, every captured live link is put
back, the named ones included. Entries that had no developer link at the start remain as the failed install left them;
this helper does not promise transactional rollback of new package-manager entries. Relink intent lists the actual final
linked checkouts on success or failure. relink selects links and creates none: a named package without a link to a
local checkout when the install starts is left as the install leaves it, and a named link whose checkout is gone is
removed with the usual warning first.
Nx is the one package every managed install replaces. The repository's lockfile owns Nx, so the Nx a shell runs comes
from bun install, never from a developer link to a local Nx checkout. The managed installs (shell entry's, a CI
runner's frozen-lockfile one, and smoo monorepo update) therefore pass { relink: ['nx'] }: a successful install
leaves Nx as Bun installed it, and every other developer link is still put back. A live Nx link makes the install stamp
stale, so the next managed install replaces it; a later ordinary local setup invocation with current bindings runs no
bun install. smoo.developerLinks.nx is obsolete and refused: smoo monorepo update removes that declaration while
keeping sibling package declarations. Nx must not be reselected by the developer-links postinstall task.
smoo:install runs setup when one of its listed inputs changed. The smoo:developer-links status also checks for stale
install links, including external Nx aliases in the root's or a workspace member's node_modules. An ordinary shell
entry therefore repairs inherited Nx aliases even when no install input changed: under the same install lock, it runs
the managed Bun installer before applying declared links. Genuine sibling links survive, a failed install reports its
cause without claiming the aliases are repaired, and the next unchanged entry installs nothing.
Declared Links
A repository pins a developer package to a sealed snapshot in its own store, not to the provider's moving branch. The
root package.json names the snapshot's 64-digit lowercase SHA-256 ID:
{
"smoo": {
"developerLinks": {
"@smoothbricks/lmao": {
"directory": "../smoothbricks/packages/lmao",
"snapshot": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
}
}
}Replace the example ID with the ID of an actual captured snapshot. The link resolves from .cache/linked-snapshots/<id>
in the consumer; a cowshed checkout whose store lacks it imports it from its project's main checkout store (the
projectRoot its .cowshed/workspace.json names), copied into a temporary entry, verified there and renamed into
place. The existing snapshot verifier checks its census, producer provenance, and package name before any alias changes.
This pins source, built outputs and dependencies together; a Git commit alone does not identify the built outputs, and a
tag can move. No second checkout materializer is involved.
The directory is only the provider location for a freshness report. When its HEAD differs from the captured HEAD,
smoo:developer-links reports a stale pin, names both commits, and continues successfully with the same snapshot.
It neither recaptures nor requests an install. If the provider disappears or cannot be read, it reports that freshness
cannot be inspected and still uses the verified pin. Missing, corrupt, malformed or wrong-package pins instead refuse
with a nonzero result; there is no fallback to the moving checkout or the published package.
To select a pin, use the capture form below once and retain the snapshot ID it prints and records in
node_modules/.smoo-declared-links's link target. Replace the capture object with that ID. To move it, deliberately
capture and review another build, then change the declared ID. An already-running process retains the code it loaded;
new resolutions use the selected snapshot. A
