@bensandee/tooling
v0.27.1
Published
CLI tool to bootstrap and maintain standardized TypeScript project tooling
Downloads
3,335
Readme
@bensandee/tooling
CLI to bootstrap and maintain standardized TypeScript project tooling.
Installation
pnpm add -D @bensandee/tooling
# Or run directly
pnpm dlx @bensandee/tooling repo:syncConventions
The tool auto-detects project structure, CI platform, project type, and Docker packages from the filesystem. .tooling.json stores overrides only — omitted fields use detected defaults. Runtime commands (docker:build, checks:run, release:changesets) work without running repo:sync first.
| Convention | Detection | Default | Override via |
| ----------------- | ----------------------------------------------------- | ---------------------------------------- | ------------------------------------ |
| Project structure | pnpm-workspace.yaml present | single | structure in .tooling.json |
| CI platform | .github/workflows/ or .forgejo/workflows/ dir | none | ci in .tooling.json |
| Project type | Dependencies in package.json (react, node, library) | default | projectType in .tooling.json |
| Docker packages | Dockerfile or docker/Dockerfile in package dirs | — | docker map in .tooling.json |
| Formatter | Existing prettier config detected | oxfmt | formatter in .tooling.json |
| Release strategy | Existing release config detected | monorepo: changesets, single: simple | releaseStrategy in .tooling.json |
CLI commands
Project management
| Command | Description |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| tooling repo:sync [dir] | Detect, generate, and sync project tooling (idempotent). First run prompts for release strategy, CI platform (if not detected), and formatter (if Prettier found). Subsequent runs are non-interactive. |
| tooling repo:sync --check [dir] | Dry-run drift detection. Exits 1 if files would change. CI-friendly. |
| tooling checks:run | Run project checks (build, docker:build, typecheck, lint, test, format, knip, tooling:check, docker:check). Flags: --skip, --add, --fail-fast. |
Flags: --yes (accept all defaults), --no-ci, --no-prompt, --eslint-plugin
checks:run
Runs checks in order: build, docker:build, typecheck, lint, test, format (--check), knip, tooling:check, docker:check. Checks without a matching script in package.json are silently skipped.
The --skip flag supports glob patterns via picomatch:
# Skip all docker steps
tooling checks:run --skip 'docker:*'
# Skip specific checks
tooling checks:run --skip build,knipThe --add flag appends extra checks (must be defined in package.json):
tooling checks:run --add e2eThe generated ci:check script defaults to pnpm check --skip 'docker:*' since CI environments typically lack Docker support.
Release management
| Command | Description |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| tooling release:changesets | Changesets version/publish for Forgejo CI. Flag: --dry-run. Env: FORGEJO_SERVER_URL, FORGEJO_REPOSITORY, FORGEJO_TOKEN. |
| tooling release:simple | Streamlined release using commit-and-tag-version. |
| tooling release:trigger | Trigger a release workflow. |
| tooling forgejo:create-release | Create a Forgejo release from a tag. |
| tooling changesets:merge | Merge a changesets version PR. |
Docker
| Command | Description |
| ------------------------ | --------------------------------------------------- |
| tooling docker:build | Build Docker images for discovered Docker packages. |
| tooling docker:publish | Build, tag, and push Docker images to a registry. |
Docker packages are discovered automatically. Any package with a Dockerfile or docker/Dockerfile is a Docker package. Image names are derived as {root-package-name}-{package-name}, build context defaults to . (project root). For single-package repos, Dockerfile or docker/Dockerfile at the project root is checked.
When Docker packages are present, repo:sync generates a deploy workflow (.forgejo/workflows/publish.yml or .github/workflows/publish.yml) triggered on version tags (v*.*.*) that runs pnpm exec tooling docker:publish.
Overrides
To override defaults, add a docker entry to .tooling.json:
{
"docker": {
"server": {
"dockerfile": "packages/server/docker/Dockerfile",
"context": "."
}
}
}The context field defaults to "." (project root) when omitted. Versions for tagging are read from each package's own package.json.
docker:build
Builds all discovered packages, or a single package with --package:
# Build all packages with docker config
tooling docker:build
# Build a single package (useful as an image:build script)
tooling docker:build --package packages/server
# Pass extra args to docker build
tooling docker:build -- --no-cache --build-arg FOO=barTo give individual packages a standalone image:build script for local testing:
{
"scripts": {
"image:build": "pnpm exec tooling docker:build --package ."
}
}Flags: --package <dir> (build a single package)
docker:publish
Runs docker:build for all packages, then logs in to the registry, tags each image with semver variants from its own version field, pushes all tags, and logs out.
Tags generated per package: latest, vX.Y.Z, vX.Y, vX
Each package is tagged independently using its own version, so packages in a monorepo can have different release cadences. Packages without a version field are rejected at publish time.
Flags: --dry-run (build and tag only, skip login/push/logout)
Required environment variables:
| Variable | Description |
| --------------------------- | --------------------------------------------------------------------- |
| DOCKER_REGISTRY_HOST | Registry hostname (e.g. code.orangebikelabs.com) |
| DOCKER_REGISTRY_NAMESPACE | Full namespace for tagging (e.g. code.orangebikelabs.com/bensandee) |
| DOCKER_USERNAME | Registry username |
| DOCKER_PASSWORD | Registry password |
Config file
.tooling.json stores overrides only — fields where the project differs from what convention/detection produces. A fully conventional project has {} or no .tooling.json at all.
Available override fields:
| Field | Type | Default (detected) |
| -------------------- | ------- | -------------------------------------------------------------------------------- |
| structure | string | "monorepo" if pnpm-workspace.yaml present, else "single" |
| useEslintPlugin | boolean | true |
| formatter | string | "prettier" if config found, else "oxfmt" |
| setupVitest | boolean | true |
| ci | string | Detected from workflow dirs, else "none" |
| setupRenovate | boolean | true |
| releaseStrategy | string | Detected from existing config, else monorepo: "changesets", single: "simple" |
| projectType | string | Auto-detected from package.json deps |
| detectPackageTypes | boolean | true |
Library API
The "." export provides type-only exports for programmatic use:
import type {
ProjectConfig,
GeneratorResult,
GeneratorContext,
Generator,
DetectedProjectState,
LegacyConfig,
} from "@bensandee/tooling";| Type | Description |
| ---------------------- | ----------------------------------------------------------------------------------------------- |
| ProjectConfig | User config shape (persisted in .tooling.json) |
| GeneratorContext | Context passed to generator functions (exists, read, write, remove, confirmOverwrite) |
| GeneratorResult | Result from a generator (created/updated/skipped files) |
| Generator | Generator function signature: (ctx: GeneratorContext) => Promise<GeneratorResult> |
| DetectedProjectState | Detected existing project state (package manager, CI, etc.) |
| LegacyConfig | Legacy config detection for migration |
Docker check
The @bensandee/tooling/docker-check export provides utilities for checking Docker Compose stacks via health checks.
Quick start
import { createRealExecutor, runDockerCheck } from "@bensandee/tooling/docker-check";
import type { CheckConfig } from "@bensandee/tooling/docker-check";
const config: CheckConfig = {
compose: {
cwd: "./deploy",
composeFiles: ["docker-compose.yaml"],
services: ["api", "db"],
},
buildCommand: "pnpm image:build",
healthChecks: [
{
name: "API",
url: "http://localhost:3000/health",
validate: async (res) => res.ok,
},
],
timeoutMs: 120_000,
pollIntervalMs: 5_000,
};
const result = await runDockerCheck(createRealExecutor(), config);
if (!result.success) {
console.error(result.reason, result.message);
}Exports
| Export | Description |
| -------------------------------------- | ----------------------------------------------------------------- |
| runDockerCheck(executor, config) | Full lifecycle: build, compose up, health check polling, teardown |
| createRealExecutor() | Production executor (real shell, fetch, timers) |
| composeUp(executor, config) | Start compose services |
| composeDown(executor, config) | Stop and remove compose services |
| composeLogs(executor, config) | Stream compose logs |
| composePs(executor, config) | List running containers |
| checkHttpHealth(executor, check) | Run a single HTTP health check |
| getContainerHealth(executor, config) | Check container-level health status |
Types
| Type | Description |
| --------------------- | ------------------------------------------------------------------------------------------ |
| CheckConfig | Full check config (compose settings, build command, health checks, timeouts) |
| ComposeConfig | Docker Compose settings (cwd, compose files, env file, services) |
| HttpHealthCheck | Health check definition (name, URL, validate function) |
| CheckResult | Result: { success: true, elapsedMs } or { success: false, reason, message, elapsedMs } |
| DockerCheckExecutor | Side-effect abstraction (exec, fetch, timers) for testability |
| ContainerInfo | Container status info from composePs |
