dstow

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: GPL-3.0 Imports: 1 Imported by: 0

README

dstow

Deploy dotfiles and configuration as symlinks, from packages in repos.

dstow links the files in a package into a target directory (your $HOME by default), the same way GNU Stow does — but it remembers what it deployed, works across many repos at once, names things by a stable qualified identity, and never depends on the directory you happen to be standing in.

  • Repos are where packages come from — a local directory, or a remote clone (github:owner/name).
  • Packages are the top-level directories inside a repo. Each package is a tree of files to link into the target.
  • Targets are where the links go. The default is $HOME; any level of config can point elsewhere.
github:rocne/dotfiles::zsh
└─ scheme ┘└ coordinate ┘  └ package

That fully-qualified name (FQN) is a package's stable identity. You rarely type the whole thing — any unambiguous suffix works (zsh, dotfiles::zsh, rocne/dotfiles::zsh), and the working directory never changes what a command does.


Table of contents


Install

Add dstow's bootstrap to your shell rc. It puts ~/.local/bin on PATH and installs dstow only if it is missing — when dstow is already present the snippet is silent, invisible, and offline (the installer is never even fetched):

dstow snippet rc >> ~/.bashrc      # or ~/.zshrc, ~/.profile, …

The emitted snippet is plain POSIX sh — dstow never edits your rc files, it only prints the text to stdout for you to redirect. On a fresh machine where dstow is not yet installed, run the snippet's install line directly:

curl -fsSL https://raw.githubusercontent.com/rocne/dstow/main/install.sh | sh

The installer drops dstow into ~/.local/bin. It is idempotent: run against a machine that already has dstow, it prints one status line and exits 0.

Installer flags: --force reinstalls even when present; --version vX.Y.Z ensures exactly that release — already installed at that version it exits 0, otherwise it installs it (no implied force). The install dir is tunable (--install-dir, DSTOW_INSTALL_DIR, XDG_BIN_HOME); ~/.local/bin is the default the snippet relies on. Downloads are checksum-verified always, and cosign-verified when cosign is available. See install.sh --help for the full surface.

With Go

If you have a Go toolchain, install straight from source — the binary reports the module version it was built from:

go install github.com/rocne/dstow/cmd/dstow@latest
Shell completion
dstow completion bash        # also: zsh, fish, powershell

See dstow completion --help for where to place the output for your shell.


Quickstart

# 1. Register a repo of packages (clones a remote; registers a local path in place).
dstow repo add github:rocne/dotfiles
dstow repo add ~/my-dotfiles

# 2. See what you have (reads config only — never touches disk).
dstow list                       # your repos
dstow list dotfiles              # a repo's packages

# 3. Deploy. Name packages, or a whole repo, by any unambiguous suffix.
dstow stow zsh git tmux
dstow stow dotfiles              # a repo: all of its packages

# 4. Check reality.
dstow status                     # what is actually deployed, live

# 5. Later: refresh, or remove.
dstow restow zsh                 # unstow then stow (pick up changes)
dstow unstow tmux                # remove tmux's links

First run on a machine that already has live config files? Adopt them into the package as you stow, so nothing is destroyed:

dstow stow --all --adopt

The model

dstow keeps three things straight, and gives you a command for each question:

Question Command Reads
What do I have configured? dstow list config only, never disk
What does dstow know about this? dstow info config + metadata, never disk
What is actually deployed? dstow status the live filesystem
  • A repo is registered once (repo add) and remembered in dstow's own registry — you never hand-edit that file. A repo is either managed (a remote dstow cloned for you) or a local path (registered in place and never modified by dstow).
  • A package is a top-level directory in a repo. By default packages live at the repo root; a repo may instead collect them under a subdirectory (see packages_dir).
  • The target is where links go — $HOME unless configured otherwise.
  • dstow records every link it makes in a ledger, so status, check, and clean can reason about what should be there versus what is.

Each package succeeds or fails on its own: a bulk run continues past failures and exits non-zero if any package failed.


Naming

Every repo and package has a fully-qualified name:

