Skip to content

Latest commit

 

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

basis

basis is a small constellation build system, written in Rust. A constellation is a group of related repositories. You build, clean, and version these repositories together.

basis can do these tasks:

  1. Build a constellation. One YAML manifest controls all repositories. Each repository has a command list for each action (build, clean, or a custom action).
  2. Track versions. basis reads the version of a Rust repository from Cargo.toml. basis reads the version of a C++ repository from a .version file and/or CMakeLists.txt.
  3. Synchronize versions. basis sets all repositories to one common version.
  4. Bump one component. basis increases the version of one repository. Then basis writes the new version into each repository that depends on it.
  5. Verify identity. basis makes sure that the git config user.email of each repository is on a permitted domain. basis does the same check for the e-mail of the GPG signing key.
  6. Report status. basis status shows the git state and the version state of the full constellation.

Install

cargo install --path .
# or run from the workspace:
cargo run -- <args>

Install a constellation

basis install prepares a full constellation from its manifest repository:

basis install acme/platform          # -> https://github.com/acme/platform
basis install git@github.com:acme/platform.git --into platform --branch main

The command does these steps:

  1. basis clones the manifest repository into a directory. The default directory name is the repository name. Use --into DIR to set a different name.
  2. basis reads basis.yaml from that directory.
  3. basis clones each member repository that has a url: into its path, adjacent to the manifest:
platform/
  basis.yaml          # from acme/platform
  core/               # cloned from its url:
  engine/             # cloned from its url:

