package vm import ( "context" goio "io" "text/tabwriter" "time" core "dappco.re/go/core" "dappco.re/go/core/container" "dappco.re/go/core/container/internal/proc" "dappco.re/go/core/i18n" "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/cli/pkg/cli" ) var ( runName string runDetach bool runMemory int runCPUs int runSSHPort int runTemplateName string runVarFlags []string ) // addVMRunCommand adds the 'run' command under vm. func addVMRunCommand(parent *cli.Command) { runCmd := &cli.Command{ Use: "run [image]", Short: i18n.T("cmd.vm.run.short"), Long: i18n.T("cmd.vm.run.long"), RunE: func(cmd *cli.Command, args []string) error { opts := container.RunOptions{ Name: runName, Detach: runDetach, Memory: runMemory, CPUs: runCPUs, SSHPort: runSSHPort, } // If template is specified, build and run from template if runTemplateName != "" { vars := ParseVarFlags(runVarFlags) return RunFromTemplate(runTemplateName, vars, opts) } // Otherwise, require an image path if len(args) == 0 { return coreerr.E("vm run", i18n.T("cmd.vm.run.error.image_required"), nil) } image := args[0] return runContainer(image, runName, runDetach, runMemory, runCPUs, runSSHPort) }, } runCmd.Flags().StringVar(&runName, "name", "", i18n.T("cmd.vm.run.flag.name")) runCmd.Flags().BoolVarP(&runDetach, "detach", "d", false, i18n.T("cmd.vm.run.flag.detach")) runCmd.Flags().IntVar(&runMemory, "memory", 0, i18n.T("cmd.vm.run.flag.memory")) runCmd.Flags().IntVar(&runCPUs, "cpus", 0, i18n.T("cmd.vm.run.flag.cpus")) runCmd.Flags().IntVar(&runSSHPort, "ssh-port", 0, i18n.T("cmd.vm.run.flag.ssh_port")) runCmd.Flags().StringVar(&runTemplateName, "template", "", i18n.T("cmd.vm.run.flag.template")) runCmd.Flags().StringArrayVar(&runVarFlags, "var", nil, i18n.T("cmd.vm.run.flag.var")) parent.AddCommand(runCmd) } func runContainer(image, name string, detach bool, memory, cpus, sshPort int) error { manager, err := container.NewLinuxKitManager(io.Local) if err != nil { return coreerr.E("runContainer", i18n.T("i18n.fail.init", "container manager"), err) } opts := container.RunOptions{ Name: name, Detach: detach, Memory: memory, CPUs: cpus, SSHPort: sshPort, } core.Print(nil, "%s %s", dimStyle.Render(i18n.Label("image")), image) if name != "" { core.Print(nil, "%s %s", dimStyle.Render(i18n.T("cmd.vm.label.name")), name) } core.Print(nil, "%s %s", dimStyle.Render(i18n.T("cmd.vm.label.hypervisor")), manager.Hypervisor().Name()) core.Println() ctx := context.Background() c, err := manager.Run(ctx, image, opts) if err != nil { return coreerr.E("runContainer", i18n.T("i18n.fail.run", "container"), err) } if detach { core.Print(nil, "%s %s", successStyle.Render(i18n.Label("started")), c.ID) core.Print(nil, "%s %d", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID) core.Println() core.Println(i18n.T("cmd.vm.hint.view_logs", map[string]any{"ID": c.ID[:8]})) core.Println(i18n.T("cmd.vm.hint.stop", map[string]any{"ID": c.ID[:8]})) } else { core.Println() core.Print(nil, "%s %s", dimStyle.Render(i18n.T("cmd.vm.label.container_stopped")), c.ID) } return nil } var psAll bool // addVMPsCommand adds the 'ps' command under vm. func addVMPsCommand(parent *cli.Command) { psCmd := &cli.Command{ Use: "ps", Short: i18n.T("cmd.vm.ps.short"), Long: i18n.T("cmd.vm.ps.long"), RunE: func(cmd *cli.Command, args []string) error { return listContainers(psAll) }, } psCmd.Flags().BoolVarP(&psAll, "all", "a", false, i18n.T("cmd.vm.ps.flag.all")) parent.AddCommand(psCmd) } func listContainers(all bool) error { manager, err := container.NewLinuxKitManager(io.Local) if err != nil { return coreerr.E("listContainers", i18n.T("i18n.fail.init", "container manager"), err) } ctx := context.Background() containers, err := manager.List(ctx) if err != nil { return coreerr.E("listContainers", i18n.T("i18n.fail.list", "containers"), err) } // Filter if not showing all if !all { filtered := make([]*container.Container, 0) for _, c := range containers { if c.Status == container.StatusRunning { filtered = append(filtered, c) } } containers = filtered } if len(containers) == 0 { if all { core.Println(i18n.T("cmd.vm.ps.no_containers")) } else { core.Println(i18n.T("cmd.vm.ps.no_running")) } return nil } w := tabwriter.NewWriter(proc.Stdout, 0, 0, 2, ' ', 0) core.Print(w, "%s", i18n.T("cmd.vm.ps.header")) core.Print(w, "%s", "--\t----\t-----\t------\t-------\t---") for _, c := range containers { // Shorten image path imageName := c.Image if len(imageName) > 30 { imageName = "..." + imageName[len(imageName)-27:] } // Format duration duration := formatDuration(time.Since(c.StartedAt)) // Status with color status := string(c.Status) switch c.Status { case container.StatusRunning: status = successStyle.Render(status) case container.StatusStopped: status = dimStyle.Render(status) case container.StatusError: status = errorStyle.Render(status) } core.Print(w, "%s\t%s\t%s\t%s\t%s\t%d", c.ID[:8], c.Name, imageName, status, duration, c.PID) } _ = w.Flush() return nil } func formatDuration(d time.Duration) string { if d < time.Minute { return core.Sprintf("%ds", int(d.Seconds())) } if d < time.Hour { return core.Sprintf("%dm", int(d.Minutes())) } if d < 24*time.Hour { return core.Sprintf("%dh", int(d.Hours())) } return core.Sprintf("%dd", int(d.Hours()/24)) } // addVMStopCommand adds the 'stop' command under vm. func addVMStopCommand(parent *cli.Command) { stopCmd := &cli.Command{ Use: "stop ", Short: i18n.T("cmd.vm.stop.short"), Long: i18n.T("cmd.vm.stop.long"), RunE: func(cmd *cli.Command, args []string) error { if len(args) == 0 { return coreerr.E("vm stop", i18n.T("cmd.vm.error.id_required"), nil) } return stopContainer(args[0]) }, } parent.AddCommand(stopCmd) } func stopContainer(id string) error { manager, err := container.NewLinuxKitManager(io.Local) if err != nil { return coreerr.E("stopContainer", i18n.T("i18n.fail.init", "container manager"), err) } // Support partial ID matching fullID, err := resolveContainerID(manager, id) if err != nil { return err } core.Print(nil, "%s %s", dimStyle.Render(i18n.T("cmd.vm.stop.stopping")), fullID[:8]) ctx := context.Background() if err := manager.Stop(ctx, fullID); err != nil { return coreerr.E("stopContainer", i18n.T("i18n.fail.stop", "container"), err) } core.Print(nil, "%s", successStyle.Render(i18n.T("common.status.stopped"))) return nil } // resolveContainerID resolves a partial ID to a full ID. func resolveContainerID(manager *container.LinuxKitManager, partialID string) (string, error) { ctx := context.Background() containers, err := manager.List(ctx) if err != nil { return "", err } var matches []*container.Container for _, c := range containers { if core.HasPrefix(c.ID, partialID) || core.HasPrefix(c.Name, partialID) { matches = append(matches, c) } } switch len(matches) { case 0: return "", coreerr.E("resolveContainerID", i18n.T("cmd.vm.error.no_match", map[string]any{"ID": partialID}), nil) case 1: return matches[0].ID, nil default: return "", coreerr.E("resolveContainerID", i18n.T("cmd.vm.error.multiple_match", map[string]any{"ID": partialID}), nil) } } var logsFollow bool // addVMLogsCommand adds the 'logs' command under vm. func addVMLogsCommand(parent *cli.Command) { logsCmd := &cli.Command{ Use: "logs ", Short: i18n.T("cmd.vm.logs.short"), Long: i18n.T("cmd.vm.logs.long"), RunE: func(cmd *cli.Command, args []string) error { if len(args) == 0 { return coreerr.E("vm logs", i18n.T("cmd.vm.error.id_required"), nil) } return viewLogs(args[0], logsFollow) }, } logsCmd.Flags().BoolVarP(&logsFollow, "follow", "f", false, i18n.T("common.flag.follow")) parent.AddCommand(logsCmd) } func viewLogs(id string, follow bool) error { manager, err := container.NewLinuxKitManager(io.Local) if err != nil { return coreerr.E("viewLogs", i18n.T("i18n.fail.init", "container manager"), err) } fullID, err := resolveContainerID(manager, id) if err != nil { return err } ctx := context.Background() reader, err := manager.Logs(ctx, fullID, follow) if err != nil { return coreerr.E("viewLogs", i18n.T("i18n.fail.get", "logs"), err) } defer func() { _ = reader.Close() }() _, err = goio.Copy(proc.Stdout, reader) return err } // addVMExecCommand adds the 'exec' command under vm. func addVMExecCommand(parent *cli.Command) { execCmd := &cli.Command{ Use: "exec [args...]", Short: i18n.T("cmd.vm.exec.short"), Long: i18n.T("cmd.vm.exec.long"), RunE: func(cmd *cli.Command, args []string) error { if len(args) < 2 { return coreerr.E("vm exec", i18n.T("cmd.vm.error.id_and_cmd_required"), nil) } return execInContainer(args[0], args[1:]) }, } parent.AddCommand(execCmd) } func execInContainer(id string, cmd []string) error { manager, err := container.NewLinuxKitManager(io.Local) if err != nil { return coreerr.E("execInContainer", i18n.T("i18n.fail.init", "container manager"), err) } fullID, err := resolveContainerID(manager, id) if err != nil { return err } ctx := context.Background() return manager.Exec(ctx, fullID, cmd) }