npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/cli

The package exposes a Bun-native executable:

smoo --help

SmoothBricks' 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/smoo launcher is executable when present.
  • Synchronizes root runtime versions inside devenv, or when --sync-runtime is passed.
  • Applies safe publish metadata defaults to npm:public packages 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 test and bun run dev stay available while delegating through Nx targets.
  • smoo monorepo validate --fix creates/updates tooling/package.json, keeps @smoothbricks/cli there 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 types comes first and default comes last.
  • Adds src to package files when 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 --all

Configure selected packages by Nx project name, package name, or package root:

smoo monorepo setup-test-tracing --projects cli,@smoothbricks/lmao,packages/nx-plugin

The 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/bun

Use --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.json owns workspace-level tools like nx, tooling/package.json owns smoo, and tooling/direnv/devenv.nix owns shell-provided tools like bun, git-format-staged, and fmt. The check holds tooling/package.json's @smoothbricks/cli range to ^<the version bun.lock resolved> and never asks the registry, so a release published after a commit cannot refuse it; smoo monorepo update moves the pin to the latest stable release and installs it. Every @smoothbricks/* package the root declares must resolve in bun.lock to the version the installed CLI uses through its dependency closure (cli → nx-plugin → cowshed …): Nx loads the root's copy, so nx sync would render the managed files with another nx-plugin than smoo did. After its install, smoo monorepo update pins 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.lock workspace 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:public packages must not be private: true.
  • private: true packages must not have npm:public.
  • Public packages must define license metadata.
  • Public packages must publish with publishConfig.access = "public".
  • Public packages must define repository.type, repository.url, and repository.directory.
  • Public packages must define files.
  • Public library packages must define types.
  • Public packages must define either exports or bin.

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-js comes from @smoothbricks/nx-plugin and runs ttsc for package JavaScript/declaration output.
  • Packages that run bun test must have tsconfig.test.json. Bun executes tests without typechecking, so smoo creates a no-emit typecheck-tests target 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 set composite: true, and package root tsconfig.json must not reference ./tsconfig.test.json. The inferred typecheck-tests target runs "$SMOO_TYPECHECK_TSC" -p tsconfig.test.json --noEmit after build instead.
  • A neighboring workspace-root Cargo.toml provides Cargo test, lint, mutation, and benchmark targets. Workspaces with cdylib member crates also receive the cacheable cargo-wasm output target.
  • build is an aggregate. It exists only when there is at least one concrete build target such as tsc-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 *-wasm instead of duplicating commands.
  • lint is 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 scanned

The 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 diff

