package dev import ( "context" "errors" "os" "time" "forge.lthn.ai/core/go/pkg/cli" "forge.lthn.ai/core/go-devops/devops" "forge.lthn.ai/core/go/pkg/i18n" "forge.lthn.ai/core/go/pkg/io" ) // 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 *cli.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 *cli.Command) { installCmd := &cli.Command{ Use: "install", Short: i18n.T("cmd.dev.vm.install.short"), Long: i18n.T("cmd.dev.vm.install.long"), RunE: func(cmd *cli.Command, args []string) error { return runVMInstall() }, } parent.AddCommand(installCmd) } func runVMInstall() error { d, err := devops.New(io.Local) if err != nil { return err } if d.IsInstalled() { cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.already_installed"))) cli.Blank() cli.Text(i18n.T("cmd.dev.vm.check_updates", map[string]interface{}{"Command": dimStyle.Render("core dev update")})) return nil } cli.Print("%s %s\n", dimStyle.Render(i18n.Label("image")), devops.ImageName()) cli.Blank() cli.Text(i18n.T("cmd.dev.vm.downloading")) cli.Blank() 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) { cli.Print("\r%s %d%%", dimStyle.Render(i18n.T("cmd.dev.vm.progress_label")), pct) lastProgress = downloaded } } }) cli.Blank() // Clear progress line if err != nil { return cli.Wrap(err, "install failed") } elapsed := time.Since(start).Round(time.Second) cli.Blank() cli.Text(i18n.T("cmd.dev.vm.installed_in", map[string]interface{}{"Duration": elapsed})) cli.Blank() cli.Text(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")})) return nil } // VM boot command flags var ( vmBootMemory int vmBootCPUs int vmBootFresh bool ) // addVMBootCommand adds the 'devops boot' command. func addVMBootCommand(parent *cli.Command) { bootCmd := &cli.Command{ Use: "boot", Short: i18n.T("cmd.dev.vm.boot.short"), Long: i18n.T("cmd.dev.vm.boot.long"), RunE: func(cmd *cli.Command, args []string) error { return runVMBoot(vmBootMemory, vmBootCPUs, vmBootFresh) }, } bootCmd.Flags().IntVar(&vmBootMemory, "memory", 0, i18n.T("cmd.dev.vm.boot.flag.memory")) bootCmd.Flags().IntVar(&vmBootCPUs, "cpus", 0, i18n.T("cmd.dev.vm.boot.flag.cpus")) bootCmd.Flags().BoolVar(&vmBootFresh, "fresh", false, i18n.T("cmd.dev.vm.boot.flag.fresh")) parent.AddCommand(bootCmd) } func runVMBoot(memory, cpus int, fresh bool) error { d, err := devops.New(io.Local) if err != nil { return err } if !d.IsInstalled() { return errors.New(i18n.T("cmd.dev.vm.not_installed")) } opts := devops.DefaultBootOptions() if memory > 0 { opts.Memory = memory } if cpus > 0 { opts.CPUs = cpus } opts.Fresh = fresh cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.config_label")), i18n.T("cmd.dev.vm.config_value", map[string]interface{}{"Memory": opts.Memory, "CPUs": opts.CPUs})) cli.Blank() cli.Text(i18n.T("cmd.dev.vm.booting")) ctx := context.Background() if err := d.Boot(ctx, opts); err != nil { return err } cli.Blank() cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.running"))) cli.Blank() cli.Text(i18n.T("cmd.dev.vm.connect_with", map[string]interface{}{"Command": dimStyle.Render("core dev shell")})) cli.Print("%s %s\n", i18n.T("cmd.dev.vm.ssh_port"), dimStyle.Render("2222")) return nil } // addVMStopCommand adds the 'devops stop' command. func addVMStopCommand(parent *cli.Command) { stopCmd := &cli.Command{ Use: "stop", Short: i18n.T("cmd.dev.vm.stop.short"), Long: i18n.T("cmd.dev.vm.stop.long"), RunE: func(cmd *cli.Command, args []string) error { return runVMStop() }, } parent.AddCommand(stopCmd) } func runVMStop() error { d, err := devops.New(io.Local) if err != nil { return err } ctx := context.Background() running, err := d.IsRunning(ctx) if err != nil { return err } if !running { cli.Text(dimStyle.Render(i18n.T("cmd.dev.vm.not_running"))) return nil } cli.Text(i18n.T("cmd.dev.vm.stopping")) if err := d.Stop(ctx); err != nil { return err } cli.Text(successStyle.Render(i18n.T("common.status.stopped"))) return nil } // addVMStatusCommand adds the 'devops status' command. func addVMStatusCommand(parent *cli.Command) { statusCmd := &cli.Command{ Use: "vm-status", Short: i18n.T("cmd.dev.vm.status.short"), Long: i18n.T("cmd.dev.vm.status.long"), RunE: func(cmd *cli.Command, args []string) error { return runVMStatus() }, } parent.AddCommand(statusCmd) } func runVMStatus() error { d, err := devops.New(io.Local) if err != nil { return err } ctx := context.Background() status, err := d.Status(ctx) if err != nil { return err } cli.Text(headerStyle.Render(i18n.T("cmd.dev.vm.status_title"))) cli.Blank() // Installation status if status.Installed { cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), successStyle.Render(i18n.T("cmd.dev.vm.installed_yes"))) if status.ImageVersion != "" { cli.Print("%s %s\n", dimStyle.Render(i18n.Label("version")), status.ImageVersion) } } else { cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), errorStyle.Render(i18n.T("cmd.dev.vm.installed_no"))) cli.Blank() cli.Text(i18n.T("cmd.dev.vm.install_with", map[string]interface{}{"Command": dimStyle.Render("core dev install")})) return nil } cli.Blank() // Running status if status.Running { cli.Print("%s %s\n", dimStyle.Render(i18n.Label("status")), successStyle.Render(i18n.T("common.status.running"))) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.container_label")), status.ContainerID[:8]) cli.Print("%s %dMB\n", dimStyle.Render(i18n.T("cmd.dev.vm.memory_label")), status.Memory) cli.Print("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.cpus_label")), status.CPUs) cli.Print("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.ssh_port")), status.SSHPort) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.uptime_label")), formatVMUptime(status.Uptime)) } else { cli.Print("%s %s\n", dimStyle.Render(i18n.Label("status")), dimStyle.Render(i18n.T("common.status.stopped"))) cli.Blank() cli.Text(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")})) } return nil } func formatVMUptime(d time.Duration) string { if d < time.Minute { return cli.Sprintf("%ds", int(d.Seconds())) } if d < time.Hour { return cli.Sprintf("%dm", int(d.Minutes())) } if d < 24*time.Hour { return cli.Sprintf("%dh %dm", int(d.Hours()), int(d.Minutes())%60) } return cli.Sprintf("%dd %dh", int(d.Hours()/24), int(d.Hours())%24) } // VM shell command flags var vmShellConsole bool // addVMShellCommand adds the 'devops shell' command. func addVMShellCommand(parent *cli.Command) { shellCmd := &cli.Command{ Use: "shell [-- command...]", Short: i18n.T("cmd.dev.vm.shell.short"), Long: i18n.T("cmd.dev.vm.shell.long"), RunE: func(cmd *cli.Command, args []string) error { return runVMShell(vmShellConsole, args) }, } shellCmd.Flags().BoolVar(&vmShellConsole, "console", false, i18n.T("cmd.dev.vm.shell.flag.console")) parent.AddCommand(shellCmd) } func runVMShell(console bool, command []string) error { d, err := devops.New(io.Local) if err != nil { return err } opts := devops.ShellOptions{ Console: console, Command: command, } ctx := context.Background() return d.Shell(ctx, opts) } // VM serve command flags var ( vmServePort int vmServePath string ) // addVMServeCommand adds the 'devops serve' command. func addVMServeCommand(parent *cli.Command) { serveCmd := &cli.Command{ Use: "serve", Short: i18n.T("cmd.dev.vm.serve.short"), Long: i18n.T("cmd.dev.vm.serve.long"), RunE: func(cmd *cli.Command, args []string) error { return runVMServe(vmServePort, vmServePath) }, } serveCmd.Flags().IntVarP(&vmServePort, "port", "p", 0, i18n.T("cmd.dev.vm.serve.flag.port")) serveCmd.Flags().StringVar(&vmServePath, "path", "", i18n.T("cmd.dev.vm.serve.flag.path")) parent.AddCommand(serveCmd) } func runVMServe(port int, path string) error { d, err := devops.New(io.Local) 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) } // VM test command flags var vmTestName string // addVMTestCommand adds the 'devops test' command. func addVMTestCommand(parent *cli.Command) { testCmd := &cli.Command{ Use: "test [-- command...]", Short: i18n.T("cmd.dev.vm.test.short"), Long: i18n.T("cmd.dev.vm.test.long"), RunE: func(cmd *cli.Command, args []string) error { return runVMTest(vmTestName, args) }, } testCmd.Flags().StringVarP(&vmTestName, "name", "n", "", i18n.T("cmd.dev.vm.test.flag.name")) parent.AddCommand(testCmd) } func runVMTest(name string, command []string) error { d, err := devops.New(io.Local) 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) } // VM claude command flags var ( vmClaudeNoAuth bool vmClaudeModel string vmClaudeAuthFlags []string ) // addVMClaudeCommand adds the 'devops claude' command. func addVMClaudeCommand(parent *cli.Command) { claudeCmd := &cli.Command{ Use: "claude", Short: i18n.T("cmd.dev.vm.claude.short"), Long: i18n.T("cmd.dev.vm.claude.long"), RunE: func(cmd *cli.Command, args []string) error { return runVMClaude(vmClaudeNoAuth, vmClaudeModel, vmClaudeAuthFlags) }, } claudeCmd.Flags().BoolVar(&vmClaudeNoAuth, "no-auth", false, i18n.T("cmd.dev.vm.claude.flag.no_auth")) claudeCmd.Flags().StringVarP(&vmClaudeModel, "model", "m", "", i18n.T("cmd.dev.vm.claude.flag.model")) claudeCmd.Flags().StringSliceVar(&vmClaudeAuthFlags, "auth", nil, i18n.T("cmd.dev.vm.claude.flag.auth")) parent.AddCommand(claudeCmd) } func runVMClaude(noAuth bool, model string, authFlags []string) error { d, err := devops.New(io.Local) 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) } // VM update command flags var vmUpdateApply bool // addVMUpdateCommand adds the 'devops update' command. func addVMUpdateCommand(parent *cli.Command) { updateCmd := &cli.Command{ Use: "update", Short: i18n.T("cmd.dev.vm.update.short"), Long: i18n.T("cmd.dev.vm.update.long"), RunE: func(cmd *cli.Command, args []string) error { return runVMUpdate(vmUpdateApply) }, } updateCmd.Flags().BoolVar(&vmUpdateApply, "apply", false, i18n.T("cmd.dev.vm.update.flag.apply")) parent.AddCommand(updateCmd) } func runVMUpdate(apply bool) error { d, err := devops.New(io.Local) if err != nil { return err } ctx := context.Background() cli.Text(i18n.T("common.progress.checking_updates")) cli.Blank() current, latest, hasUpdate, err := d.CheckUpdate(ctx) if err != nil { return cli.Wrap(err, "failed to check for updates") } cli.Print("%s %s\n", dimStyle.Render(i18n.Label("current")), valueStyle.Render(current)) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.latest_label")), valueStyle.Render(latest)) cli.Blank() if !hasUpdate { cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.up_to_date"))) return nil } cli.Text(warningStyle.Render(i18n.T("cmd.dev.vm.update_available"))) cli.Blank() if !apply { cli.Text(i18n.T("cmd.dev.vm.run_to_update", map[string]interface{}{"Command": dimStyle.Render("core dev update --apply")})) return nil } // Stop if running running, _ := d.IsRunning(ctx) if running { cli.Text(i18n.T("cmd.dev.vm.stopping_current")) _ = d.Stop(ctx) } cli.Text(i18n.T("cmd.dev.vm.downloading_update")) cli.Blank() start := time.Now() err = d.Install(ctx, func(downloaded, total int64) { if total > 0 { pct := int(float64(downloaded) / float64(total) * 100) cli.Print("\r%s %d%%", dimStyle.Render(i18n.T("cmd.dev.vm.progress_label")), pct) } }) cli.Blank() if err != nil { return cli.Wrap(err, "update failed") } elapsed := time.Since(start).Round(time.Second) cli.Blank() cli.Text(i18n.T("cmd.dev.vm.updated_in", map[string]interface{}{"Duration": elapsed})) return nil }