github:rocne/dotfiles          # a repo
github:rocne/dotfiles::zsh     # a package in it
local:/home/you/dots::vim      # a package in a local repo
  • Refer to anything by any unambiguous suffix of its FQN. zsh, dotfiles::zsh, and the full form all name the same package — as long as the suffix is unique. When it is not, dstow refuses and lists the qualified spellings to pick from.
  • :: forces the package/repo boundary. dots::zsh means "the package zsh in a repo whose name ends dots", never a repo called dots::zsh.
  • A scheme prefix requires the full coordinate (github:rocne/dotfiles, not github:dotfiles).
  • The working directory is irrelevant. dstow never guesses from where you are standing.

Paths and names never collide: an operand that looks like a path (~/.zshrc, ./x, /etc/...) is treated as a path; everything else is a name expression.


Commands

Run dstow <command> --help for the full help and examples of any command.

Deploy
Command What it does
dstow stow <name>… | --all Link packages into their targets
dstow unstow <name>… | --all Remove packages' links
dstow restow <name>… | --all Unstow then stow — the idempotent refresh
dstow adopt <file> [<package>] Import an existing file into a package, leaving a link behind
  • Names are packages or repos; naming a repo acts on all of its packages.
  • With no names, dstow asks before acting on everything. In a script that is an error — pass --all.
  • Explicitly naming a package overrides its exclude_from_bulk setting.
  • restow on a not-stowed package simply stows it (the unstow phase no-ops).

Flags: --all (every package of every repo, no prompt) · --adopt (on stow/restow: adopt a real file found at an expected path instead of refusing) · -n/--dry-run (show the plan, change nothing).

adopt — live content always wins; adopt never destroys running configuration:

dstow adopt ~/.zshrc zsh                 # import one file into the zsh package
dstow adopt ~/.config/foo/foo.toml       # no package named: pick from ranked candidates
dstow adopt --occupied zsh               # adopt every occupied path of a package

adopt flags: --occupied · -n/--dry-run · --force (overwrite differing package content without asking).

Inspect
dstow list [<name>]        # configured content: repos ⊃ packages ⊃ paths
dstow info [<name>]        # every field dstow holds about one scope
dstow status [<name>…]     # live deployment state
dstow status <path>        # what occupies a path, who owns it, adoption candidates
  • list enumerates a scope's content and never inspects disk. Flags: --repos, --packages, --json.
  • info reads one scope's fields — inherent facts (version, paths, source, scheme, qualified name) and effective config (target, dot-translation, fold, ignores) — from configuration and metadata, never by touching targets. Flags: -f/--field <field> (repeatable), -r/--recurse, --json.
  • status inspects reality. Package states: stowed, partially stowed, not stowed, occupied, damaged, plus a drifted marker when the deployed shape differs from what current config would produce. Remote repos also show behind/ahead as of the last repo update. Flag: --json.
Maintain
dstow check      # verify every ledgered link; classify broken and orphaned
dstow clean      # execute exactly what check reported
dstow rebuild    # reconstruct a lost ledger by walking configured targets (rare)
  • check is instant (no tree walk) and read-only. It classifies stale links as broken (destination gone) or orphaned (resolves into a known repo, but no current config would produce it), and reports contradicted ledger entries (disk disagrees with the record). Flag: --json.
  • clean executes exactly check's report — the two can never disagree. Broken links are removed freely; orphans are confirmed (or --yes / --force to remove without asking). Contradicted entries are pruned, disk untouched.
  • rebuild is the only full tree walk dstow has — explicit and rare, for when the ledger is lost.
Groups
dstow repo add <source> [--stow]     # register a repo (path, URL, github:owner/name)
dstow repo remove <repo> [--unstow] [--force]
dstow repo update [<repo>…]          # download remote changes; touch nothing on disk
dstow repo upgrade [<repo>…]         # fast-forward clean clones to what update fetched

dstow snippet rc                     # print the shell-rc bootstrap snippet
dstow colors theme <name> [--format env|toml]   # emit a theme
  • repo update and repo upgrade are two explicit phases; neither runs on its own. update touches the network and nothing else — afterwards status shows behind/ahead. upgrade is fast-forward only: divergence or local work refuses loudly, with no stash/merge/rebase, and never re-stows (structural drift shows up in status). Update, review, then upgrade.
  • repo remove deletes managed clones but only forgets local-path repos (your directory is never touched). It refuses while the repo still has stowed links (offering --unstow) and refuses to delete a managed clone holding work not present at its source (--force overrides both).