check 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.ts runs bun install only when its inputs changed since the last successful install: the root and every workspace member package.json, bun.lock, bunfig.toml, the patch files patchedDependencies names, and the Bun version, or when a required installed package is missing or an external developer link is dangling. The record lives in node_modules/.smoo-install, so deleting node_modules reinstalls; a failed install records nothing and is retried on the next entry. A CI runner writes it too, after its install: devenv's smoo:install task 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 missing ttsc. A developer entry installs with --frozen-lockfile whenever bun.lock exists, so shell entry never rewrites the lockfile: a manifest that needs a lockfile change degrades the shell with Bun's reason, and bun install run on purpose writes the change. A CI runner always installs with --frozen-lockfile. A CI runner is one that sets GITHUB_ACTIONS=true (GitHub Actions, and Forgejo Actions, which mirrors its variables as GITHUB_*), never CI alone: agent harnesses export CI=true on every command they run. When that frozen install misses, the entry installs again without the flag, prints the git diff of what the install changed (the bun.lock the 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 present bun.lock/bun.lockb SHA-256 after success. Missing or mismatching lock receipts require the normal installer to run; cowshed doctor reports node-modules-install-missing or node-modules-lock-drift without 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. Direct devenv shell sources .cowshed/env in 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 to bun.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. See specs/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.nix enables languages.python.uv gets its uv workspace synced the same way (uv sync --all-packages --all-groups --all-extras, --locked on a CI runner) into devenv's UV_PROJECT_ENVIRONMENT, with the interpreter devenv provides, and activated after the sync. Its inputs are the root pyproject.toml, uv.lock and every workspace member's pyproject.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 .pth line 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 .pth line), and the managed module clears languages.python.libraries, whose default would put the checkout's .devenv/profile into the interpreter's store path. devenv's own languages.python.venv and uv.sync must 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. The smoo:developer-links task 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 changes CARGO_HOME under 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 .envrc loads 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, or node_modules was deleted (devenv's execIfModified), and smoo:cargo-fetch runs cargo fetch --locked only when CARGO_HOME lacks a workspace's locked packages (its status), which the Nx plugin's offline cargo metadata needs. A failed task loads the shell anyway; CI runs both with devenv tasks run, which fails. devenv also evaluates the enterShell script once BEFORE it runs any task, only to capture the environment (it sets DEVENV_SKIP_TASKS for that pass), so a repository's own enterShell hook that needs what the tasks install, such as the ttsc in node_modules, must skip the pass: on a fresh checkout or a runner that restored another lockfile's node_modules the hook would abort the capture before the install could run. This repository's tooling/direnv/enter-shell.ts does, 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 .envrc watches package.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 watch nixpkgs-overlay/flake.nix and _sources/generated.nix. Import the existing local flake outputs directly in devenv.nix (((import ./nixpkgs-overlay/flake.nix).outputs {}).overlays.default), rather than declaring the overlay as a path: 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, using ttsc -p tsconfig.lib.json --emit without calling Nx or smoo. The existing node_modules/.cache/smoo/dist-verified markers gate emit, and absent or empty dist rebuilds 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-fetch bounds each cargo fetch --locked, which holds Cargo's package-cache lock while it runs and so stops every cargo that shares CARGO_HOME while 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 in TTSC_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. GOFLAGS carries -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 (its NX_WORKSPACE_ROOT_PATH names 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_TSC names the native compiler the inferred typecheck and typecheck-tests targets run. devenv.smoo.nix builds it from one pinned TypeScript source revision with go_1_27 from the nixpkgs-go input. 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-js for 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/native compiler. 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 one nixpkgs-go package set as the module argument goPkgs. A repository's devenv.nix takes Go from it instead of importing the same Go inputs again, so both producers use one evaluated package set.

  • tooling/direnv/nx-socket-dir.sh exports NX_WORKSPACE_ROOT_PATH and NX_SOCKET_DIR. A checkout gets its own socket dir under DEVENV_RUNTIME; an inherited one that resolves into another checkout is dropped. A cowshed workspace (.cowshed/workspace.json) uses <link>/nx, where <link> is the COWSHED_RUNTIME_LINK its .cowshed/env names: 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-path runs first and serves an unserved workspace with cowshed exec <ws> -- true, refusing by name only when that fails (cowshed specs/cowshed/02_workspaces.md, "Workspace environment").

  • On Darwin, tooling/direnv/apple-developer.sh hands the Apple toolchain to Xcode: it unsets CC/CXX, drops a /nix/store SDKROOT or DEVELOPER_DIR, and removes every /nix/store PATH entry that carries nixpkgs xcbuild's xcrun, which answers unable to find sdk: 'macosx' once the nix developer directory is gone. xcrun is then Apple's /usr/bin/xcrun, so xcrun --sdk macosx --show-sdk-path names 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.sh makes cc-rs's compiler variables the shell's own: CC_<target>, CXX_<target>, AR_<target> and RANLIB_<target>, their HOST_ and TARGET_ forms, and CFLAGS, CXXFLAGS, ARFLAGS and RANLIBFLAGS in all of those forms and generic. It keeps the ones the shell's env declares, which devenv.smoo.nix passes 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 (cowshed specs/cowshed/16_build_volumes.md, "One Cargo environment on every path").
  • The repository's local git config includes tooling/workspace.gitconfig by a path relative to the config file (../tooling/workspace.gitconfig in .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 a WARNING naming the path and errno it hit, then Git hooks and repository config were not applied; the next entry that may write them wires them. applyWorkspaceGitConfig from @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's devenv binary cache, whose signing key devenv would otherwise ask the Cachix API for, and the shell's packages come from cache.nixos.org or are built locally. A host that wants that cache names it in its own nix.conf. The shell exports NX_USE_LOCAL=true, so an Nx daemon starting (and nx configure-ai-agents, nx init) answers from the installed nx instead of installing nx@latest from the registry; nx migrate then also runs with the installed CLI, and NX_USE_LOCAL=false restores the newest one for that command. A daemon still asks GitHub for nrwl/nx-ai-agents-config when 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 exports NX_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 every lint task 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/cowshed

Every 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