Skip to content

feat(packages): flat registry with smart defaults#49

Merged
paulnsorensen merged 4 commits intomainfrom
paulnsorensen/flat-pkg-spec
Mar 11, 2026
Merged

feat(packages): flat registry with smart defaults#49
paulnsorensen merged 4 commits intomainfrom
paulnsorensen/flat-pkg-spec

Conversation

@paulnsorensen
Copy link
Owner

Unified packages.yaml replaces nested .brew/.apt files and brew/apt/packages.yaml dirs. One flat list, smart defaults: bare strings = brew, maps override (source, dev, git). Sources: brew, cask, tap, cargo, apt.

packages/sync.sh groups by source at runtime. SHA-256 cache skips unchanged syncs. To add a package: bare string for brew, map for anything else. dots sync auto-installs; dots sync refresh bypasses cache.

All 15 bats tests pass. No YAGNI abstractions — just streamlined package management.

🧀

Copilot AI review requested due to automatic review settings March 10, 2026 09:57
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the nested .brew/apt/packages.yaml/brew/packages.yaml system with a single flat packages.yaml at the repo root, backed by a new packages/sync.sh script. The sync script groups entries by source at runtime, uses a SHA-256 hash cache to skip unchanged syncs, and supports a refresh flag to bypass the cache. CLAUDE.md and test files are updated accordingly.

Changes:

  • New packages.yaml flat registry (bare strings = brew; maps for cask/tap/cargo/apt/dev)
  • New packages/sync.sh with hash cache; replaces .brew and .apt
  • .sync-with-rollback, bin/dots, CLAUDE.md, and tests updated to integrate the new system; old brew/packages.yaml and apt/packages.yaml removed

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages.yaml New flat package registry replacing brew/packages.yaml + apt/packages.yaml
packages/sync.sh New unified sync script with SHA-256 hash cache
.sync-with-rollback Replaces .brew/.apt invocations with packages/sync.sh; adds refresh arg
bin/dots Documents dots s refresh in help text
CLAUDE.md Updated architecture docs, skip list, and "When Adding New Packages" guide
brew/packages.yaml Removed (superseded by root packages.yaml)
apt/packages.yaml Removed (superseded by root packages.yaml)
.brew Removed (superseded by packages/sync.sh)
.apt Removed (superseded by packages/sync.sh)
tests/config-validation.bats Test updated to validate new packages.yaml location and schema

for file in *; do
# Skip state management files
if [[ "${file}" == ".git" || "${file}" == ".local" || "${file}" == "reference" || "${file}" == ".worktrees" || "${file}" == "apt" ]]; then
if [[ "${file}" == ".git" || "${file}" == ".local" || "${file}" == "reference" || "${file}" == ".worktrees" || "${file}" == "packages" || "${file}" == "brew" || "${file}" == "apt" ]]; then
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The packages.yaml root-level file is not excluded from the symlink loop in .sync-with-rollback. The loop iterates over all files in the dotfiles directory and symlinks them as ~/.<filename>, so packages.yaml will be symlinked as ~/.packages.yaml. Since this file is a package registry (not a dotfile to be installed in home), it should be added to the skip list alongside packages, brew, and apt.

To fix this, add "packages.yaml" to the skip condition on line 282 of .sync-with-rollback. The CLAUDE.md skip list documentation on line 197 should also be updated accordingly.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed in 2708b93 — added packages.yaml to the skip list in .sync-with-rollback and updated the CLAUDE.md skip list docs.

packages/sync.sh Outdated
echo " + $name"
elif [[ -n "$git_url" ]]; then
echo " Installing $name from $git_url..."
cargo install --git "$git_url"
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cargo install --git "$git_url" (line 207) does not pass the crate name ($name) to the command. While cargo install --git <url> installs all installable binary crates from the repo, the $name variable is used to check whether the package is already installed (line 203). If the installed binary crate name in Cargo.toml ever differs from the name field in packages.yaml, the check would always fail and attempt a reinstall on every sync (or more accurately, with --force semantics). More importantly, for repos with multiple installable crates, omitting the crate name would install ALL of them.

The correct invocation is cargo install --git "$git_url" "$name", which both specifies the crate to install and keeps it consistent with the name-based installed check.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in 2708b93. Now passes "$name" to cargo install --git to target the specific crate.