org/repo is a short form of a GitHub HTTPS URL. A full git URL (https://…, git@…, file://…) is also permitted. basis does not change members that are present. basis reports members that have no url:. After the installation, go into the constellation directory. Then run basis status or basis build.

You can run basis install without an argument from a directory in an existing constellation. (basis finds the manifest in a parent directory. All commands do this search.) Then basis clones only the members that are missing. Use this after you add a repository to the manifest, or to complete a partial checkout. basis does not change members that are present.

Auxiliary files (files:)

basis install clones only the manifest repository. But a constellation frequently also needs loose files: helper scripts for a _postclone hook, or shared configuration files. Declare these files in the manifest. Then basis downloads them explicitly, and the constellation does not depend on a full clone of the manifest repository:

files:
  - path: gen-dbg.sh
    url: https://raw.githubusercontent.com/acme/constellation/main/gen-dbg.sh
    executable: true

basis downloads these files with curl before it clones the members. Thus a hook can use them. basis does not change files that are present.

Post-clone hook (_postclone)

A repository can have a special _postclone action. When basis install clones that repository, basis runs the action automatically in the repository directory. Use the hook to patch generated files or local development files. An action with a name that starts with _ is a hook. Hooks run automatically. The basis action list does not show hooks.

  - name: release-generator
    path: release-generator
    url: https://github.com/acme/release-generator
    lang: other
    actions:
      _postclone: ["../gen-dbg.sh"]   # regenerate local-dev overrides on clone

The manifest (basis.yaml)

constellation: my-product
version: 1.2.0            # optional canonical version of the constellation
email_domain: corp.com   # optional identity policy (see `basis verify`)

repos:
  - name: core           # unique name, used with --repo
    path: core           # path relative to the manifest
    lang: rust           # rust | cpp
    url: https://github.com/acme/core   # optional canonical git URL
    actions:
      build: [cargo build --release]
      clean: [cargo clean]

  - name: engine
    path: engine
    lang: cpp
    url: git@github.com:acme/engine.git
    provides: core               # optional package name exposed to dependents
    version_file: .version       # optional, default: .version
    cmake_file: CMakeLists.txt   # optional, default: CMakeLists.txt
    actions:
      build:
        - cmake -B build -S .
        - cmake --build build
      clean: [rm -rf build]
  • Each repository has a map of actions. Each action is a list of shell commands, in order. The commands run in the repository directory through sh -c.
  • You can select almost all action names. Run an action with basis <action>. Only the reserved names are not permitted (see below).

Commands

basis                                    # list all actions in the manifest

# A name that is not reserved runs the matching `action` across the constellation:
basis <action> [--repo NAME]... [-k] [-n] [--tmux|--no-tmux]
basis build                              # run the `build` action everywhere
basis run                                # run the `run` action (e.g. services)
basis test --repo core --tmux            # run `test` in a tmux display

# Reserved subcommands (not permitted as action names):
basis install [org/repo] [--into DIR] [--branch B]   # clone a constellation
                                                     # (no arg: clone missing
                                                     #  members of current manifest)
basis update [--repo NAME]...            # git pull --ff-only the cloned repos
basis status                             # git + version status of all repos
basis verify                             # check git/GPG e-mail domains
basis display [NAME] [--detached|--kill] # start a tmux dev dashboard
basis mcp                                # serve the constellation over MCP (stdio)

basis version                            # alias of `version show`
basis version show                       # list every repo's version
basis version set <X.Y.Z>                # set an explicit version everywhere
basis version sync [--to <X.Y.Z>]        # set all repos to one version
basis version bump <repo> [--major|--minor|--patch|--to X.Y.Z]

build, clean, run, and test are not special names. They are action names that basis finds in the manifest. The names install, update, status, verify, display, version, and mcp are reserved. Only these names are not permitted as action names.

Common flags:

  • -f, --file <PATH> — the manifest path. The default is basis.yaml. If PATH is a bare filename, basis searches for it upward from the current directory. (git finds .git in the same manner.) Thus you can run basis from each subdirectory of the constellation. If PATH has a directory component (example: ../basis.yaml), basis uses it without a search. Repository paths are relative to the directory that contains the manifest.
  • -r, --repo <NAME> — operate only on the given repositories. You can give this flag more than one time.
  • -k, --keep-going — if a command fails, continue with the other repositories.
  • -n, --dry-run — show the commands, but do not run them.
  • -t, --tmux — run the action in a per-task tmux display. Each repository gets one pane. The panes run in parallel. You can add --detached and --layout <L>.

Version sync target

basis version sync selects its target version in this order:

  1. The --to <X.Y.Z> value, if you give it.
  2. If not, the top-level version: of the manifest.
  3. If not, the highest semver version in the repositories.

For a Rust repository, basis writes [package].version in Cargo.toml. basis keeps the file format. For a C++ repository, basis writes the .version file. If the CMake file has a project(... VERSION x.y.z ...) call, basis also updates that call.

Bump a component and its dependents

basis version bump <repo> increases the version of one component. The default is --patch. You can give --major, --minor, or --to X.Y.Z. Then basis updates each repository that depends on the component:

  • Rust dependents — basis updates the version of the applicable entry in [dependencies], [dev-dependencies], or [build-dependencies]. basis keeps path, the features, and package = renames.
  • C++ dependents — basis sets the new version in find_package(<name> <ver> ...) in the CMake file.

basis finds the dependents by the provided name of the component. This is the provides: field, if set. If not, it is the Rust crate name ([package].name). If not, it is the repository name.

$ basis version bump core --minor
bumping core 1.0.0 -> 1.1.0 (provides 'core')
  ✓ core version set to 1.1.0
  ↳ app now requires core 1.1.0
  ↳ engine now requires core 1.1.0

tmux displays

Per-task displays (from the manifest)

A task can name a display in the manifest. A display is the tmux session in which the task runs. basis creates the display only when the task runs: one pane for each repository, in parallel. basis creates nothing before that time.

Use a display for a long task that you monitor: services, or watchers. Do not give a display to a short task (build, clean). A task without a display runs in the current terminal:

tasks:
  run:
    display: services         # run this task in the "services" display
    layout: even-vertical
  # build / clean: no display — they run inline in the current terminal

With this configuration, basis run does these steps:

  1. basis creates a tmux session with the name services.
  2. Each selected repository that has the action gets one pane. The pane starts in the repository directory. The pane runs the commands of the action.
  3. basis skips the repositories that do not have the action.
  4. basis applies the layout and attaches to the session.

The usual example is a long run task. It starts each service. Each service shows its output live in its own pane:

tasks:
  run:
    display: services
    layout: even-vertical     # stacked logs, one per repo
repos:
  - name: api
    actions: { run: ["cargo run --bin api"] }
  - name: worker
    actions: { run: ["cargo run --bin worker"] }
  - name: web
    actions: { run: ["npm run dev"] }
basis run            # api / worker / web each get a pane in "services", logs live

basis sends the commands to a live shell. When you push Ctrl-C, the pane stays open. You can start the process in the pane again. When you run basis run again, basis attaches to the same session.

Close a display: basis binds Ctrl-q (without prefix) to kill-session in each display it creates. When you push Ctrl+Q in an attached display, tmux closes the display and stops the processes in it. Ctrl-b d only detaches you. The display then continues in the background.

Restart a display: When you push Ctrl+R (without prefix), basis restarts each pane. basis sends Ctrl-C to stop the process. Then basis runs the initial pane command again. (basis keeps this command in the @basis_restart pane option.) Use this after a code change: one key runs the full task again. Note: the two key bindings are global in the tmux server. In tmux, they replace the standard Ctrl+Q and Ctrl+R functions. Thus the reverse-i-search of the shell is not available in the panes.

Overrides for one invocation:

basis run                        # uses the task's display: setting
basis run --tmux                 # force a display (named <constellation>-run)
basis run --no-tmux              # force the current terminal for this run
basis run --detached             # with tmux: create but don't attach
basis run --layout tiled         # with tmux: override the layout

Predefined dashboards (displays:)

A display can also be a named tmux session in the manifest: a permanent development dashboard (servers, watchers, logs, a free shell):

displays:
  dev:
    session: myproj-dev      # optional, default <constellation>-<display>
    layout: tiled            # tiled | even-horizontal | even-vertical | main-vertical | ...
    panes:
      - { repo: core,   command: "cargo watch -x run" }   # cmd in the repo dir
      - { repo: engine, action: build }                   # reuse a repo action
      - { name: logs,   cwd: ., command: "tail -f log/dev.log" }
      - { name: shell }                                    # just a shell in base dir
basis display              # list configured displays
basis display dev          # create the session (if needed) and attach
basis display dev --detached   # create but don't attach (prints attach hint)
basis display dev --kill       # stop and remove the session

Each pane starts in a directory. basis uses cwd, if given. If not, basis uses the repo directory. If not, basis uses the manifest directory. Each pane runs a command. basis uses command, if given. If not, basis uses the given action of repo (basis connects its commands with &&). If not, basis starts a plain shell. basis sends the commands to a live shell. When the task stops, the pane stays open. You can run the task again. basis display NAME is idempotent: if the session exists, basis attaches to it and does not create it again.

basis status shows each configured display. It also shows if the tmux session of the display operates:

displays:
  dev      ● running  3 pane(s), tiled            [demo-dev]
  tests    ○ stopped  2 pane(s), even-horizontal  [demo-tests]

MCP server

basis mcp serves the constellation through the Model Context Protocol with the stdio transport. Thus an AI agent can control the constellation. basis generates the tool set from the manifest:

  • One tool for each action (build, clean, test, …). Each tool has a repos filter. The filter permits only the repositories that have the action. Each tool also has the keep_going and dry_run flags. Hooks (_postclone, …) do not become tools. If the task of an action has a display: (long services), the tool description has a LONG-RUNNING warning. Through MCP, such an action runs inline, and the call does not return until the processes stop.
  • Fixed tools: basis_repos (the member list as structured data), basis_status, basis_update, basis_versions, basis_version_sync, basis_version_bump.

For each call, the server starts the basis binary again and collects the output. Thus a tool call and the equal CLI command have the same behavior. (Actions always run with --no-tmux.) The server reads the manifest again for each request. Thus a change to basis.yaml shows in the subsequent tools/list.

The JSON-RPC stream uses stdout. Thus the server writes its log to stderr: one start message (constellation, repository count, tool count), and one line for each request (tool name, arguments, ok or error, duration):

basis mcp: constellation 'example' — 2 repo(s), 9 tool(s); serving MCP on stdio
· initialize (claude-code) → ok [0ms]
· tools/list → ok [1ms]
· build {"dry_run":true,"repos":["core"]} → ok [12ms]

Add the server to an MCP client. basis status shows the applicable commands for Claude Code and for Codex, with the full manifest path:

mcp: serve this constellation to AI agents (stdio):
  claude mcp add my-product -- basis --file /path/to/constellation/basis.yaml mcp
  codex mcp add my-product -- basis --file /path/to/constellation/basis.yaml mcp

A plain basis mcp command also operates when the work directory of the client is in the constellation. basis then finds the manifest with the usual upward search.

Identity verification

basis verify makes sure that contributors use a company identity. For each repository that has an e-mail-domain policy, the command checks two items:

  • git config user.email is on a permitted domain.
  • The OpenPGP signing key (user.signingkey, or the key that matches the git e-mail) has a user ID with an e-mail on a permitted domain.

The domains come from email_domain (one value) and/or email_domains (a list). A repository can replace the constellation policy with its own field. A signature in SSH format (gpg.format=ssh) has no e-mail. basis reports it as unverifiable, not as a failure. If a checked repository fails, the command exits with a code that is not zero.

$ basis verify
==> core
  allowed domains: corp.com
  ✓ git email: dev@corp.com
  ✓ gpg key: ABCD1234 [dev@corp.com]
  ✓ ok

basis status does the same checks. It shows a short id ✓ / id ✗ / id ! column for each repository ( if no policy applies), plus a summary line. basis status only gives information, and it always exits with code 0. basis verify is the gate that stops errors. Use verify in CI or in a pre-push hook.

$ basis status
  core    rust  1.0.0       id ✓  main clean origin✓
  app     rust  1.0.0       id ✗  main dirty origin✗

versions: all versions at 1.0.0
identity: 1 repo(s) fail (run `basis verify` for details)

Canonical repository URLs

Each repository can give a canonical git url:. basis status compares it with the local origin remote. The report shows one of these values:

  • origin✓origin and the canonical URL are the same.
  • origin✗origin points to a different location. The report shows the expected value and the found value below the table.
  • no-origin — the repository has no origin remote.
  • missing — the repository directory is not cloned.

basis normalizes the URLs before the comparison. Thus git@github.com:acme/core.git and https://github.com/acme/core are the same repository. basis ignores the scheme, the git@ userinfo, and a .git ending.

Example

The examples/ directory has an example that you can run. From the repository root:

basis -f examples/basis.yaml status
basis -f examples/basis.yaml version sync
basis -f examples/basis.yaml build -n

About

Simple build system for complex projects

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages