diff --git a/README.md b/README.md index 18cf7df..6f9330a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A Go-based bootstrap tool that installs a development environment across macOS, Debian/Ubuntu, RHEL/Fedora, and Arch Linux on `x86_64` and `aarch64`. It installs system packages, optional Flatpak GUI apps, and a set of custom third-party tools (Go, Neovim, Zig, NVM, pyenv, oh-my-zsh, Firecracker on -Linux). +Linux, agy, claude, codex, and copilot). ## Install @@ -40,5 +40,6 @@ Flags: headless: both are skipped. - `--no-vm` — macOS only: skip provisioning the Fedora-on-QEMU/VirtualBox VM that backs the `firecracker()` zsh wrapper. +- `--no-ai` — skip installation of AI/LLM CLI tools (agy, claude, codex, copilot). Package lists live in `packages.go`. Edit and rebuild. diff --git a/custom.go b/custom.go index 241ee19..9fc6283 100644 --- a/custom.go +++ b/custom.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "os/exec" "path/filepath" "regexp" "sort" @@ -54,6 +55,7 @@ var defaultInstallPaths = map[string]string{ "pyenv": "~/.pyenv", "neovim": "/usr/local/bin/nvim", "oh-my-zsh": "~/.oh-my-zsh", + "agy": "~/.local/bin/agy", } func expandHome(p string) string { @@ -84,10 +86,42 @@ func pipInstalled() bool { // isCustomPkgInstalled returns (installed, checkPath). pip ships inside the // Python distribution rather than at a fixed path, so it's detected with // `python3 -m pip --version`. +func npmInstalled(cmdName string) (bool, string) { + if hasCmd(cmdName) { + if p, err := exec.LookPath(cmdName); err == nil { + return true, p + } + return true, "" + } + home, err := os.UserHomeDir() + if err != nil { + return false, "" + } + matches, _ := filepath.Glob(filepath.Join(home, ".local/share/pnpm/bin", cmdName)) + if len(matches) > 0 { + return true, matches[0] + } + matches, _ = filepath.Glob(filepath.Join(home, ".nvm/versions/node/*/bin", cmdName)) + if len(matches) > 0 { + return true, matches[0] + } + return false, "" +} + func isCustomPkgInstalled(pkg *CustomPackage) (bool, string) { - if strings.ToLower(pkg.Name) == "pip" { + name := strings.ToLower(pkg.Name) + if name == "pip" { return pipInstalled(), "" } + if name == "claude" { + return npmInstalled("claude") + } + if name == "codex" { + return npmInstalled("codex") + } + if name == "copilot" { + return npmInstalled("copilot") + } raw := pkg.InstallPath if raw == "" { raw = defaultInstallPath(pkg) @@ -428,6 +462,7 @@ func resolveLatest(pkg *CustomPackage) { func installCustomPackages(toInstall []*CustomPackage) { fmt.Println("\n=== Custom Packages ===") + ensureNodeLTS() for _, pkg := range toInstall { name := strings.ToLower(pkg.Name) _, checkPath := isCustomPkgInstalled(pkg) @@ -448,6 +483,7 @@ func installCustomPackages(toInstall []*CustomPackage) { switch name { case "nvm": installNVM() + ensureNodeLTS() continue case "pyenv": installPyenv() @@ -467,6 +503,18 @@ func installCustomPackages(toInstall []*CustomPackage) { installNeovim(pkg, tmp) os.RemoveAll(tmp) continue + case "agy": + installAgy() + continue + case "claude": + installNpmPackage("@anthropic-ai/claude-code") + continue + case "codex": + installNpmPackage("@openai/codex") + continue + case "copilot": + installNpmPackage("@github/copilot") + continue } resolveLatest(pkg) diff --git a/main.go b/main.go index 2722ddf..e2d9452 100644 --- a/main.go +++ b/main.go @@ -33,6 +33,7 @@ func main() { only := flag.String("only", "", "Install only the named section (system|flatpak|custom)") gui := flag.Bool("gui", false, "Include GUI applications (headed environments).") noVM := flag.Bool("no-vm", false, "macOS only: skip provisioning the Fedora-on-QEMU VM that backs the firecracker() zsh wrapper.") + noAI := flag.Bool("no-ai", false, "Skip installation of LLM/AI CLI tools (agy, claude, codex, copilot).") flag.Parse() switch *only { @@ -54,6 +55,12 @@ func main() { if isMacOS && strings.ToLower(custom[i].Name) == "firecracker" { continue } + if *noAI { + name := strings.ToLower(custom[i].Name) + if name == "agy" || name == "claude" || name == "codex" || name == "copilot" { + continue + } + } customPtrs = append(customPtrs, &custom[i]) } diff --git a/packages.go b/packages.go index f17945b..21f3bf7 100644 --- a/packages.go +++ b/packages.go @@ -136,5 +136,9 @@ func customPackages() []CustomPackage { {Name: "pyenv"}, {Name: "pip"}, {Name: "oh-my-zsh"}, + {Name: "agy"}, + {Name: "claude"}, + {Name: "codex"}, + {Name: "copilot"}, } } diff --git a/post.go b/post.go index e9c7375..37269ed 100644 --- a/post.go +++ b/post.go @@ -243,6 +243,11 @@ func ensureNodeLTS() { if !runShell(`bash -c "source ~/.nvm/nvm.sh && nvm alias default 'lts/*' && nvm use --lts"`, CmdOpts{}).OK() { errLog("Setting nvm default to LTS failed") } + + fmt.Println("[NVM] Enabling corepack and installing latest pnpm ...") + if !runShell(`bash -c 'source ~/.nvm/nvm.sh && corepack enable && corepack prepare pnpm@latest --activate && pnpm config set global-bin-dir "$HOME/.local/share/pnpm/bin"'`, CmdOpts{}).OK() { + errLog("Failed to enable corepack and prepare pnpm") + } } // ── oh-my-zsh ─────────────────────────────────────────────────────────── @@ -455,3 +460,24 @@ func askYN(prompt string) bool { answer := strings.ToLower(strings.TrimSpace(string(buf[:n]))) return answer == "y" } + +func installAgy() { + fmt.Println(" Installing agy via curl ...") + if !runShell("curl -fsSL https://antigravity.google/cli/install.sh | bash", CmdOpts{}).OK() { + errLog("agy installation failed") + } +} + +func installNpmPackage(pkgName string) { + home, _ := os.UserHomeDir() + if _, err := os.Stat(filepath.Join(home, ".nvm")); err != nil { + errLog("NVM is not installed — cannot install " + pkgName) + return + } + ensureNodeLTS() + fmt.Printf(" Installing %s via pnpm ...\n", pkgName) + if !runShell(fmt.Sprintf(`bash -c "source ~/.nvm/nvm.sh && pnpm add -g %s"`, pkgName), CmdOpts{}).OK() { + errLog(fmt.Sprintf("%s installation failed", pkgName)) + } +} +