Comment on lines +83 to +183
sync_brew() {
if ! command -v brew &>/dev/null; then
log_info "Installing Homebrew..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi

log_info "Syncing brew packages"

# Taps first (other packages may depend on them)
local taps
taps=$(get_names "tap")
if [[ -n "$taps" ]]; then
echo -e "\n${GREEN}Taps:${NC}"
local tapped
tapped=$(brew tap)
while IFS= read -r tap; do
[[ -z "$tap" ]] && continue
if echo "$tapped" | grep -qx "$tap"; then
echo " + $tap"
else
echo " Installing tap $tap..."
brew tap "$tap"
fi
done <<< "$taps"
fi

# Core formulae
local formulae
formulae=$(get_names "brew")
if [[ -n "$formulae" ]]; then
echo -e "\n${GREEN}Formulae:${NC}"
local installed
installed=$(brew list --formulae 2>/dev/null || true)
while IFS= read -r pkg; do
[[ -z "$pkg" ]] && continue
if echo "$installed" | grep -qx "$pkg"; then
echo " + $pkg"
else
echo " Installing $pkg..."
brew install "$pkg"
fi
done <<< "$formulae"
fi

# Core casks
local casks
casks=$(get_names "cask")
if [[ -n "$casks" ]]; then
echo -e "\n${GREEN}Casks:${NC}"
local installed_casks
installed_casks=$(brew list --cask 2>/dev/null || true)
while IFS= read -r pkg; do
[[ -z "$pkg" ]] && continue
if echo "$installed_casks" | grep -qx "$pkg"; then
echo " + $pkg"
else
echo " Installing $pkg..."
brew install --cask "$pkg"
fi
done <<< "$casks"
fi

# Dev packages
if [[ "${DOTFILES_DEV:-false}" == "true" ]]; then
local dev_formulae
dev_formulae=$(get_names "brew" "--dev")
if [[ -n "$dev_formulae" ]]; then
echo -e "\n${GREEN}Dev formulae:${NC}"
local installed
installed=$(brew list --formulae 2>/dev/null || true)
while IFS= read -r pkg; do
[[ -z "$pkg" ]] && continue
if echo "$installed" | grep -qx "$pkg"; then
echo " + $pkg"
else
echo " Installing $pkg..."
brew install "$pkg"
fi
done <<< "$dev_formulae"
fi

local dev_casks
dev_casks=$(get_names "cask" "--dev")
if [[ -n "$dev_casks" ]]; then
echo -e "\n${GREEN}Dev casks:${NC}"
local installed_casks
installed_casks=$(brew list --cask 2>/dev/null || true)
while IFS= read -r pkg; do
[[ -z "$pkg" ]] && continue
if echo "$installed_casks" | grep -qx "$pkg"; then
echo " + $pkg"
else
echo " Installing $pkg..."
brew install --cask "$pkg"
fi
done <<< "$dev_casks"
fi
fi

log_success "Brew sync complete"
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sync_brew function spans approximately 100 lines (lines 83–183), which exceeds the project's 40-line function complexity budget defined in the coding guidelines. The function handles four distinct concerns (taps, formulae, casks, dev packages). Each of these could be extracted into a helper function (e.g., install_brew_pkgs <pkg_list> [--cask]) to bring the function under the limit and eliminate the duplicated install-loop pattern that appears four times.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — extracted brew_install_pkgs helper in 2708b93. sync_brew is now ~30 lines, and the duplicated install loop is gone. Also batch-fetches brew list --formulae and brew list --cask once upfront instead of per-section.

Replace nested .brew/.apt files and brew/apt/packages.yaml directories with a single flat packages.yaml registry. Each entry is a bare string (brew formula) or a map with overrides (source, dev, git). Defaults: source=brew, dev=false. Sources: brew, cask, tap, cargo, apt.

packages/sync.sh groups entries by source at runtime, using SHA-256 cache to skip unchanged syncs. Tests pass. Syncing now just: `dots sync`.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@paulnsorensen paulnsorensen force-pushed the paulnsorensen/flat-pkg-spec branch from 899b78c to e675e0e Compare March 11, 2026 06:43
paulnsorensen and others added 3 commits March 10, 2026 23:58
- Add packages.yaml to symlink skip list (prevents ~/.packages.yaml)
- Pass crate name to cargo install --git (prevents multi-crate issues)
- Extract brew_install_pkgs helper (sync_brew under 40-line budget)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bare strings now install via platform package manager (brew on mac,
  apt on linux) instead of always meaning brew
- apt: field overrides package name for linux (e.g. fd -> fd-find)
- platform: field restricts to mac or linux
- Flow-style maps for one-liner overrides (dev, apt name, platform)
- Eliminates separate source: apt entries entirely
- get_platform_pkgs replaces get_names for cross-platform queries
- get_source_pkgs handles explicit sources (tap, cask)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The package name is now the map key, eliminating the redundant name:
field. `- fd: { apt: fd-find }` instead of `- { name: fd, apt: fd-find }`.
Queries use to_entries[0] to extract key (name) and value (overrides).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@paulnsorensen paulnsorensen merged commit b56b728 into main Mar 11, 2026
2 checks passed
@paulnsorensen paulnsorensen deleted the paulnsorensen/flat-pkg-spec branch March 11, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants