diff --git a/cmd/vm/cmd_commands.go b/cmd/vm/cmd_commands.go new file mode 100644 index 0000000..2631e82 --- /dev/null +++ b/cmd/vm/cmd_commands.go @@ -0,0 +1,13 @@ +// Package vm provides LinuxKit virtual machine management commands. +// +// Commands: +// - run: Run a VM from image (.iso, .qcow2, .vmdk, .raw) or template +// - ps: List running VMs +// - stop: Stop a running VM +// - logs: View VM logs +// - exec: Execute command in VM via SSH +// - templates: Manage LinuxKit templates (list, build) +// +// Uses qemu or hyperkit depending on system availability. +// Templates are built from YAML definitions and can include variables. +package vm diff --git a/cmd/vm/cmd_container.go b/cmd/vm/cmd_container.go new file mode 100644 index 0000000..5f6d524 --- /dev/null +++ b/cmd/vm/cmd_container.go @@ -0,0 +1,345 @@ +package vm + +import ( + "context" + "errors" + "fmt" + goio "io" + "os" + "strings" + "text/tabwriter" + "time" + + "forge.lthn.ai/core/cli/pkg/cli" + "forge.lthn.ai/core/go-container" + "forge.lthn.ai/core/go-i18n" + "forge.lthn.ai/core/go-io" +) + +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 errors.New(i18n.T("cmd.vm.run.error.image_required")) + } + 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 fmt.Errorf(i18n.T("i18n.fail.init", "container manager")+": %w", err) + } + + opts := container.RunOptions{ + Name: name, + Detach: detach, + Memory: memory, + CPUs: cpus, + SSHPort: sshPort, + } + + fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("image")), image) + if name != "" { + fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.name")), name) + } + fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.hypervisor")), manager.Hypervisor().Name()) + fmt.Println() + + ctx := context.Background() + c, err := manager.Run(ctx, image, opts) + if err != nil { + return fmt.Errorf(i18n.T("i18n.fail.run", "container")+": %w", err) + } + + if detach { + fmt.Printf("%s %s\n", successStyle.Render(i18n.Label("started")), c.ID) + fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID) + fmt.Println() + fmt.Println(i18n.T("cmd.vm.hint.view_logs", map[string]any{"ID": c.ID[:8]})) + fmt.Println(i18n.T("cmd.vm.hint.stop", map[string]any{"ID": c.ID[:8]})) + } else { + fmt.Printf("\n%s %s\n", 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 fmt.Errorf(i18n.T("i18n.fail.init", "container manager")+": %w", err) + } + + ctx := context.Background() + containers, err := manager.List(ctx) + if err != nil { + return fmt.Errorf(i18n.T("i18n.fail.list", "containers")+": %w", 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 { + fmt.Println(i18n.T("cmd.vm.ps.no_containers")) + } else { + fmt.Println(i18n.T("cmd.vm.ps.no_running")) + } + return nil + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, i18n.T("cmd.vm.ps.header")) + _, _ = fmt.Fprintln(w, "--\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) + } + + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%d\n", + 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 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", int(d.Hours())) + } + return fmt.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 errors.New(i18n.T("cmd.vm.error.id_required")) + } + return stopContainer(args[0]) + }, + } + + parent.AddCommand(stopCmd) +} + +func stopContainer(id string) error { + manager, err := container.NewLinuxKitManager(io.Local) + if err != nil { + return fmt.Errorf(i18n.T("i18n.fail.init", "container manager")+": %w", err) + } + + // Support partial ID matching + fullID, err := resolveContainerID(manager, id) + if err != nil { + return err + } + + fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.stop.stopping")), fullID[:8]) + + ctx := context.Background() + if err := manager.Stop(ctx, fullID); err != nil { + return fmt.Errorf(i18n.T("i18n.fail.stop", "container")+": %w", err) + } + + fmt.Printf("%s\n", 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 strings.HasPrefix(c.ID, partialID) || strings.HasPrefix(c.Name, partialID) { + matches = append(matches, c) + } + } + + switch len(matches) { + case 0: + return "", errors.New(i18n.T("cmd.vm.error.no_match", map[string]any{"ID": partialID})) + case 1: + return matches[0].ID, nil + default: + return "", errors.New(i18n.T("cmd.vm.error.multiple_match", map[string]any{"ID": partialID})) + } +} + +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 errors.New(i18n.T("cmd.vm.error.id_required")) + } + 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 fmt.Errorf(i18n.T("i18n.fail.init", "container manager")+": %w", 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 fmt.Errorf(i18n.T("i18n.fail.get", "logs")+": %w", err) + } + defer func() { _ = reader.Close() }() + + _, err = goio.Copy(os.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 errors.New(i18n.T("cmd.vm.error.id_and_cmd_required")) + } + 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 fmt.Errorf(i18n.T("i18n.fail.init", "container manager")+": %w", err) + } + + fullID, err := resolveContainerID(manager, id) + if err != nil { + return err + } + + ctx := context.Background() + return manager.Exec(ctx, fullID, cmd) +} diff --git a/cmd/vm/cmd_templates.go b/cmd/vm/cmd_templates.go new file mode 100644 index 0000000..cfbb4b7 --- /dev/null +++ b/cmd/vm/cmd_templates.go @@ -0,0 +1,311 @@ +package vm + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "text/tabwriter" + + "forge.lthn.ai/core/cli/pkg/cli" + "forge.lthn.ai/core/go-container" + "forge.lthn.ai/core/go-i18n" + "forge.lthn.ai/core/go-io" +) + +// addVMTemplatesCommand adds the 'templates' command under vm. +func addVMTemplatesCommand(parent *cli.Command) { + templatesCmd := &cli.Command{ + Use: "templates", + Short: i18n.T("cmd.vm.templates.short"), + Long: i18n.T("cmd.vm.templates.long"), + RunE: func(cmd *cli.Command, args []string) error { + return listTemplates() + }, + } + + // Add subcommands + addTemplatesShowCommand(templatesCmd) + addTemplatesVarsCommand(templatesCmd) + + parent.AddCommand(templatesCmd) +} + +// addTemplatesShowCommand adds the 'templates show' subcommand. +func addTemplatesShowCommand(parent *cli.Command) { + showCmd := &cli.Command{ + Use: "show ", + Short: i18n.T("cmd.vm.templates.show.short"), + Long: i18n.T("cmd.vm.templates.show.long"), + RunE: func(cmd *cli.Command, args []string) error { + if len(args) == 0 { + return errors.New(i18n.T("cmd.vm.error.template_required")) + } + return showTemplate(args[0]) + }, + } + + parent.AddCommand(showCmd) +} + +// addTemplatesVarsCommand adds the 'templates vars' subcommand. +func addTemplatesVarsCommand(parent *cli.Command) { + varsCmd := &cli.Command{ + Use: "vars ", + Short: i18n.T("cmd.vm.templates.vars.short"), + Long: i18n.T("cmd.vm.templates.vars.long"), + RunE: func(cmd *cli.Command, args []string) error { + if len(args) == 0 { + return errors.New(i18n.T("cmd.vm.error.template_required")) + } + return showTemplateVars(args[0]) + }, + } + + parent.AddCommand(varsCmd) +} + +func listTemplates() error { + templates := container.ListTemplates() + + if len(templates) == 0 { + fmt.Println(i18n.T("cmd.vm.templates.no_templates")) + return nil + } + + fmt.Printf("%s\n\n", repoNameStyle.Render(i18n.T("cmd.vm.templates.title"))) + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, i18n.T("cmd.vm.templates.header")) + _, _ = fmt.Fprintln(w, "----\t-----------") + + for _, tmpl := range templates { + desc := tmpl.Description + if len(desc) > 60 { + desc = desc[:57] + "..." + } + _, _ = fmt.Fprintf(w, "%s\t%s\n", repoNameStyle.Render(tmpl.Name), desc) + } + _ = w.Flush() + + fmt.Println() + fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.show"), dimStyle.Render("core vm templates show ")) + fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.vars"), dimStyle.Render("core vm templates vars ")) + fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.run"), dimStyle.Render("core vm run --template --var SSH_KEY=\"...\"")) + + return nil +} + +func showTemplate(name string) error { + content, err := container.GetTemplate(name) + if err != nil { + return err + } + + fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("common.label.template")), repoNameStyle.Render(name)) + fmt.Println(content) + + return nil +} + +func showTemplateVars(name string) error { + content, err := container.GetTemplate(name) + if err != nil { + return err + } + + required, optional := container.ExtractVariables(content) + + fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("common.label.template")), repoNameStyle.Render(name)) + + if len(required) > 0 { + fmt.Printf("%s\n", errorStyle.Render(i18n.T("cmd.vm.templates.vars.required"))) + for _, v := range required { + fmt.Printf(" %s\n", varStyle.Render("${"+v+"}")) + } + fmt.Println() + } + + if len(optional) > 0 { + fmt.Printf("%s\n", successStyle.Render(i18n.T("cmd.vm.templates.vars.optional"))) + for v, def := range optional { + fmt.Printf(" %s = %s\n", + varStyle.Render("${"+v+"}"), + defaultStyle.Render(def)) + } + fmt.Println() + } + + if len(required) == 0 && len(optional) == 0 { + fmt.Println(i18n.T("cmd.vm.templates.vars.none")) + } + + return nil +} + +// RunFromTemplate builds and runs a LinuxKit image from a template. +func RunFromTemplate(templateName string, vars map[string]string, runOpts container.RunOptions) error { + // Apply template with variables + content, err := container.ApplyTemplate(templateName, vars) + if err != nil { + return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "apply template"})+": %w", err) + } + + // Create a temporary directory for the build + tmpDir, err := os.MkdirTemp("", "core-linuxkit-*") + if err != nil { + return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "create temp directory"})+": %w", err) + } + defer func() { _ = os.RemoveAll(tmpDir) }() + + // Write the YAML file + yamlPath := filepath.Join(tmpDir, templateName+".yml") + if err := os.WriteFile(yamlPath, []byte(content), 0644); err != nil { + return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "write template"})+": %w", err) + } + + fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.template")), repoNameStyle.Render(templateName)) + fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.building")), yamlPath) + + // Build the image using linuxkit + outputPath := filepath.Join(tmpDir, templateName) + if err := buildLinuxKitImage(yamlPath, outputPath); err != nil { + return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "build image"})+": %w", err) + } + + // Find the built image (linuxkit creates .iso or other format) + imagePath := findBuiltImage(outputPath) + if imagePath == "" { + return errors.New(i18n.T("cmd.vm.error.no_image_found")) + } + + fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.image")), imagePath) + fmt.Println() + + // Run the image + manager, err := container.NewLinuxKitManager(io.Local) + if err != nil { + return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "initialize container manager"})+": %w", err) + } + + fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.hypervisor")), manager.Hypervisor().Name()) + fmt.Println() + + ctx := context.Background() + c, err := manager.Run(ctx, imagePath, runOpts) + if err != nil { + return fmt.Errorf(i18n.T("i18n.fail.run", "container")+": %w", err) + } + + if runOpts.Detach { + fmt.Printf("%s %s\n", successStyle.Render(i18n.T("common.label.started")), c.ID) + fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID) + fmt.Println() + fmt.Println(i18n.T("cmd.vm.hint.view_logs", map[string]any{"ID": c.ID[:8]})) + fmt.Println(i18n.T("cmd.vm.hint.stop", map[string]any{"ID": c.ID[:8]})) + } else { + fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.container_stopped")), c.ID) + } + + return nil +} + +// buildLinuxKitImage builds a LinuxKit image from a YAML file. +func buildLinuxKitImage(yamlPath, outputPath string) error { + // Check if linuxkit is available + lkPath, err := lookupLinuxKit() + if err != nil { + return err + } + + // Build the image + // linuxkit build --format iso-bios --name + cmd := exec.Command(lkPath, "build", + "--format", "iso-bios", + "--name", outputPath, + yamlPath) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +// findBuiltImage finds the built image file. +func findBuiltImage(basePath string) string { + // LinuxKit can create different formats + extensions := []string{".iso", "-bios.iso", ".qcow2", ".raw", ".vmdk"} + + for _, ext := range extensions { + path := basePath + ext + if _, err := os.Stat(path); err == nil { + return path + } + } + + // Check directory for any image file + dir := filepath.Dir(basePath) + base := filepath.Base(basePath) + + entries, err := os.ReadDir(dir) + if err != nil { + return "" + } + + for _, entry := range entries { + name := entry.Name() + if strings.HasPrefix(name, base) { + for _, ext := range []string{".iso", ".qcow2", ".raw", ".vmdk"} { + if strings.HasSuffix(name, ext) { + return filepath.Join(dir, name) + } + } + } + } + + return "" +} + +// lookupLinuxKit finds the linuxkit binary. +func lookupLinuxKit() (string, error) { + // Check PATH first + if path, err := exec.LookPath("linuxkit"); err == nil { + return path, nil + } + + // Check common locations + paths := []string{ + "/usr/local/bin/linuxkit", + "/opt/homebrew/bin/linuxkit", + } + + for _, p := range paths { + if _, err := os.Stat(p); err == nil { + return p, nil + } + } + + return "", errors.New(i18n.T("cmd.vm.error.linuxkit_not_found")) +} + +// ParseVarFlags parses --var flags into a map. +// Format: --var KEY=VALUE or --var KEY="VALUE" +func ParseVarFlags(varFlags []string) map[string]string { + vars := make(map[string]string) + + for _, v := range varFlags { + parts := strings.SplitN(v, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + // Remove surrounding quotes if present + value = strings.Trim(value, "\"'") + vars[key] = value + } + } + + return vars +} diff --git a/cmd/vm/cmd_vm.go b/cmd/vm/cmd_vm.go new file mode 100644 index 0000000..d5a00fb --- /dev/null +++ b/cmd/vm/cmd_vm.go @@ -0,0 +1,42 @@ +// Package vm provides LinuxKit VM management commands. +package vm + +import ( + "forge.lthn.ai/core/cli/pkg/cli" + "forge.lthn.ai/core/go-i18n" +) + +func init() { + cli.RegisterCommands(AddVMCommands) +} + +// Style aliases from shared +var ( + repoNameStyle = cli.RepoStyle + successStyle = cli.SuccessStyle + errorStyle = cli.ErrorStyle + dimStyle = cli.DimStyle +) + +// VM-specific styles +var ( + varStyle = cli.NewStyle().Foreground(cli.ColourAmber500) + defaultStyle = cli.NewStyle().Foreground(cli.ColourGray500).Italic() +) + +// AddVMCommands adds container-related commands under 'vm' to the CLI. +func AddVMCommands(root *cli.Command) { + vmCmd := &cli.Command{ + Use: "vm", + Short: i18n.T("cmd.vm.short"), + Long: i18n.T("cmd.vm.long"), + } + + root.AddCommand(vmCmd) + addVMRunCommand(vmCmd) + addVMPsCommand(vmCmd) + addVMStopCommand(vmCmd) + addVMLogsCommand(vmCmd) + addVMExecCommand(vmCmd) + addVMTemplatesCommand(vmCmd) +} diff --git a/go.mod b/go.mod index 892b5a1..1dcea8e 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,56 @@ module forge.lthn.ai/core/go-container go 1.26.0 require ( + forge.lthn.ai/core/cli v0.1.0 forge.lthn.ai/core/go-config v0.1.0 + forge.lthn.ai/core/go-i18n v0.1.0 forge.lthn.ai/core/go-io v0.0.3 github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 ) require ( + forge.lthn.ai/core/go v0.1.0 // indirect + forge.lthn.ai/core/go-crypt v0.0.3 // indirect + forge.lthn.ai/core/go-inference v0.0.2 // indirect + forge.lthn.ai/core/go-log v0.0.1 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/kr/text v0.2.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.20 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect ) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8537f76 --- /dev/null +++ b/go.sum @@ -0,0 +1,118 @@ +forge.lthn.ai/core/cli v0.1.0 h1:2XRiEMVzUElnQlZnHYDyfKIKQVPcCzGuYHlnz55GjsM= +forge.lthn.ai/core/cli v0.1.0/go.mod h1:mZ7dzccfzo0BP2dE7Mwuw9dXuIowiEd1G5ZGMoLuxVc= +forge.lthn.ai/core/go v0.1.0 h1:Ow/1NTajrrNPO0zgkskEyEGdx4SKpiNqTaqM0txNOYI= +forge.lthn.ai/core/go v0.1.0/go.mod h1:lwi0tccAlg5j3k6CfoNJEueBc5l9mUeSBX/x6uY8ZbQ= +forge.lthn.ai/core/go-config v0.1.0 h1:bQnlt8MvFvgPisl//jw4IMHMoCcaIt5FLurwYWqlMx0= +forge.lthn.ai/core/go-config v0.1.0/go.mod h1:jsCzg3BykHqlHZs13PDhP/dq8yTZjsiEyZ35q6jA3Aw= +forge.lthn.ai/core/go-crypt v0.0.3 h1:KG5dQstPfcohIitZJRF7jEdR4H1gjb4YrxjkzIQ8CGE= +forge.lthn.ai/core/go-crypt v0.0.3/go.mod h1:BFHULU7hJBXkg4EXDO62pZvpUctzrzrW9x8gJEaBKX8= +forge.lthn.ai/core/go-i18n v0.1.0 h1:F7JVSoVkZtzx9JfhpntM9z3iQm1vnuMUi/Zklhz8PCI= +forge.lthn.ai/core/go-i18n v0.1.0/go.mod h1:Q4xsrxuNCl/6NfMv1daria7t1RSiyy8ml+6jiPtUcBs= +forge.lthn.ai/core/go-inference v0.0.2 h1:aHjBkYyLKxLr9tbO4AvzzV/lsZueGq/jeo33SLh113k= +forge.lthn.ai/core/go-inference v0.0.2/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= +forge.lthn.ai/core/go-io v0.0.3 h1:TlhYpGTyjPgAlbEHyYrVSeUChZPhJXcLZ7D/8IbFqfI= +forge.lthn.ai/core/go-io v0.0.3/go.mod h1:ZlU9OQpsvNFNmTJoaHbFIkisZyc0eCq0p8znVWQLRf0= +forge.lthn.ai/core/go-log v0.0.1 h1:x/E6EfF9vixzqiLHQOl2KT25HyBcMc9qiBkomqVlpPg= +forge.lthn.ai/core/go-log v0.0.1/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=