Changelog
Every release, with what changed and how to upgrade.
[0.6.0] - 2026-09-18
Changed
--graphdraws a tree: each service under the dependency that gates its start, with its readiness condition and any other dependencies asalso after.--jsonaddswaveto each service.
Added
- Launch warns when a task directory declares dependencies but has no
node_modules, instead of failing a few lines later with "command not found".
[0.5.0] - 2026-09-17
Added
spinup --initregisters a project step by step: choose the alias, review each detected command with where it came from, change, remove or add services, pick tmux or foreground mode, preview everything that will be written, and confirm. Cancelling writes nothing.--path <dir>registers a directory other than the current one.--relinkpoints a registered alias at the current directory or--path, for moved checkouts and worktrees, after confirmation. Running an alias from another checkout of the project suggests--relink.-y/--yesanswers confirmations for scripts. Without a terminal,--relinkand--regeneraterefuse unless it is given.--logswrites each service's output to$XDG_STATE_HOME/spinup/logs/<alias>/<service>.log: private files, the previous run kept as.log.1, and a 10 MB limit per file.--statusshows the paths.spinup --update [version]replaces the binary with a release after checking it againstSHA256SUMSand confirming it runs.spinup --completion bash|zsh|fishprints a completion script for aliases, flags, actions and services, forspinupand every alias command.--checkand--dry-runreport ports the action needs that are already in use, from readiness conditions and Compose's published ports, naming the listener.- Detection for Go, Rust, Ruby, PHP, Java/Kotlin (Spring Boot, Quarkus) and Deno.
schema/spinup.schema.json, generated from the config schema. Generated configs reference it so editors complete and validate.spinup.yml.- Releases include musl builds for Alpine, a man page,
SHA256SUMSand signed build provenance. docs/with installation, a quick start, configuration, commands, detection, sessions and upgrading.
Changed
- The installer verifies checksums, confirms the binary runs and reports the expected version, stages the download in the destination directory, and only then replaces an existing binary. It picks musl builds automatically and rejects malformed options before downloading.
- Every release is smoke-tested on each published file, on Linux x64 and arm64
(glibc and musl) and macOS arm64 and x64, and a tag that disagrees with
package.jsonis refused. CI runs the test suite natively on the same platforms. - All workflow actions are pinned to commit SHAs.
Fixed
- Shell completion on macOS's bash 3.2, which has no
mapfile.
[0.4.0] - 2026-09-16
Upgrading
This release changes behavior that existing configs and scripts can depend on:
- Configs are validated strictly. A misspelled or unsupported key, a blank command,
an invalid environment variable name, a dependency cycle or an empty
simpleaction is now an error.spinup <alias> --doctorshows the offending field. - The exit status is the failed task's own, 130 or 143 after a signal, 2 when
--checkor--doctorfinds the action cannot run, and 3 from--statuswhen nothing is running. Every failure used to exit 1. - A
delayholds back only that service's dependents. - Relaunching an alias whose tmux session is running attaches to it instead of
restarting it. Use
--restartfor that. - Newly generated configs run Compose as one
composeservice. Existing configs are unchanged until--regenerate. - The GitHub repository is now
PedroElizalde01/spinup. Old URLs redirect; the installer URL in this README is the new one.
Added
Readiness conditions. A service can declare
ready: { port },{ http },{ log }or{ exit: 0 }with a timeout, and its dependents wait until it holds. A failing or timed-out condition stops the run and names the condition; in tmux the session is kept for inspection.--planand--graphshow conditions.--status,--attach,--stopand--restart [service]for tmux workspaces. They act only on the session spinup created for that project and action.--restart <service>respawns one pane from the current config, waits for its readiness condition and names its dependents.--statussupports--jsonand exits 3 when the session is not running.Project commands work without an alias inside a project directory.
spinup --startruns the directory's config as it is, without registering it, installing a command or generating anything.Detection recognizes commands the project defines for development:
bin/dev,Procfile.dev, anddevinjustfile,Makefile,Taskfile.ymlandmise.toml.Each generated service carries a comment saying where its command came from. Scanning prints it, and
--doctorshows a fresh scan's origins and notes.Python services are detected in workspace members as well as the root, and run through
uv run,poetry runor a local venv when the project uses one.--action <name>selects a generated action for launching,--plan,--graph,--env,--checkand--doctor. Inspection uses exactly the action a launch would; an unknown name fails before anything happens and lists the alternatives. A non-default action launches in its own tmux session,<alias>-<action>.--jsonon--list,--plan,--graph,--env,--checkand--doctorprints one JSON document on stdout and nothing else. Environment values are never included.--dry-runwith a launch resolves the action, start order, directories and environment origins and starts nothing.--no-color, alongsideNO_COLOR.--planprints start order, resolved directories, dependencies and delays.--graphprints each service with the services it actually depends on; independent services no longer appear chained together.--listshows each project's default action, mode and whether its config is present and valid.Configs carry an optional
version: 1. A newer version is refused with an upgrade message.spinupwith no arguments andspinup --helpprint the wordmark, version and a one-line tagline before the usage text. Nothing is printed when stdout is not a terminal.Interactive edits patch the existing YAML document node by node, so comments and formatting on everything that did not change survive a save. Saving without changing anything leaves the file's bytes alone and reports "No changes."
Regenerating keeps the previous config bytes in a private
.spinup.yml.bakbefore replacing the file.
Changed
Detection prefers a project's own launcher, then a monorepo's root
devscript, and only then launches workspace members one by one. Declared workspace globs and!exclusions are honored anywhere in the repository, and thepackageManagerfield wins over lockfiles, with conflicts reported.testandcheckscripts are no longer chosen as development servers, a library'sindex.jsis not treated as a server, and an empty or library-only project is no longer registered with an inventednpm start. Without a detected command, registration asks for one in a terminal and fails elsewhere.Python entrypoints are confirmed by finding the app object in source instead of guessed from dependency names. Without a venv or tool,
python3is used.Compose is one
docker compose upservice namedcompose, resolved bydocker compose configwhen available. It used to be one process per container from a statically parsed file.Colliding service names are made unique by runtime or path before dependencies are inferred.
Required tools are inferred from the first word of the selected action's commands.
uv run,poetry runand venv commands no longer require a system Python, and Bun commands no longer require Node.Services start concurrently as soon as their own dependencies are ready. A
delaynow holds back only that service's dependents, not every later service.The CLI exits with the failed task's own status, 130 after a handled Ctrl+C and 143 after a handled SIGTERM. Every failure used to exit 1.
--checkand--doctorexit 2 when the selected action cannot run.Validation is strict. Unknown keys, blank commands, unexportable environment variable names, dependency cycles and empty
simpleactions are rejected at load, with the real window and pane path of the offending field.The generated command's
--startmeans "launch unless a management flag was given", somy-app --doctorinspects through the wrapper. An explicitspinup --start <alias>for an unregistered alias fails instead of registering the current directory.Both backends receive one effective environment, decided once: the invoking shell, then the selected files where the shell did not set a key. A tmux pane no longer inherits stale keys from a server started long ago.
Regeneration describes itself as a replacement, lists changed environment keys without their values, reports reordering and window changes, and refuses to proceed without a terminal to confirm in. An unregistered project with an existing config gets the same preview instead of a silent overwrite.
Switching a multi-window tmux action to
simpleasks before dropping the other windows, naming them and their services.--interactivefails clearly without a terminal instead of hanging on a prompt.--removereports when no generated command was found for the alias.--envreports the action, which files were read, the origin of each key, and which keys the shell is overriding. Values remain masked.The favicon is an S mark; the
RunitConfigtype isSpinupConfig.
Fixed
Launching an alias whose tmux session already existed killed that session and rebuilt it, restarting the user's running work or destroying an unrelated session that happened to share the name. Sessions spinup creates now record their project and action; a relaunch attaches to its own session, and any other session with that name is left alone with an error naming its owner.
tmux failures echoed the full command line, which for pane creation included every
-e KEY=VALUE. Failures now report the operation, target and exit status with redacted output.A lone task ran in spinup's own process group, so SIGTERM sent to spinup left its descendants running. Multi-task cleanup skipped any group whose shell had exited even when its child still held a port. Every task now runs in its own group; shutdown sends SIGTERM, waits a bounded grace period, then SIGKILLs survivors, checking group liveness rather than the shell's exit state.
Registration wrote the project config before the wrapper, so a rejected alias had already rewritten
.spinup.yml, and a failed registry write left a runnable wrapper with no entry. The wrapper is created first and removed again if a later step fails.Removal deleted the registry entry before the wrapper, so a failed unlink left a command that launched nothing. The wrapper goes first; a failed removal keeps the registration and reports failure.
The registry lock was evicted on age alone, deleting a slow but live holder's lock. The lock now records its holder's pid and is recovered only when that process is gone; otherwise the timeout names the holder.
Interactive saves moved the edited tmux window to the front, invented a layout when none was configured, and dropped dependencies on services in other windows. Edits now apply to the original window in place, and only references to services the user removed are dropped, across every window.
A real v0.2.2 wrapper carries no marker, so upgrading left it exec'ing the removed
runitbinary while spinup refused to repair it as a foreign file. Ownership is now established by matching the whole body against the formats spinup has written for that alias. Legacy alias collisions no longer produce a 66-character key; migration creates the new wrapper before reclaiming the old.A dangling symlink in the shim directory looked like a missing file and the wrapper was created at the link's target. Destinations are classified with
lstat, symlinks and directories are refused, new wrappers are created exclusively, and approved wrappers are replaced through a rename.Saving an existing
0600config republished it with the umask default. The original mode is preserved, intermediate files are private, and a symlinked config has its target replaced rather than the link becoming a regular file.spinup --help | headno longer prints an EPIPE stack trace.install.sh --versionwith a release older than v0.3.0 fails with an explanation instead of requesting an asset that does not exist.Compose files were chosen in the wrong order: with both
compose.yamlanddocker-compose.ymlpresent, spinup described a different application than Compose runs. Override files and profiles are now respected.A Node and a Python app at the root, or two workspaces with the same directory name, produced duplicate service names and a config that failed validation.
A long value on the setup card, such as a shim path outside the home directory, ran past the card's border.
Output forwarding in
simplemode honors backpressure: a slow consumer stalls the task instead of the parent queueing its whole output in memory.A detached tmux session was created at 80x24, where a few splits already failed with "no space for new pane". It now takes the terminal's size or a generous default and is resized on attach.
A wrapper for a new alias was briefly visible empty between creation and write. It is now staged privately and published atomically; a lost creation race accepts the winner's wrapper instead of failing.
Environment precedence is defined and documented. A value set in the invoking shell now wins over an environment file, and a task's own
env:block wins over both. Files were previously applied on top of the shell, so an explicitPORT=4000 my-appwas silently discarded..env.developmentis no longer read for every action. It was applied even to abuildaction, conflating an action with a deployment mode. Files are now.env,.env.local,.env.<action>,.env.<action>.local, and a mode file for a different action is reported as skipped instead of being ignored in silence.Environment files resolve against the action's
rootrather than the directory the project was registered from.loadEnvno longer mutates the parent process environment, so--envcannot change how a later command runs.--checkand--doctorexit non-zero when the project cannot run. Both reported success while printing the failure:--checkexited 0 with a missing working directory, and--doctorprintedreadywith a required tool absent.A working directory that exists but is a regular file is now reported.
access()succeeds for a file, so such a config passed validation.Tool requirements are inferred from the action that will actually run, instead of every action in the file, which demanded tmux from projects that never use it.
pythonis probed aspython3first, removing a false negative on the many distributions that ship nopythonexecutable.The Docker CLI, the Compose plugin, and a reachable daemon are checked separately.
docker -vproved only the first.Tool probes are bounded by a timeout so a diagnostic cannot hang.
[0.3.0] - 2026-09-06
Renamed
The tool is now spinup. runit is an established UNIX service supervisor that
already occupies apt install runit, brew install runit and the runit npm package,
which left no usable distribution channel and made the project hard to search for.
Existing installations migrate on the next run, with each change reported:
~/.config/runitmoves to~/.config/spinup, and only when the new path is absent.- Generated commands that still exec
runitare rewritten to execspinup. - A project's existing
.runit.ymlkeeps being read and written in place. New projects get.spinup.yml. No project ends up with both. RUNIT_SHIM_DIRis still honored;SPINUP_SHIM_DIRis the current name.
The GitHub repository and installer URL are unchanged.
A reliability release. Every entry below was reproduced against v0.2.2 and
re-verified after the change; see RUNIT_REVIEW.md section 11 for the evidence.
Security
- Aliases now accept one narrow format everywhere. Previously
../../namewrote a file outside the shim directory, an alias containing$(...)ran that substitution on every invocation, and an existing executable in the shim directory was replaced without warning. spinuprefuses an alias that would shadow a command already onPATH, and refuses to overwrite or delete a file in the shim directory that it did not create.- Compiled binaries no longer absorb the
.envof the directory they are invoked from. Launching one project from inside another handed over that directory's environment, including credentials. - tmux panes receive their environment through tmux rather than having
exportstatements typed into a shell, so values are no longer recoverable from pane scrollback. - Replaced
js-yamland updatedyaml, clearing four published advisories (two high).bun auditnow runs in CI.
Fixed
- Commands are no longer prefixed with
execbefore being handed to a shell. That replaced the shell at the first word, soa && bran onlya, pipelines lost their tail, andFOO=bar cmdfailed outright. - A failing task now aborts its siblings instead of leaving them running until the longest-lived one exits on its own, and the originating exit code is preserved.
- Multi-task runs terminate the whole process group, so a shell's descendants no longer survive as orphans holding ports.
- Task output is streamed rather than buffered. A task emitting more than 100MB was previously killed mid-run.
- tmux sessions are targeted by exact name. Launching
apipreviously matched and destroyed an unrelatedapi-stagingsession. - tmux windows and panes are addressed by the ids tmux assigns, fixing startup under
base-index/pane-base-indexset to 1. - Layout is reapplied as panes are added; eight services in a small window previously
failed with
no space for new paneafter the fourth. spinupswitches the current client instead of failing when already inside tmux.- A failure while building a workspace tears down only the session
spinupcreated. - Concurrent registrations no longer lose entries. Twenty parallel registrations kept 16 of 20 while creating all 20 shims; they now keep all 20.
- The registry and the project config are replaced atomically, so an interrupted write leaves either the old file or the new one.
- Shim creation precedes registration, so a refused alias cannot leave a registered project with no runnable command.
- Interactive editing preserves
env,delayanddependsOn, and the window's own name and layout, instead of rebuilding each service from name, cwd and command. Removing a service also drops references to it, so the result still validates. - Opening the config in
$EDITORand changing nothing now leaves the file untouched. It was reserialized on every edit, stripping comments and reflowing inline lists. $EDITORvalues carrying arguments, such ascode --wait, work. The value was treated as a single executable name and failed with ENOENT.--regeneratecompares configurations structurally. It previously diffed sets of trimmed lines, so swapping two services' commands reported no changes at all and the change was never applied.
Added
linux-arm64release binaries. The installer already resolved that platform, so it previously pointed at an asset that was never built.- An MIT
LICENSE. The project had none, which reserved all rights and contradicted distributing it. spinup --version, sourced frompackage.jsonso source and compiled builds agree.XDG_CONFIG_HOMEsupport for the registry, ignoring a relative value as the spec requires, andSPINUP_SHIM_DIRto relocate generated commands.- A one-time migration for aliases registered before the format was enforced: they are renamed, their shims moved, and each change reported. Entries that cannot be rescued are reported and left in place rather than dropped.
- A CI workflow gating typecheck, tests,
bun audit, ShellCheck, and a binary smoke test that verifies version parity and the dotenv isolation above. Releases now require it to pass. Actions are pinned to commit SHAs and build jobs run read-only.
Changed
- Unrecognized command-line arguments are rejected instead of silently discarded.
- tmux workspace mode requires tmux 3.0 or newer, the first release with the
-eflag used to pass environment to a pane.mode: simpleneeds no tmux. - tmux panes are created in configuration order and started in dependency order, so pane position no longer depends on the dependency graph.
Known issues
Carried over from v0.2.2 and tracked in RUNIT_REVIEW.md:
- Generated
docker,prisma-generate, andprisma-migrateactions still cannot be selected; every start path uses the default action (F18). dependsOnorders startup only; it does not wait for readiness (F10).--graphdraws edges between independent services (F17).
[0.2.2] - 2026-03-14
Initial documented release.