// Package cli — mode CLI pendamping (F-028). // Seluruh logika inti di-share dengan TUI; paket ini hanya presentasi + kode keluar. package cli import ( "context" "fmt" "io" "os" "runtime" "time" "gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/audio" "gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/config" "gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/docker" "gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/i18n" "gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/stack" ) // Doctor memeriksa kesehatan sistem dan menulis laporan ke w. // Kode keluar: 0 = sehat (warning boleh), 1 = ada kegagalan fatal. func Doctor(w io.Writer) int { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() fail := 0 // Konfigurasi dimuat lebih dulu — host & compose dipakai di bawah. cfg, cfgErr := config.Load() // Engine Docker/Podman — dependency inti (auto-detect bila host kosong). engineName := "" switch cl, engine, err := docker.Detect(cfg.Docker.Host); { case err != nil: fmt.Fprintf(w, "engine : %s %v\n", i18n.T("error.fail"), err) fail++ default: defer cl.Close() engineName = engine.Name host := engine.Host if host == "" { if env := os.Getenv("DOCKER_HOST"); env != "" { host = env } else { host = i18n.T("doctor.hostDefault") } } if info, err := cl.Info(ctx); err != nil { fmt.Fprintf(w, "engine : %s %s %v\n", i18n.T("error.warn"), engine.Name, err) } else { fmt.Fprintf(w, "engine : %s %s\n", i18n.T("error.ok"), i18n.Tf("doctor.engineOk", engine.Name, info.ServerVersion, info.APIVersion, info.Running, info.Containers, info.Images)) } fmt.Fprintf(w, "socket : %s\n", host) } // Audio — opsional; engine init async, tunggu maks 2 dtk. eng := audio.New(audio.Options{Enabled: true, Volume: 70, MuteOnSSH: true}) eng.WaitReady(2 * time.Second) fmt.Fprintf(w, "audio : %s\n", eng.Status()) fmt.Fprintf(w, "go : %s (%s/%s)\n", runtime.Version(), runtime.GOOS, runtime.GOARCH) // Compose — dipakai operasi stack (up/down/pull); "auto" mengikuti engine. compose := docker.NewCompose(cfg.Docker.ComposeCommand, engineName) if ver, err := compose.Version(ctx); err != nil { fmt.Fprintf(w, "compose : %s %v\n", i18n.T("error.warn"), err) } else { fmt.Fprintf(w, "compose : %s %s\n", i18n.T("error.ok"), i18n.Tf("doctor.composeOk", ver, compose.Command())) } cfgPath := config.Path() switch { case cfgErr != nil: fmt.Fprintf(w, "config : %s %v\n", i18n.T("error.warn"), cfgErr) default: if _, err := os.Stat(cfgPath); err != nil { fmt.Fprintf(w, "config : %s\n", i18n.Tf("doctor.configMissing", cfgPath)) } else { fmt.Fprintf(w, "config : %s\n", cfgPath) } } stacks := config.ExpandPath(cfg.StacksDir) if st, err := os.Stat(stacks); err == nil && st.IsDir() { n := 0 if list, err := stack.ScanFS(stacks); err == nil { n = len(list) } fmt.Fprintf(w, "stacks : %s\n", i18n.Tf("doctor.stacksCount", stacks, n)) } else { fmt.Fprintf(w, "stacks : %s\n", i18n.Tf("doctor.stacksMissing", stacks)) } if fail > 0 { return 1 } return 0 }