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
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<!-- How did you test this? -->
- [ ] `go vet ./...` passes
- [ ] Relevant tests added or updated
- [ ] Tested locally (`./openboot --dry-run` or similar)
- [ ] Tested locally (`./openboot install --dry-run` or similar)

## Notes for reviewer

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ Bug reports and feature requests: [open an issue](https://github.com/openbootdot
git clone https://github.com/openbootdotdev/openboot.git
cd openboot
go build -o openboot ./cmd/openboot
./openboot --dry-run
./openboot install --dry-run
```

</details>
Expand Down
31 changes: 17 additions & 14 deletions internal/cli/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,40 @@ import (
"github.com/openbootdotdev/openboot/internal/ui"
)

// installCfg is the single config instance shared by the root command (openboot)
// and the install subcommand (openboot install). Both bind their flags here so
// that `openboot -p developer` and `openboot install -p developer` are identical.
// installCfg is the config instance used by the install subcommand.
var installCfg = &config.Config{}

var installCmd = &cobra.Command{
Use: "install [source]",
Short: "Set up your Mac dev environment",
Long: `Install and configure your Mac development environment.

Source resolution (position argument, in order):
Source resolution (positional argument, in order):
1. ./path, /path, or *.json → local file
2. user/slug → openboot.dev config
3. preset name → built-in preset (minimal, developer, full)
4. other word → treated as an openboot.dev alias

With no arguments, resumes from your saved sync source (or interactive if none).
With no arguments, resumes from your saved sync source (or runs the interactive
wizard if you have never synced before).

Explicit flags (--from, --user, -p) override the positional argument.`,
Example: ` # Resume last sync (or interactive if never synced)
Explicit flags (--from, --user, -p) take precedence over the positional argument.`,
Example: ` # Interactive setup (or resume last sync)
openboot install

# Install from a cloud config
openboot install alice/dev-setup
# Quick setup with a built-in preset
openboot install -p developer

# Install from a local file
openboot install ./backup.json
# Install from your cloud config
openboot install -u githubusername

# Install a built-in preset
openboot install -p developer
# Install from a specific cloud config
openboot install alice/dev-setup

# Install from a local file or snapshot
openboot install --from ./backup.json

# Preview without installing
# Preview changes without installing
openboot install --dry-run`,
Args: cobra.MaximumNArgs(1),
SilenceUsage: true,
Expand All @@ -68,6 +70,7 @@ func init() {
installCmd.Flags().StringVar(&installCfg.Shell, "shell", "", "shell setup: install, skip")
installCmd.Flags().StringVar(&installCfg.Macos, "macos", "", "macOS preferences: configure, skip")
installCmd.Flags().StringVar(&installCfg.Dotfiles, "dotfiles", "", "dotfiles: clone, link, skip")
installCmd.Flags().StringVar(&installCfg.PostInstall, "post-install", "", "post-install script: skip")

installCmd.Flags().BoolVar(&installCfg.Update, "update", false, "update Homebrew before installing")
installCmd.Flags().BoolVar(&installCfg.AllowPostInstall, "allow-post-install", false, "allow post-install scripts in silent mode")
Expand Down
46 changes: 9 additions & 37 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,26 @@ var version = "dev"

var rootCmd = &cobra.Command{
Use: "openboot",
Short: "Set up your Mac dev environment in one command",
Long: `OpenBoot - Mac development environment setup tool
Short: "Set up your Mac dev environment",
Long: `OpenBoot Mac development environment setup tool

Automates installation of Homebrew packages, CLI tools, GUI apps, npm packages,
shell configuration, and macOS preferences.`,
Example: ` # Interactive setup with package selection
openboot
Example: ` # Interactive setup
openboot install

# Quick setup with a preset
openboot -p developer
openboot install -p developer

# Install from your cloud config
openboot -u githubusername
openboot install -u githubusername

# Install from a local config or snapshot file
openboot --from config.json
openboot install --from config.json

# Capture your current environment
openboot snapshot --json > my-setup.json`,
SilenceUsage: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Install always-on file logging; --verbose controls stderr level.
// Failure here is never fatal — Init falls back to stderr internally.
Expand All @@ -52,46 +53,17 @@ shell configuration, and macOS preferences.`,
// Only the install flow needs the package catalog and auto-update.
// All other commands (snapshot, login, logout, etc.) run without
// network overhead.
installCmds := map[string]bool{
"openboot": true, // root command delegates to install
"install": true,
}
if installCmds[cmd.Name()] {
if cmd.Name() == "install" {
updater.AutoUpgrade(version)
config.RefreshPackagesFromRemote()
}

return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
// `openboot` with no subcommand is equivalent to `openboot install`.
// Root flags bind directly to installCfg, so no bridging is needed.
return runInstallCmd(cmd, args)
},
}

