Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

90 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dot

dot is a conservative, declarative bootstrap runner for personal development environments and dotfiles across Linux, macOS, and Windows.

It reads a TOML manifest, selects one target and optional profile, and coordinates external package providers, manual installation actions, generic actions, and symbolic links. The manifest remains a readable inventory of the environment it describes.

yslib/dotfiles is the complete application example used to develop dot. It describes an Arch Linux base/desktop/laptop profile tree plus independent macOS and Windows environments.

dot is intentionally not a package manager or a general-purpose configuration DSL. It does not search repositories, solve dependencies, compare versions, implement installers, or keep an installed-state database. Those responsibilities remain with declared commands such as pacman, brew, scoop, npm, and cargo.

Installation

Prebuilt binaries are published on the GitHub Releases page for:

  • Linux x86-64, statically linked with musl;
  • macOS Apple Silicon;
  • Windows x86-64.

Rename the downloaded asset to dot (dot.exe on Windows), make it executable where necessary, and place it on PATH.

To build from source with stable Rust:

git clone https://github.com/yslib/dot.git
cd dot
cargo build --release

The binary is written to target/release/dot (dot.exe on Windows).

Quick start

Create dot.toml in the directory containing the files you want to manage:

[targets.workstation]
platform = { os = "linux", distro = ["debian", "ubuntu"] }

[targets.workstation.providers.apt]
probe   = { program = "apt-get", args = ["--version"] }
install = { program = "sudo", args = ["apt-get", "install", "-y", "${package:names}"] }

[targets.workstation.packages]
git     = { provider = "apt" }
ripgrep = { provider = "apt" }

[targets.workstation.links]
nvim = { source = "config/nvim", target = "${xdg:config}/nvim" }

Inspect the complete selected intent without executing it:

dot dry-run --target workstation

Check whether the effective providers are currently available:

dot check providers --target workstation

Apply the environment:

dot apply --target workstation

The default configuration path is ./dot.toml; use --config PATH to select another file. --target may be omitted when exactly one configured target is compatible with the current platform.

Command line

The command is always explicit:

dot [--config PATH] apply
    [--target TARGET] [--profile PROFILE] [--job KIND:ID]...

dot [--config PATH] dry-run
    [--target TARGET] [--profile PROFILE] [--job KIND:ID]...

dot [--config PATH] check providers
    [--target TARGET] [--profile PROFILE]

dot [--config PATH] list targets [--all]

dot [--config PATH] list profiles
    [--target TARGET]

dot [--config PATH] list jobs
    [--target TARGET] [--profile PROFILE]

The optional profile is one globally unique profile node name inside the target. Omitting it selects the target root; --profile @root is the explicit equivalent. If the target is omitted, selection succeeds only when exactly one target is compatible. Execution commands and provider check reject an explicit incompatible target. list profiles and list jobs may inspect an explicitly named incompatible target structurally.

Exact job selection

Omitting --job selects all effective jobs. Repeat --job to select exact package, action, and link jobs:

dot dry-run --target workstation \
  --job package:ripgrep \
  --job link:nvim

dot apply --target workstation \
  --job package:ripgrep \
  --job link:nvim

The accepted forms are exactly:

package:ID
action:ID
link:ID

A provider-backed package automatically includes its required provider. Providers cannot be selected directly, and a Batch package remains one indivisible job. Selector argument order does not control execution order.

Machine-readable lists

The list commands provide stable catalogs for shell scripts, completion engines, fzf, and other external tools. Output is UTF-8, headerless TSV with one record per line and fixed columns:

list targets:   TARGET  COMPATIBILITY  OS  ARCH  DISTRO  DISTRO_FAMILY  ENVIRONMENT
list profiles:  PROFILE PATH           DEPTH
list jobs:      SELECTOR KIND          ID  VIA  DETAIL

list targets includes only compatible targets by default; --all includes every target and labels compatibility. list profiles starts with @root<TAB><root><TAB>0. list jobs describes the unresolved effective package, action, and link records and never emits provider rows.

The first field is the canonical reusable selector and is written verbatim. In later fields, backslash, tab, carriage return, and newline are escaped as \\, \t, \r, and \n. All records are prepared before stdout is written, so configuration or selection errors leave stdout empty. A downstream broken pipe is normal successful termination.

See the Design Model for the stable meaning of every TSV column.

External fzf composition

This Bash example lets fzf select one or more complete TSV rows, extracts each canonical first field, and passes it as a separately quoted --job argument:

#!/usr/bin/env bash
set -euo pipefail

target=workstation
profile=@root
command=${1:-dry-run}

case "$command" in
  apply|dry-run) ;;
  *) printf 'usage: %s [apply|dry-run]\n' "$0" >&2; exit 2 ;;
esac

selection_file=$(mktemp)
cleanup() {
  rm -f "$selection_file"
}
trap cleanup EXIT