Also
dstow completion <shell>    # bash | zsh | fish | powershell
dstow version               # print version
Global flags

Available on every command:

Flag Meaning
--color <when> auto (default), always, or never — the value is required
-q, --quiet Suppress routine chatter; announcements, warnings, and errors always survive
-y, --yes Assume "yes" at confirmations of stated intent (never resolves ambiguity, answers a bulk prompt, or bypasses a guard)
-h, --help Help for dstow or any command

-n/--dry-run is available on stow/unstow/restow/adopt. --json is available on list/info/status/check.


Configuration

dstow reads TOML config from four levels; nearer levels win, and ignore chains are additive (a level adds to, never silences, inherited ignores). You write every config file yourself except the repo registry, which dstow owns.

File Location Written by
Global config $XDG_CONFIG_HOME/dstow/config.toml you
Repo config <repo>/.dstow/config.toml you
Package config <repo>/<pkg>/.dstow/config.toml you
Repo registry $XDG_CONFIG_HOME/dstow/repos.toml dstow — never hand-edit
User theme presets $XDG_CONFIG_HOME/dstow/themes/<name>.toml you

Keys are snake_case. CLI flags use kebab-case — the one deliberate spelling mismatch. There is no config-mutation command: you declare configuration by editing the file, like every other knob.

Key Type / default package repo global
target path — default $HOME
translate_dot_prefixes bool — default true
ignore list of patterns (additive)
exclude_from_bulk bool — default false
packages_dir repo-relative path
fold_trees bool — default false
[color] table + theme see Theming
  • target — where this scope's links go. Path values expand ~ and $VAR/${VAR} (evaluated per invocation); the result must be absolute. An unset variable is a loud error naming the variable, file, and key.
  • translate_dot_prefixes — translate a leading dot- in filenames to . (the stow --dotfiles convention). On by default.
  • exclude_from_bulk — keep a package out of --all and repo-wide runs; naming it explicitly still acts on it.
  • packages_dir — opt-in, repo level only: a repo-root-relative directory where the repo's packages live. Unset means packages sit at the repo root. "packages" is the recommended convention for fresh repos.
  • fold_trees — GNU Stow's "tree folding". Off by default.
Ignores

The ignore key carries gitignore-glob patterns, matched per package against package-root-relative paths (no slash = basename at any depth; a leading slash anchors; a trailing slash is directory-only; ** is supported):

ignore = ["*.log", "/build/", "**/__pycache__"]

Leading ! (negation) and leading // are refused and reserved. dstow always ignores a package's own root .dstow/ directory — that is not a config knob.

Unknown or misplaced keys

dstow warns, never refuses: an unknown key gets a did-you-mean; a key that is legal at a different level names that level. These warnings survive --quiet.


Migrating from GNU Stow

dstow reads your existing stow configuration — you do not have to rewrite it to start.

  • .stow-local-ignore and .stowrc are honored as compat files, parsed quirk-faithfully. A native config.toml whose content is flag-lines (the first token starts with -) is routed to the compat parser with a loud announcement naming the native equivalent.
  • ~/.stowrc's --dir contributes a session repo for the current shell, announced, with a fix: suggesting a permanent repo add. A repo-level --dir is warned and ignored.
  • Option mapping: --targettarget; --no-foldingfold_trees = false; --dotfilestranslate_dot_prefixes = true; --ignore → an additive ignore entry (in stow-regex, its native language). Options dstow does not map (--adopt, --override, --defer, verbosity, simulate) are warned-and-ignored per option, naming why and the native remedy. The file always runs; degradation is loud, never a rejection.
  • Supplement mode: when both a stow rc and native config set the same knob, non-overlapping and equal values are silent; a genuine conflict is resolved native-wins with a loud warning naming the level, files, knob, values, and winner, plus a fix: suggesting removal from the rc. ignore chains never conflict — they are additive and per-language.

A migrated stow repo works out of the box: package identity is locational, there are no marker files to add, and mkdir-and-go stays intact.