func init() {
rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "enable debug logging to stderr")
rootCmd.Flags().SortFlags = false

// Root is an alias for `openboot install`, so its flags bind directly to
// installCfg — the same struct used by the install subcommand. This ensures
// `openboot -p developer` and `openboot install -p developer` are identical
// code paths with no config divergence.
rootCmd.Flags().StringVarP(&installCfg.Preset, "preset", "p", "", "use a preset: minimal, developer, full")
rootCmd.Flags().StringVarP(&installCfg.User, "user", "u", "", "install from openboot.dev/username config")
rootCmd.Flags().String("from", "", "install from a local config or snapshot JSON file")
rootCmd.Flags().BoolVarP(&installCfg.Silent, "silent", "s", false, "non-interactive mode (for CI/CD)")
rootCmd.Flags().BoolVar(&installCfg.DryRun, "dry-run", false, "preview changes without installing")
rootCmd.Flags().BoolVar(&installCfg.PackagesOnly, "packages-only", false, "install packages only, skip system config")

rootCmd.Flags().StringVar(&installCfg.Shell, "shell", "", "shell setup: install, skip")
rootCmd.Flags().StringVar(&installCfg.Macos, "macos", "", "macOS preferences: configure, skip")
rootCmd.Flags().StringVar(&installCfg.Dotfiles, "dotfiles", "", "dotfiles: clone, link, skip")
rootCmd.Flags().StringVar(&installCfg.PostInstall, "post-install", "", "post-install script: skip")
rootCmd.Flags().BoolVar(&installCfg.AllowPostInstall, "allow-post-install", false, "allow post-install scripts in silent mode")

rootCmd.Flags().BoolVar(&installCfg.Update, "update", false, "update Homebrew before installing")

rootCmd.AddCommand(installCmd)
rootCmd.AddCommand(versionCmd)
Expand Down
6 changes: 5 additions & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,11 @@ main() {
echo "Starting OpenBoot setup..."
echo ""

exec openboot "$@"
if [[ "$snapshot_mode" == true ]]; then
exec openboot "$@"
else
exec openboot install "$@"
fi
}

