go/cmd/dev/dev_vm.go

505 lines
13 KiB
Go
Raw Normal View History

package dev
import (
"context"
"fmt"
"os"
"time"
"github.com/host-uk/core/pkg/devops"
"github.com/leaanthony/clir"
)
// addVMCommands adds the dev environment VM commands to the dev parent command.
// These are added as direct subcommands: core dev install, core dev boot, etc.
func addVMCommands(parent *clir.Command) {
addVMInstallCommand(parent)
addVMBootCommand(parent)
addVMStopCommand(parent)
addVMStatusCommand(parent)
addVMShellCommand(parent)
addVMServeCommand(parent)
addVMTestCommand(parent)
addVMClaudeCommand(parent)
addVMUpdateCommand(parent)
}
// addVMInstallCommand adds the 'dev install' command.
func addVMInstallCommand(parent *clir.Command) {
installCmd := parent.NewSubCommand("install", "Download and install the dev environment image")
installCmd.LongDescription("Downloads the platform-specific dev environment image.\n\n" +
"The image includes Go, PHP, Node.js, Python, Docker, and Claude CLI.\n" +
"Downloads are cached at ~/.core/images/\n\n" +
"Examples:\n" +
" core dev install")
installCmd.Action(func() error {
return runVMInstall()
})
}
func runVMInstall() error {
d, err := devops.New()
if err != nil {
return err
}
if d.IsInstalled() {
fmt.Println(successStyle.Render("Dev environment already installed"))
fmt.Println()
fmt.Printf("Use %s to check for updates\n", dimStyle.Render("core dev update"))
return nil
}
fmt.Printf("%s %s\n", dimStyle.Render("Image:"), devops.ImageName())
fmt.Println()
fmt.Println("Downloading dev environment...")
fmt.Println()
ctx := context.Background()
start := time.Now()
var lastProgress int64
err = d.Install(ctx, func(downloaded, total int64) {
if total > 0 {
pct := int(float64(downloaded) / float64(total) * 100)
if pct != int(float64(lastProgress)/float64(total)*100) {
fmt.Printf("\r%s %d%%", dimStyle.Render("Progress:"), pct)
lastProgress = downloaded
}
}
})
fmt.Println() // Clear progress line
if err != nil {
return fmt.Errorf("install failed: %w", err)
}
elapsed := time.Since(start).Round(time.Second)
fmt.Println()
fmt.Printf("%s in %s\n", successStyle.Render("Installed"), elapsed)
fmt.Println()
fmt.Printf("Start with: %s\n", dimStyle.Render("core dev boot"))
return nil
}
// addVMBootCommand adds the 'devops boot' command.
func addVMBootCommand(parent *clir.Command) {
var memory int
var cpus int
var fresh bool
bootCmd := parent.NewSubCommand("boot", "Start the dev environment")
bootCmd.LongDescription("Boots the dev environment VM.\n\n" +
"Examples:\n" +
" core dev boot\n" +
" core dev boot --memory 8192 --cpus 4\n" +
" core dev boot --fresh")
bootCmd.IntFlag("memory", "Memory in MB (default: 4096)", &memory)
bootCmd.IntFlag("cpus", "Number of CPUs (default: 2)", &cpus)
bootCmd.BoolFlag("fresh", "Stop existing and start fresh", &fresh)
bootCmd.Action(func() error {
return runVMBoot(memory, cpus, fresh)
})
}
func runVMBoot(memory, cpus int, fresh bool) error {
d, err := devops.New()
if err != nil {
return err
}
if !d.IsInstalled() {
return fmt.Errorf("dev environment not installed (run 'core dev install' first)")
}
opts := devops.DefaultBootOptions()
if memory > 0 {
opts.Memory = memory
}
if cpus > 0 {
opts.CPUs = cpus
}
opts.Fresh = fresh
fmt.Printf("%s %dMB, %d CPUs\n", dimStyle.Render("Config:"), opts.Memory, opts.CPUs)
fmt.Println()
fmt.Println("Booting dev environment...")
ctx := context.Background()
if err := d.Boot(ctx, opts); err != nil {
return err
}
fmt.Println()
fmt.Println(successStyle.Render("Dev environment running"))
fmt.Println()
fmt.Printf("Connect with: %s\n", dimStyle.Render("core dev shell"))
fmt.Printf("SSH port: %s\n", dimStyle.Render("2222"))
return nil
}
// addVMStopCommand adds the 'devops stop' command.
func addVMStopCommand(parent *clir.Command) {
stopCmd := parent.NewSubCommand("stop", "Stop the dev environment")
stopCmd.LongDescription("Stops the running dev environment VM.\n\n" +
"Examples:\n" +
" core dev stop")
stopCmd.Action(func() error {
return runVMStop()
})
}
func runVMStop() error {
d, err := devops.New()
if err != nil {
return err
}
ctx := context.Background()
running, err := d.IsRunning(ctx)
if err != nil {
return err
}
if !running {
fmt.Println(dimStyle.Render("Dev environment is not running"))
return nil
}
fmt.Println("Stopping dev environment...")
if err := d.Stop(ctx); err != nil {
return err
}
fmt.Println(successStyle.Render("Stopped"))
return nil
}
// addVMStatusCommand adds the 'devops status' command.
func addVMStatusCommand(parent *clir.Command) {
statusCmd := parent.NewSubCommand("vm-status", "Show dev environment status")
statusCmd.LongDescription("Shows the current status of the dev environment.\n\n" +
"Examples:\n" +
" core dev vm-status")
statusCmd.Action(func() error {
return runVMStatus()
})
}
func runVMStatus() error {
d, err := devops.New()
if err != nil {
return err
}
ctx := context.Background()
status, err := d.Status(ctx)
if err != nil {
return err
}
fmt.Println(headerStyle.Render("Dev Environment Status"))
fmt.Println()
// Installation status
if status.Installed {
fmt.Printf("%s %s\n", dimStyle.Render("Installed:"), successStyle.Render("Yes"))
if status.ImageVersion != "" {
fmt.Printf("%s %s\n", dimStyle.Render("Version:"), status.ImageVersion)
}
} else {
fmt.Printf("%s %s\n", dimStyle.Render("Installed:"), errorStyle.Render("No"))
fmt.Println()
fmt.Printf("Install with: %s\n", dimStyle.Render("core dev install"))
return nil
}
fmt.Println()
// Running status
if status.Running {
fmt.Printf("%s %s\n", dimStyle.Render("Status:"), successStyle.Render("Running"))
fmt.Printf("%s %s\n", dimStyle.Render("Container:"), status.ContainerID[:8])
fmt.Printf("%s %dMB\n", dimStyle.Render("Memory:"), status.Memory)
fmt.Printf("%s %d\n", dimStyle.Render("CPUs:"), status.CPUs)
fmt.Printf("%s %d\n", dimStyle.Render("SSH Port:"), status.SSHPort)
fmt.Printf("%s %s\n", dimStyle.Render("Uptime:"), formatVMUptime(status.Uptime))
} else {
fmt.Printf("%s %s\n", dimStyle.Render("Status:"), dimStyle.Render("Stopped"))
fmt.Println()
fmt.Printf("Start with: %s\n", dimStyle.Render("core dev boot"))
}
return nil
}
func formatVMUptime(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%ds", int(d.Seconds()))
}
if d < time.Hour {
return fmt.Sprintf("%dm", int(d.Minutes()))
}
if d < 24*time.Hour {
return fmt.Sprintf("%dh %dm", int(d.Hours()), int(d.Minutes())%60)
}
return fmt.Sprintf("%dd %dh", int(d.Hours()/24), int(d.Hours())%24)
}
// addVMShellCommand adds the 'devops shell' command.
func addVMShellCommand(parent *clir.Command) {
var console bool
shellCmd := parent.NewSubCommand("shell", "Connect to the dev environment")
shellCmd.LongDescription("Opens an interactive shell in the dev environment.\n\n" +
"Uses SSH by default, or serial console with --console.\n\n" +
"Examples:\n" +
" core dev shell\n" +
" core dev shell --console\n" +
" core dev shell -- ls -la")
shellCmd.BoolFlag("console", "Use serial console instead of SSH", &console)
shellCmd.Action(func() error {
args := shellCmd.OtherArgs()
return runVMShell(console, args)
})
}
func runVMShell(console bool, command []string) error {
d, err := devops.New()
if err != nil {
return err
}
opts := devops.ShellOptions{
Console: console,
Command: command,
}
ctx := context.Background()
return d.Shell(ctx, opts)
}
// addVMServeCommand adds the 'devops serve' command.
func addVMServeCommand(parent *clir.Command) {
var port int
var path string
serveCmd := parent.NewSubCommand("serve", "Mount project and start dev server")
serveCmd.LongDescription("Mounts the current project into the dev environment and starts a dev server.\n\n" +
"Auto-detects the appropriate serve command based on project files.\n\n" +
"Examples:\n" +
" core dev serve\n" +
" core dev serve --port 3000\n" +
" core dev serve --path public")
serveCmd.IntFlag("port", "Port to serve on (default: 8000)", &port)
serveCmd.StringFlag("path", "Subdirectory to serve", &path)
serveCmd.Action(func() error {
return runVMServe(port, path)
})
}
func runVMServe(port int, path string) error {
d, err := devops.New()
if err != nil {
return err
}
projectDir, err := os.Getwd()
if err != nil {
return err
}
opts := devops.ServeOptions{
Port: port,
Path: path,
}
ctx := context.Background()
return d.Serve(ctx, projectDir, opts)
}
// addVMTestCommand adds the 'devops test' command.
func addVMTestCommand(parent *clir.Command) {
var name string
testCmd := parent.NewSubCommand("test", "Run tests in the dev environment")
testCmd.LongDescription("Runs tests in the dev environment.\n\n" +
"Auto-detects the test command based on project files, or uses .core/test.yaml.\n\n" +
"Examples:\n" +
" core dev test\n" +
" core dev test --name integration\n" +
" core dev test -- go test -v ./...")
testCmd.StringFlag("name", "Run named test command from .core/test.yaml", &name)
testCmd.Action(func() error {
args := testCmd.OtherArgs()
return runVMTest(name, args)
})
}
func runVMTest(name string, command []string) error {
d, err := devops.New()
if err != nil {
return err
}
projectDir, err := os.Getwd()
if err != nil {
return err
}
opts := devops.TestOptions{
Name: name,
Command: command,
}
ctx := context.Background()
return d.Test(ctx, projectDir, opts)
}
// addVMClaudeCommand adds the 'devops claude' command.
func addVMClaudeCommand(parent *clir.Command) {
var noAuth bool
var model string
var authFlags []string
claudeCmd := parent.NewSubCommand("claude", "Start sandboxed Claude session")
claudeCmd.LongDescription("Starts a Claude Code session inside the dev environment sandbox.\n\n" +
"Provides isolation while forwarding selected credentials.\n" +
"Auto-boots the dev environment if not running.\n\n" +
"Auth options (default: all):\n" +
" gh - GitHub CLI auth\n" +
" anthropic - Anthropic API key\n" +
" ssh - SSH agent forwarding\n" +
" git - Git config (name, email)\n\n" +
"Examples:\n" +
" core dev claude\n" +
" core dev claude --model opus\n" +
" core dev claude --auth gh,anthropic\n" +
" core dev claude --no-auth")
claudeCmd.BoolFlag("no-auth", "Don't forward any auth credentials", &noAuth)
claudeCmd.StringFlag("model", "Model to use (opus, sonnet)", &model)
claudeCmd.StringsFlag("auth", "Selective auth forwarding (gh,anthropic,ssh,git)", &authFlags)
claudeCmd.Action(func() error {
return runVMClaude(noAuth, model, authFlags)
})
}
func runVMClaude(noAuth bool, model string, authFlags []string) error {
d, err := devops.New()
if err != nil {
return err
}
projectDir, err := os.Getwd()
if err != nil {
return err
}
opts := devops.ClaudeOptions{
NoAuth: noAuth,
Model: model,
Auth: authFlags,
}
ctx := context.Background()
return d.Claude(ctx, projectDir, opts)
}
// addVMUpdateCommand adds the 'devops update' command.
func addVMUpdateCommand(parent *clir.Command) {
var apply bool
updateCmd := parent.NewSubCommand("update", "Check for and apply updates")
updateCmd.LongDescription("Checks for dev environment updates and optionally applies them.\n\n" +
"Examples:\n" +
" core dev update\n" +
" core dev update --apply")
updateCmd.BoolFlag("apply", "Download and apply the update", &apply)
updateCmd.Action(func() error {
return runVMUpdate(apply)
})
}
func runVMUpdate(apply bool) error {
d, err := devops.New()
if err != nil {
return err
}
ctx := context.Background()
fmt.Println("Checking for updates...")
fmt.Println()
current, latest, hasUpdate, err := d.CheckUpdate(ctx)
if err != nil {
return fmt.Errorf("failed to check for updates: %w", err)
}
fmt.Printf("%s %s\n", dimStyle.Render("Current:"), valueStyle.Render(current))
fmt.Printf("%s %s\n", dimStyle.Render("Latest:"), valueStyle.Render(latest))
fmt.Println()
if !hasUpdate {
fmt.Println(successStyle.Render("Already up to date"))
return nil
}
fmt.Println(warningStyle.Render("Update available"))
fmt.Println()
if !apply {
fmt.Printf("Run %s to update\n", dimStyle.Render("core dev update --apply"))
return nil
}
// Stop if running
running, _ := d.IsRunning(ctx)
if running {
fmt.Println("Stopping current instance...")
_ = d.Stop(ctx)
}
fmt.Println("Downloading update...")
fmt.Println()
start := time.Now()
err = d.Install(ctx, func(downloaded, total int64) {
if total > 0 {
pct := int(float64(downloaded) / float64(total) * 100)
fmt.Printf("\r%s %d%%", dimStyle.Render("Progress:"), pct)
}
})
fmt.Println()
if err != nil {
return fmt.Errorf("update failed: %w", err)
}
elapsed := time.Since(start).Round(time.Second)
fmt.Println()
fmt.Printf("%s in %s\n", successStyle.Render("Updated"), elapsed)
return nil
}