Theming

dstow colorizes semantic states — stowed is green, damaged bold red, drifted cyan, and so on — and every commentary line carries a greppable word prefix (note:, warning:, error:, fix:) so output stays meaningful with color off. Defaults use only the 16 base ANSI colors, so your terminal theme re-themes dstow automatically, and colorblind/low-vision users retheme through terminal preferences.

Color is enabled by a fixed precedence — --color > NO_COLOR > CLICOLOR_FORCE > CLICOLOR > TTY detection — and theme choices are strictly downstream: a theme can never re-enable color the chain turned off.

Themes layer, top wins:

  1. DSTOW_COLORS — the one theming environment variable; packed per-slot overrides in an LS_COLORS-family syntax, values in git's color.* grammar:

    export DSTOW_COLORS='damaged=bold red:stowed=#a6e3a1'
    # or generate a whole theme:
    export DSTOW_COLORS=$(dstow colors theme catppuccin-mocha)
    
  2. The [color] table in global config — one key per slot, same grammar:

    [color]
    stowed = "#a6e3a1"
    damaged = "bold red"
    
  3. The theme config key — a bare string is a theme name (your themes dir first, then the bundled presets); a path form is a theme file anywhere, including inside a repo (so a repo can ship its own theme):

    theme = "catppuccin-mocha"
    # theme = "~/themes/mine.toml"
    
  4. The default ANSI-16 palette.

Emit a theme for your session or to a file — the packed string, the config table, and theme files share one slot vocabulary and one value grammar, so they are losslessly convertible:

dstow colors theme catppuccin-mocha                       # packed DSTOW_COLORS string
dstow colors theme catppuccin-mocha --format toml \
  > ~/.config/dstow/themes/mine.toml                      # a theme file

The color slots are: stowed partially_stowed not_stowed occupied damaged drifted (states); broken orphaned contradicted (check classes); note warning error fix (severities); name heading muted (prose). A theme file is exactly the bare [color] schema — no wrapper keys.


Hooks

Run your own executables around deploy actions. Hooks live in a hooks/ directory inside .dstow/, at any of the three levels:

<repo>/<pkg>/.dstow/hooks/      # package-level
<repo>/.dstow/hooks/            # repo-level
$XDG_CONFIG_HOME/dstow/hooks/   # global

Eight git-style per-event executables, one file per event:

pre-stow    post-stow
pre-unstow  post-unstow
pre-restow  post-restow
pre-adopt   post-adopt
  • Each hook is exec'd directly — the shebang chooses the interpreter — and must be executable (a correctly-named but non-executable file is warned with a chmod +x hint; a misspelled name gets a did-you-mean).
  • restow fires only the restow pair (not stow/unstow).
  • The working directory is the scope's own directory (package dir, repo dir, or the global config dir).
  • Subdirectories of hooks/ are inert helper space — never fired, never warned; lib/ is the documented convention for shared scripts.
  • <event>.d/ directories are reserved (inert-and-warned in v1; run-parts drop-ins are committed for v2).
Hook context

dstow passes context as DSTOW_HOOK_* environment variables (no arguments, no stdin). A variable is absent, never empty, when it does not apply:

Variable Value pkg repo global
DSTOW_HOOK_LEVEL package / repo / global
DSTOW_HOOK_ACTION stow / unstow / restow / adopt
DSTOW_HOOK_PHASE pre / post
DSTOW_HOOK_FQN this scope's FQN (canonical-encoded)
DSTOW_HOOK_SCHEME e.g. github (decoded)
DSTOW_HOOK_COORDINATE e.g. rocne/dotfiles (decoded)
DSTOW_HOOK_PACKAGE bare package name (decoded)
DSTOW_HOOK_PACKAGE_DIR absolute path
DSTOW_HOOK_TARGET effective target root (absolute)
DSTOW_HOOK_REPO_FQN the repo's FQN (canonical-encoded)
DSTOW_HOOK_REPO_DIR absolute path
DSTOW_HOOK_PACKAGES FQNs acting under this scope, newline-separated