main "$@"
2 changes: 1 addition & 1 deletion scripts/mock-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
fi
export OPENBOOT_DRY_RUN=true
export OPENBOOT_API_URL=http://localhost:{port}
{binary} -s -u testuser/test-config
{binary} install -s -u testuser/test-config
exit 0
}}
main
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/dotfiles_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (

// TestVM_Journey_DotfilesClonedAndLinked runs
//
// openboot --preset minimal --silent --dotfiles clone --shell skip --macos skip
// openboot install --preset minimal --silent --dotfiles clone --shell skip --macos skip
//
// and verifies that:
// 1. ~/.dotfiles is a valid git repository (clone succeeded).
Expand Down Expand Up @@ -112,7 +112,7 @@ func TestVM_Journey_DotfilesClonedAndLinked(t *testing.T) {

// TestVM_Journey_DotfilesLink_OnlyLinks runs
//
// openboot --preset minimal --silent --dotfiles link --shell skip --macos skip
// openboot install --preset minimal --silent --dotfiles link --shell skip --macos skip
//
// when ~/.dotfiles already exists (from a previous clone), verifying that the
// link-only mode does not re-clone but still creates symlinks.
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/macos_defaults_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type macOSPrefCheck struct {

// TestVM_Journey_MacOSDefaults_AllCategoriesWritten runs
//
// openboot --preset minimal --silent --shell skip --dotfiles skip --macos configure
// openboot install --preset minimal --silent --shell skip --dotfiles skip --macos configure
//
// and verifies that representative preferences from each of the eight
// categories in internal/macos/categories.go are actually written to the
Expand Down
10 changes: 5 additions & 5 deletions test/e2e/real_install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestE2E_InstallSinglePackage_JQ(t *testing.T) {
binary := testutil.BuildTestBinary(t)

// When: we install jq via openboot (minimal preset includes jq)
cmd := exec.Command(binary, "--packages-only", "--silent", "--preset", "minimal")
cmd := exec.Command(binary, "install", "--packages-only", "--silent", "--preset", "minimal")
cmd.Env = append(os.Environ(),
"OPENBOOT_GIT_NAME=Test User",
"OPENBOOT_GIT_EMAIL=test@example.com",
Expand Down Expand Up @@ -67,7 +67,7 @@ func TestE2E_InstallMultiplePackages(t *testing.T) {
}`)
defer os.Remove(tmpConfig)

cmd := exec.Command(binary, "--packages-only", "--silent", "--preset", "minimal")
cmd := exec.Command(binary, "install", "--packages-only", "--silent", "--preset", "minimal")
cmd.Env = append(os.Environ(),
"OPENBOOT_GIT_NAME=Test User",
"OPENBOOT_GIT_EMAIL=test@example.com",
Expand Down Expand Up @@ -183,7 +183,7 @@ func TestE2E_DryRunDoesNotInstall(t *testing.T) {
}`)
defer os.Remove(tmpConfig)

cmd := exec.Command(binary, "--dry-run", "--packages-only", "--silent", "--preset", "minimal")
cmd := exec.Command(binary, "install", "--dry-run", "--packages-only", "--silent", "--preset", "minimal")
cmd.Env = append(os.Environ(),
"OPENBOOT_GIT_NAME=Test User",
"OPENBOOT_GIT_EMAIL=test@example.com",
Expand All @@ -201,7 +201,7 @@ func TestE2E_BrewUpdateBeforeInstall(t *testing.T) {
binary := testutil.BuildTestBinary(t)

// Given: we request brew update
cmd := exec.Command(binary, "--update", "--dry-run", "--packages-only", "--silent", "--preset", "minimal")
cmd := exec.Command(binary, "install", "--update", "--dry-run", "--packages-only", "--silent", "--preset", "minimal")
cmd.Env = append(os.Environ(),
"OPENBOOT_GIT_NAME=Test User",
"OPENBOOT_GIT_EMAIL=test@example.com",
Expand All @@ -224,7 +224,7 @@ func TestE2E_GitConfigSetup(t *testing.T) {
testEmail := "e2e-test@example.com"

// Given: we have test git credentials
cmd := exec.Command(binary, "--packages-only", "--silent", "--preset", "minimal")
cmd := exec.Command(binary, "install", "--packages-only", "--silent", "--preset", "minimal")
cmd.Env = append(os.Environ(),
"OPENBOOT_GIT_NAME="+testName,
"OPENBOOT_GIT_EMAIL="+testEmail,
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/smoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func TestSmoke_DryRunNoSideEffects(t *testing.T) {
before := captureSnapshot(t, binary)

// When: run with --dry-run --preset full
cmd := exec.Command(binary, "--preset", "full", "--dry-run", "--silent")
cmd := exec.Command(binary, "install", "--preset", "full", "--dry-run", "--silent")
cmd.Env = append(os.Environ(),
"OPENBOOT_GIT_NAME=Smoke Test",
"OPENBOOT_GIT_EMAIL=smoke@test.local",
Expand Down
Loading