Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

## v1.0 (unreleased)
## v1.0.0

OpenBoot v1.0 narrows the core workflow to two verbs: **`install`** (add things to your Mac) and **`snapshot`** (save your current state somewhere). The supporting **`doctor`** and **`update`** maintenance commands remain available; everything else is either cloud-config CRUD or independent tooling.

Expand Down Expand Up @@ -37,6 +37,8 @@ No aliases are kept — silent aliasing would regress behavior invisibly (the ol
- **`snapshot --publish`**: direct non-interactive cloud upload. Respects the sync source (updates it) or creates a new config with a prompt. Does not ask for name/desc/visibility when updating existing.
- **`snapshot` in pipe**: piping `openboot snapshot` to another command now emits JSON to stdout automatically (TTY detection).
- **Shell capture**: snapshots now include the Oh-My-Zsh state, theme, and plugins (previously the field was defined but never populated — publishing silently dropped shell data).
- **Interactive dry-run**: `install --dry-run` on a TTY now walks the same full-screen wizard as a real install and previews the reviewed plan (DRY-RUN banner, no changes made). Non-TTY dry-run stays linear.
- **`install --update` help corrected**: the flag has always meant "update Homebrew and exit"; the help text now says so instead of implying it combines with an install.

### Philosophy

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,9 @@ Removed in v1.0: `pull`, `push`, `diff`, `clean`, `log`, `restore`, `init`, `set
--from FILE Install from a local config or snapshot JSON file
-s, --silent Non-interactive mode (requires env vars)
--dry-run Preview what would be installed
--pick NAMES Comma-separated packages to install from a remote config
--packages-only Install packages only, skip system config
--update Update Homebrew before installing
--update Update Homebrew and exit
--shell MODE Shell setup: install, skip
--macos MODE macOS prefs: configure, skip
--dotfiles MODE Dotfiles: clone, link, skip
Expand Down
49 changes: 31 additions & 18 deletions internal/snapshot/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
# SNAPSHOT PACKAGE

Environment capture, matching, and restoration. 8 files (4 source + 4 test), 1,783 lines.
Environment capture, matching, and restoration. 6 source files + tests.

## FILES

| File | Lines | Purpose |
|------|-------|---------|
| `capture.go` | 528 | Capture formulae/casks/taps/npm/prefs/shell/git/devtools |
| `capture.go` | 562 | Capture pipeline: formulae/casks/taps/npm/bun/prefs/dock/login items/git/dotfiles/devtools/shell |
| `dock.go` | 268 | Dock persistent-apps capture (`com.apple.dock` plist parsing) |
| `loginitems.go` | 60 | Login items capture via `osascript` |
| `match.go` | 112 | Match captured packages against catalog, Jaccard similarity for preset detection |
| `local.go` | 62 | Read/write snapshots to `~/.openboot/snapshot.json` |
| `snapshot.go` | 61 | Data structures: Snapshot, PackageSnapshot, MacOSPrefs, ShellConfig |
| `local.go` | 74 | Read/write snapshots to `~/.openboot/snapshot.json`; `LoadFile`/`ParseBytes` |
| `snapshot.go` | 202 | Data structures + `PackageSnapshot` JSON codec accepting legacy shapes |

## CAPTURE PIPELINE

`CaptureWithProgress()` runs 8 sequential steps, each reporting via callback:
`CaptureWithProgress()` runs 12 sequential steps (`captureSteps` in capture.go), each reporting via callback:

1. Homebrew Formulae → `brew leaves` (top-level only, excludes dependencies)
2. Homebrew Casks → `brew list --cask`
3. Homebrew Taps → `brew tap`
4. npm Packages → `npm list -g --json`
5. macOS Preferences → reads known defaults keys
6. Shell Config → detects shell, oh-my-zsh, plugins, aliases
7. Git Config → user.name, user.email, core.editor, etc.
8. Dev Tools → version detection for node, go, python, rust, docker, etc.

Each step is independent. Failures are non-fatal (captured as empty).
4. NPM Global Packages → `npm list -g --json`
5. Bun Global Packages
6. macOS Preferences → reads known defaults keys
7. Dock Apps → `com.apple.dock` persistent-apps
8. Login Items → `osascript`
9. Git Configuration → user.name, user.email, core.editor, etc.
10. Dotfiles → repo URL of `~/.dotfiles`
11. Dev Tools → version detection for node, go, python, rust, docker, etc.
12. Shell Config → detects shell, oh-my-zsh, plugins, aliases

Each step is independent. Failures are non-fatal (recorded in `failed_steps`, snapshot marked `partial`).

## MATCHING LOGIC (match.go)

Expand All @@ -43,12 +49,19 @@ Each step is independent. Failures are non-fatal (captured as empty).
"formulae": ["curl", "wget"],
"casks": ["visual-studio-code"],
"taps": ["homebrew/core"],
"npm": ["typescript"]
"npm": ["typescript"],
"bun": []
},
"macos_prefs": { ... },
"shell_config": { ... },
"git_config": { ... },
"dev_tools": { ... }
"macos_prefs": [ ... ],
"shell": { ... },
"git": { ... },
"dotfiles": { ... },
"dev_tools": [ ... ],
"matched_preset": "developer",
"catalog_match": { ... },
"dock_apps": ["/Applications/Safari.app"],
"login_items": [{ "name": "Raycast", "path": "...", "hidden": false }],
"health": { "failed_steps": [], "partial": false }
}
```

Expand All @@ -66,7 +79,7 @@ Snapshot data is mapped to `config.SnapshotGitConfig` and `config.SnapshotShellC

## WHEN MODIFYING

- Adding capture step: Add to `CaptureWithProgress()`, update `totalSteps`, add to `Snapshot` struct
- Adding capture step: Add an entry to `captureSteps` in `capture.go`, add to `Snapshot` struct
- Adding restore step: Add to `installer.RunFromSnapshot()`, create `config.Snapshot*Config` type, wire in `cli/snapshot.go`
- Adding preset detection: Modify `DetectBestPreset()` scoring in `match.go`
- Tests: Table-driven with testify. `capture_test.go` mocks command output. `match_test.go` tests Jaccard scoring.
28 changes: 16 additions & 12 deletions internal/sync/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
# SYNC PACKAGE

Compute diff + execute plan for syncing a remote config with the local system. 3 source files + 3 test files, 1,040 lines.
Compute diff + execute plan for syncing a remote config with the local system.

## FILES

| File | Lines | Purpose |
|------|-------|---------|
| `source.go` | 95 | `SyncSource` struct + Load/Save/Delete to `~/.openboot/sync_source.json` |
| `diff.go` | 239 | `SyncDiff` struct + `ComputeDiff()` comparing remote config vs local system |
| `plan.go` | 196 | `SyncPlan` struct + `Execute()` applying selected changes via brew/npm/shell/macos |
| `source.go` | 102 | `SyncSource` struct + Load/Save/Delete to `~/.openboot/sync_source.json` |
| `diff.go` | 280 | `SyncDiff` struct + `ComputeDiff()` comparing remote config vs local system |
| `plan.go` | 212 | `SyncPlan` struct + `Execute()`/`ExecuteContext()` applying selected changes via brew/npm/shell/macos |

## HOW IT WORKS

```
openboot install user/config → saves SyncSource to disk
openboot sync → loads SyncSource
openboot install (no args) → loads SyncSource (cli/install.go runSyncInstall)
→ fetches latest RemoteConfig
→ ComputeDiff(rc) compares remote vs local
user selects changes in TUI (cli/sync.go)
3-way prompt: install / customize / cancel (cli/sync_helpers.go)
Execute(plan, dryRun) applies changes
ExecuteContext(ctx, plan, dryRun) applies additions
```

## SOURCE PERSISTENCE (source.go)
Expand All @@ -49,11 +49,11 @@ Key types:
- `ShellDiff` — theme/plugins changes
- `MacOSPrefDiff` — per-preference domain/key/value diff

Helper: `ToSet([]string) map[string]bool` — exported for use in `cli/sync.go`
Helper: `ToSet([]string) map[string]bool` — exported wrapper around the `diff` package's `ToSet`

## PLAN EXECUTION (plan.go)

`Execute(plan *SyncPlan, dryRun bool) (*SyncResult, error)`:
`Execute(plan *SyncPlan, dryRun bool)` / `ExecuteContext(ctx, plan, dryRun) (*SyncResult, error)`:

Execution order (dependency-aware):
1. Install taps (other packages may depend on them)
Expand All @@ -63,6 +63,10 @@ Execution order (dependency-aware):
5. Update shell (theme + plugins via `shell.RestoreFromSnapshot`)
6. Apply macOS preferences (via `macos.Configure`)

The uninstall branches remain implemented, but since v1.0 no CLI path
populates the `Uninstall*` fields — install is additive
(`buildInstallPlan` in `cli/sync_helpers.go` never sets them).

Error handling: Collects all errors via `errors.Join` (continues on failure).

## REUSED FUNCTIONS
Expand All @@ -83,11 +87,11 @@ Error handling: Collects all errors via `errors.Join` (continues on failure).

- Pure logic functions (diffLists, ToSet, HasChanges, Totals, TotalActions, IsEmpty) have 100% coverage
- `getLocalDotfilesURL` tested with temp git repo at 80%
- `ComputeDiff` and `Execute` depend on external commands (brew, npm, git) — not unit-testable without interface refactoring. Exercise them via real-subprocess tests in `test/integration/` (run as part of L1, `make test-unit`).
- `Execute` is unit-tested with faked `brew.Runner`/`npm.Runner` doubles (`execute_test.go`). `ComputeDiff` still captures via real commands (brew, npm, git) — exercise it via real-subprocess tests in `test/integration/` (run as part of L1, `make test-unit`).
- Source persistence tested with `t.TempDir()` + `t.Setenv("HOME", tmpDir)` pattern

## WHEN MODIFYING

- Adding a new diff category: Add fields to `SyncDiff`, update `HasChanges/TotalMissing/TotalExtra/TotalChanged`, add capture in `ComputeDiff`, update `cli/sync.go` TUI
- Adding a new plan action: Add fields to `SyncPlan`, update `TotalActions`, add execution branch in `Execute`, update `cli/sync.go` `buildSyncPlan`
- Adding a new diff category: Add fields to `SyncDiff`, update `HasChanges/TotalMissing/TotalExtra/TotalChanged`, add capture in `ComputeDiff`, update the printed diff in `cli/sync_helpers.go`
- Adding a new plan action: Add fields to `SyncPlan`, update `TotalActions`, add execution branch in `Execute`, update `buildInstallPlan` in `cli/sync_helpers.go`
- Changing persistence format: Update `SyncSource` struct — JSON tags are the wire format
Loading