("Coordinate" is used in the Maven sense — the parts that locate a repo.) FQN values are percent-encoded so they paste straight back into dstow commands; decomposed segments carry decoded real values. Iterate DSTOW_HOOK_PACKAGES with while IFS= read -r pkg; do …; done.

  • Both hook output streams go to dstow's stderr; stdin passes through. A hook's output is commentary, never dstow's answer — a hook that must emit data writes a file.
  • Write commands refuse from inside a hook; reads are fully allowed. A hook may run dstow status --json or dstow info -f target to read state, but deploy verbs, adopt, clean, rebuild, and repo mutations refuse (detected via DSTOW_HOOK_ACTION in the environment).

Reference

Environment variables
Variable Effect
DSTOW_PATH Colon-separated absolute local directory paths, registered as session repos for this shell only (PATH convention; no priority, no qualified sources, no ~/$VAR expansion — relative entries are refused loudly)
DSTOW_COLORS Packed per-slot theme overrides (see Theming)
NO_COLOR Disables color (standard)
CLICOLOR / CLICOLOR_FORCE Standard color enable/force signals, below --color in precedence
XDG_CONFIG_HOME Config + registry + themes root (…/dstow/); defaults to ~/.config
XDG_STATE_HOME Ledger location (…/dstow/); defaults to ~/.local/state
XDG_DATA_HOME Managed clones (…/dstow/repos/…); defaults to ~/.local/share
DSTOW_HOOK_* dstow's output lane to hooks — set by dstow, read by your hooks (see Hooks)

Managed clones live at $XDG_DATA_HOME/dstow/repos/<scheme>/<owner>/<name> (percent-encoded, so links point into a filesystem-safe, canonical location).

Exit codes
Code Meaning
0 Success
1 Negative answer — a package failed, a requested field is unset/empty, or check found findings
2 Usage error — a bad flag, wrong argument count, or unknown command
3 Refusal / environment — non-interactive ambiguity, a corrupt or newer-than-known ledger, or lock contention
The name group

A hidden utility group for scripting the naming grammar directly. It operates on one coordinate segment, percent-encoding the characters the grammar reserves (:, @, …) and leaving ordinary ones alone:

dstow name encode 'weird:name'    # -> weird%3Aname
dstow name decode 'weird%3Aname'  # -> weird:name
Files at a glance
Path What
$XDG_CONFIG_HOME/dstow/config.toml your global config
$XDG_CONFIG_HOME/dstow/repos.toml the dstow-owned repo registry
$XDG_CONFIG_HOME/dstow/themes/ your theme presets
$XDG_CONFIG_HOME/dstow/hooks/ global hooks
$XDG_STATE_HOME/dstow/ the ledger
$XDG_DATA_HOME/dstow/repos/ managed clones
<repo>/.dstow/ repo config + hooks
<repo>/<pkg>/.dstow/ package config + hooks

Everything above is the v1 surface. dstow <command> --help is the authoritative, always-current reference for any command's flags and examples.

License

dstow is licensed under the GNU General Public License v3.0. Copyright (c) 2026 Rocne Scribner.

Documentation

Overview

Package dstow carries repo-root artifacts that must ride inside the binary. The go:embed directive cannot reach above a package's directory, so files vendored at the repo root (snippet.sh, owned by release-ci per D26) are embedded here and consumed by the internal packages.

Index

Constants

This section is empty.

Variables

View Source
var RCSnippet string

RCSnippet is the vendored snippet.sh, verbatim — the canonical rc bootstrap (DESIGN §9.1 B1 as amended: authored in release-ci, vendored beside install.sh, embedded so `dstow snippet rc` emits the one canonical file with zero transcription drift (B2)).

Functions

This section is empty.

Types

This section is empty.

Directories