if ! dot list jobs --target "$target" --profile "$profile" |
  fzf --multi > "$selection_file"; then
  printf 'job selection failed\n' >&2
  exit 1
fi

selectors=()
while IFS=$'\t' read -r selector _; do
  selectors+=("$selector")
done < "$selection_file"

((${#selectors[@]})) || {
  printf 'no jobs selected\n' >&2
  exit 1
}

job_args=()
for selector in "${selectors[@]}"; do
  job_args+=(--job "$selector")
done

dot "$command" \
  --target "$target" \
  --profile "$profile" \
  "${job_args[@]}"

Run the wrapper with dry-run to inspect the chosen plan or apply to execute it. dot never invokes, configures, or depends on fzf; it does not read selection from stdin. The wrapper turns fzf output into ordinary command-line selectors before starting apply or dry-run, leaving their stdin available to interactive child commands.

Configuration

Configuration is intentionally finite and explicit. A target is a complete environment declaration. A selected nested profile inherits the target and only the profile nodes along the path to that node. Deeper keyed records replace complete earlier records; record fields and lists do not merge.

A provider declares how to probe and install, with optional activation and ensure actions. Every provider-backed package is one declared install unit: a Single uses its table key as the package name, while a Batch supplies an explicit non-empty names list. dot does not infer grouping.

[targets.workstation]
platform = { os = "macos" }

[targets.workstation.providers.brew]
probe   = { program = "brew", args = ["--version"] }
install = { program = "brew", args = ["install", "${package:names}"] }

[targets.workstation.packages.ripgrep]
provider = "brew"

[targets.workstation.packages.cli-tools]
provider = "brew"
names = ["bat", "fd"]

A manual package contains its own install action. Generic actions describe other idempotent work, and links map existing sources to native symlink targets. The complete configuration is statically validated at load, while environment and path values are resolved only for the selected execution closure.

See the Configuration Reference for the complete types, fields, examples, and interpolation rules.

Execution and diagnostics

Apply and dry-run build the same selected, resolved execution plan. Dry-run renders that plan without running provider, package, or action commands and without inspecting or changing link state. Its output describes intent, not whether the current machine can satisfy it.

Apply executes serially in stable provider, provider-backed package, manual package, action, and link phases. A provider failure blocks only selected packages that require it; unrelated selected work continues. Planning is atomic, and the final report exits non-zero if any selected item failed or was blocked.

check providers is separate from job selection and independently attempts every effective provider. Activation resolution/application or probe resolution/preparation can produce NOT_READY before a process is launched. When preparation succeeds, the probe process executes at most once. Any provider-local failure does not stop later providers from being attempted. The command does not run ensure or install, process packages or actions, or inspect links. Because a launched probe is an arbitrary external command, provider check is diagnostic, not a side-effect-free simulation.

Apply and dry-run reports are human-readable tables, not stable serialized interfaces. The list-command TSV contract is the stable machine-facing interface. Version 0.0.1 does not provide --json.

Goals

  • Make a personal development environment reproducible without hiding the commands that establish it.
  • Keep each target locally complete, even when independent targets repeat data.
  • Model only the small amount of domain knowledge needed for cross-platform bootstrap work.
  • Keep the manifest readable as an explicit inventory.
  • Let procedural edge cases remain ordinary shell or PowerShell scripts.

Non-goals

dot deliberately omits:

  • repository search, dependency solving, versions, updates, and uninstall;
  • package or link receipts and other persistent managed-state databases;
  • per-item condition expressions and arbitrary evaluation;
  • cross-target inheritance, profile references, and multiple inheritance;
  • action dependency graphs and provider dependency resolution;
  • built-in download, archive, checksum, build, or service-management logic;
  • implicit shell execution;
  • link removal, garbage collection, copies, and fallback link strategies.

If installation logic is inherently procedural, invoke a script. If a feature would require dot to understand how a particular package manager or arbitrary program works, it probably belongs outside dot.

Development platform override

The development-only dev-platform-override Cargo feature adds the global --platform <TOML> option for compatibility testing:

cargo run --features dev-platform-override -- \
  --platform '{ os = "windows", arch = "x86_64" }' \
  dry-run

The injected PlatformInfo controls compatibility for dry-run, provider check, and target listings, and omitted-target inference for profile/job listings. Commands, environment variables, XDG paths, and filesystem state still belong to the host. Apply accepts but ignores the override and always uses detected host facts. dot prints a warning whenever this option is used. Without the feature, --platform is not part of the CLI.

Further reading

  • Configuration Reference — user-facing types, fields, examples, and interpolation rules.
  • Configuration schema — authoritative structural schema and string roles.
  • Design — runtime and design semantics, execution boundaries, and explicit design decisions.
  • Unified job execution — selected-plan architecture, serial order, and failure behavior.
  • yslib/dotfiles — complete real-world configuration for Linux, macOS, and Windows.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages