Skip to content

Commit 2a28a95

Browse files
committed
feat: add openboot init for project environment setup
Adds .openboot.yml support for declaring project dependencies in repos. Teams can commit .openboot.yml to their project root, and new developers run 'openboot init' to install brew packages, npm globals, and run setup scripts. - Add ProjectConfig YAML parser (internal/config/project.go) - Add init command CLI (internal/cli/init.go) - Add initializer orchestrator (internal/initializer/) - Support brew taps/packages/casks, npm packages - Support env vars, init scripts, verify scripts - Add unit tests and example config
1 parent 3bd4661 commit 2a28a95

6 files changed

Lines changed: 659 additions & 0 deletions

File tree

.openboot.yml.example

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
version: "1.0"
2+
3+
brew:
4+
taps:
5+
- homebrew/cask-fonts
6+
packages:
7+
- git
8+
- node@20
9+
- go
10+
- jq
11+
- ripgrep
12+
casks:
13+
- visual-studio-code
14+
- docker
15+
16+
npm:
17+
- typescript
18+
- eslint
19+
- prettier
20+
21+
env:
22+
NODE_ENV: development
23+
API_URL: http://localhost:3000
24+
25+
init:
26+
- npm install
27+
- cp .env.example .env
28+
- npm run db:migrate
29+
30+
verify:
31+
- node --version
32+
- npm --version
33+
- git --version
34+
- docker --version

internal/cli/init.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/openbootdotdev/openboot/internal/config"
9+
"github.com/openbootdotdev/openboot/internal/initializer"
10+
"github.com/openbootdotdev/openboot/internal/updater"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
var initCmd = &cobra.Command{
15+
Use: "init [directory]",
16+
Short: "Set up project environment from .openboot.yml",
17+
Long: `Read .openboot.yml from the project directory and install declared dependencies,
18+
run init scripts, and verify the environment is ready.`,
19+
Example: ` # Initialize from current directory
20+
openboot init
21+
22+
# Initialize from specific directory
23+
openboot init /path/to/project
24+
25+
# Preview changes without installing
26+
openboot init --dry-run`,
27+
Args: cobra.MaximumNArgs(1),
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
updater.AutoUpgrade(version)
30+
31+
dir := "."
32+
if len(args) > 0 {
33+
dir = args[0]
34+
}
35+
36+
absDir, err := filepath.Abs(dir)
37+
if err != nil {
38+
return fmt.Errorf("failed to resolve directory: %w", err)
39+
}
40+
41+
if _, err := os.Stat(absDir); os.IsNotExist(err) {
42+
return fmt.Errorf("directory does not exist: %s", absDir)
43+
}
44+
45+
projectCfg, err := config.LoadProjectConfig(absDir)
46+
if err != nil {
47+
return err
48+
}
49+
50+
initCfg := &initializer.Config{
51+
ProjectDir: absDir,
52+
ProjectConfig: projectCfg,
53+
DryRun: cfg.DryRun,
54+
Silent: cfg.Silent,
55+
Update: cfg.Update,
56+
Version: version,
57+
}
58+
59+
return initializer.Run(initCfg)
60+
},
61+
}
62+
63+
func init() {
64+
initCmd.Flags().SortFlags = false
65+
initCmd.Flags().BoolVar(&cfg.DryRun, "dry-run", false, "preview changes without installing")
66+
initCmd.Flags().BoolVarP(&cfg.Silent, "silent", "s", false, "non-interactive mode (for CI/CD)")
67+
initCmd.Flags().BoolVar(&cfg.Update, "update", false, "update Homebrew before installing")
68+
}

internal/cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ func init() {
9696
rootCmd.Flags().BoolVar(&cfg.Update, "update", false, "update Homebrew before installing")
9797

9898
rootCmd.AddCommand(installCmd)
99+
rootCmd.AddCommand(initCmd)
99100
rootCmd.AddCommand(versionCmd)
100101
rootCmd.AddCommand(updateCmd)
101102
rootCmd.AddCommand(doctorCmd)

internal/config/project.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"gopkg.in/yaml.v3"
9+
)
10+
11+
const ProjectConfigFileName = ".openboot.yml"
12+
13+
// ProjectConfig represents the .openboot.yml file in a project repository
14+
type ProjectConfig struct {
15+
Version string `yaml:"version"`
16+
Brew *BrewConfig `yaml:"brew,omitempty"`
17+
Npm []string `yaml:"npm,omitempty"`
18+
Env map[string]string `yaml:"env,omitempty"`
19+
Init []string `yaml:"init,omitempty"`
20+
Verify []string `yaml:"verify,omitempty"`
21+
}
22+
23+
// BrewConfig represents Homebrew package configuration
24+
type BrewConfig struct {
25+
Taps []string `yaml:"taps,omitempty"`
26+
Packages []string `yaml:"packages,omitempty"`
27+
Casks []string `yaml:"casks,omitempty"`
28+
}
29+
30+
// LoadProjectConfig reads and parses .openboot.yml from the specified directory
31+
func LoadProjectConfig(dir string) (*ProjectConfig, error) {
32+
configPath := filepath.Join(dir, ProjectConfigFileName)
33+
34+
data, err := os.ReadFile(configPath)
35+
if err != nil {
36+
if os.IsNotExist(err) {
37+
return nil, fmt.Errorf("%s not found in %s", ProjectConfigFileName, dir)
38+
}
39+
return nil, fmt.Errorf("failed to read %s: %w", ProjectConfigFileName, err)
40+
}
41+
42+
var pc ProjectConfig
43+
if err := yaml.Unmarshal(data, &pc); err != nil {
44+
return nil, fmt.Errorf("failed to parse %s: %w", ProjectConfigFileName, err)
45+
}
46+
47+
if err := pc.Validate(); err != nil {
48+
return nil, fmt.Errorf("invalid config: %w", err)
49+
}
50+
51+
return &pc, nil
52+
}
53+
54+
// Validate checks if the project config is valid
55+
func (pc *ProjectConfig) Validate() error {
56+
if pc.Version == "" {
57+
return fmt.Errorf("version field is required")
58+
}
59+
60+
if pc.Version != "1.0" {
61+
return fmt.Errorf("unsupported version: %s (supported: 1.0)", pc.Version)
62+
}
63+
64+
return nil
65+
}
66+
67+
// HasPackages returns true if the config has any packages to install
68+
func (pc *ProjectConfig) HasPackages() bool {
69+
if pc.Brew != nil {
70+
if len(pc.Brew.Packages) > 0 || len(pc.Brew.Casks) > 0 || len(pc.Brew.Taps) > 0 {
71+
return true
72+
}
73+
}
74+
return len(pc.Npm) > 0
75+
}
76+
77+
// HasInit returns true if the config has init scripts
78+
func (pc *ProjectConfig) HasInit() bool {
79+
return len(pc.Init) > 0
80+
}
81+
82+
// HasVerify returns true if the config has verify scripts
83+
func (pc *ProjectConfig) HasVerify() bool {
84+
return len(pc.Verify) > 0
85+
}
86+
87+
// HasEnv returns true if the config has environment variables
88+
func (pc *ProjectConfig) HasEnv() bool {
89+
return len(pc.Env) > 0
90+
}

0 commit comments

Comments
 (0)