Path Synopsis
cmd
dstow command
Command dstow is a dotfiles manager built on the gostow engine.
Command dstow is a dotfiles manager built on the gostow engine.
internal
cli
Package cli is dstow's command-line front end and composition root (A1/A2): the only consumer that turns the app core's data into a cobra CLI, renders results through the ui printer, and maps typed domain errors to exit codes.
Package cli is dstow's command-line front end and composition root (A1/A2): the only consumer that turns the app core's data into a cobra CLI, renders results through the ui printer, and maps typed domain errors to exit codes.
config
Package config is dstow's four-level configuration chain (DESIGN.md §3 + A8): the legality matrix over the one key vocabulary, use-time path expansion, warnings-as-data for unknown and misplaced keys, stow compatibility (stowrc discovery, slotting, option mapping, and supplement diffing via gostow's public stowrc package), content-sniff routing for a renamed rc, DSTOW_PATH parsing, and the metadata-location accessor.
Package config is dstow's four-level configuration chain (DESIGN.md §3 + A8): the legality matrix over the one key vocabulary, use-time path expansion, warnings-as-data for unknown and misplaced keys, stow compatibility (stowrc discovery, slotting, option mapping, and supplement diffing via gostow's public stowrc package), content-sniff routing for a renamed rc, DSTOW_PATH parsing, and the metadata-location accessor.
engine
Package engine is dstow's one seam onto gostow (DESIGN.md A14–A16): the deployment verbs as per-package operations, and the two introspections (Expected, Owner) the ledger and maintenance verbs compose with.
Package engine is dstow's one seam onto gostow (DESIGN.md A14–A16): the deployment verbs as per-package operations, and the two introspections (Expected, Owner) the ledger and maintenance verbs compose with.
git
Package git is dstow's version-control seam: system git behind a port (DESIGN.md A17).
Package git is dstow's version-control seam: system git behind a port (DESIGN.md A17).
hooks
Package hooks is dstow's hook engine (DESIGN.md §5 + A11): discovery of the eight per-event executables in one hooks directory, the DSTOW_HOOK_* environment contract each hook runs under, direct exec of a hook, and the per-invocation sequencer that fires the nested/LIFO lifecycle (REQUIREMENTS §9.1) once per scope.
Package hooks is dstow's hook engine (DESIGN.md §5 + A11): discovery of the eight per-event executables in one hooks directory, the DSTOW_HOOK_* environment contract each hook runs under, direct exec of a hook, and the per-invocation sequencer that fires the nested/LIFO lifecycle (REQUIREMENTS §9.1) once per scope.
ignore
Package ignore is dstow's native ignore matcher (DESIGN.md A15 + §3.4): the additive ignore chain's gitignore-glob entries, compiled once and matched per package against package-root-relative paths.
Package ignore is dstow's native ignore matcher (DESIGN.md A15 + §3.4): the additive ignore chain's gitignore-glob entries, compiled once and matched per package against package-root-relative paths.
ledger
Package ledger is dstow's current-state index of the symlinks it believes exist (DESIGN.md §6 + A10; ADR 0001).
Package ledger is dstow's current-state index of the symlinks it believes exist (DESIGN.md §6 + A10; ADR 0001).
name
Package name is dstow's pure naming grammar: parse and format fully qualified names (FQNs), percent-encode and -decode coordinate segments, resolve segment-boundary suffix matches, force package-kind with a leading "::", and classify an operand as a path or a name expression.
Package name is dstow's pure naming grammar: parse and format fully qualified names (FQNs), percent-encode and -decode coordinate segments, resolve segment-boundary suffix matches, force package-kind with a leading "::", and classify an operand as a path or a name expression.
ops
Package ops is dstow's application core (A13): the verbs as deep modules composing config, repo, engine, ledger, and hooks into structured results.
Package ops is dstow's application core (A13): the verbs as deep modules composing config, repo, engine, ledger, and hooks into structured results.
repo
Package repo is dstow's repo set (DESIGN.md A9): the repo registry (read/write with the same temp-file+fsync+rename discipline as the ledger), the source grammar and its internal github/local schemes, the managed-clone directory layout (A19), package enumeration (M2/M3), and name resolution over the set through the pure name package.
Package repo is dstow's repo set (DESIGN.md A9): the repo registry (read/write with the same temp-file+fsync+rename discipline as the ledger), the source grammar and its internal github/local schemes, the managed-clone directory layout (A19), package enumeration (M2/M3), and name resolution over the set through the pure name package.
ui
Package ui is dstow's sole owner of the terminal streams (A4): every other module returns data — diagnostics included — and ui alone renders it.
Package ui is dstow's sole owner of the terminal streams (A4): every other module returns data — diagnostics included — and ui alone renders it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL