import { confirm, intro, isCancel, multiselect, outro, select, spinner, } from "@clack/prompts"; import type { ServiceMetadata } from "./types.ts"; import { describeSource, type Config } from "./config.ts"; import { installService, loadCatalog, printAvailable, printStatus, resolveEnv, uninstallService, updateService, } from "./commands.ts"; import { readEnvFile } from "./env.ts"; import { serviceDir } from "./paths.ts"; import { loadState } from "./state.ts"; import * as ui from "./util.ts"; export async function runTui(cfg: Config): Promise { ui.banner(); intro(ui.cyan("Homelab Installer")); ui.info(`Source: ${describeSource(cfg)}`); let running = true; while (running) { const action = await select({ message: "What would you like to do?", options: [ { value: "install", label: "Install services", hint: "add new services" }, { value: "update", label: "Update services", hint: "pull newer versions" }, { value: "uninstall", label: "Uninstall services", hint: "stop and remove" }, { value: "list", label: "List available services" }, { value: "status", label: "Show installed services" }, { value: "exit", label: "Exit" }, ], }); if (isCancel(action)) { running = false; break; } switch (action) { case "install": await tuiInstall(cfg); break; case "update": await tuiUpdate(cfg); break; case "uninstall": await tuiUninstall(cfg); break; case "list": await printAvailable(cfg); break; case "status": printStatus(); break; case "exit": running = false; break; default: break; } } outro(ui.green("Goodbye!")); } async function tuiInstall(cfg: Config): Promise { const metas = await loadCatalog(cfg); if (metas.length === 0) { ui.warn("No services found in the catalog."); return; } const state = loadState(); const choices = metas .filter((m) => !state.services[m.id]) .map((m) => ({ value: m.id, label: `${m.name} (${m.version})`, hint: m.category, })); if (choices.length === 0) { ui.info("All available services are already installed."); return; } const selected = await multiselect({ message: "Select services to install (space to toggle, enter to confirm)", options: choices, required: true, }); if (isCancel(selected)) return; const byId = new Map(metas.map((m) => [m.id, m])); for (const id of selected as string[]) { const meta = byId.get(id); if (!meta) continue; await runWithSpinner(`Installing ${meta.name}...`, meta, cfg, async () => { const vars = await resolveEnv(meta, cfg); return installService(meta, cfg, vars); }); } } async function tuiUpdate(cfg: Config): Promise { const state = loadState(); const installed = Object.values(state.services); if (installed.length === 0) { ui.info("No services installed yet."); return; } const metas = await loadCatalog(cfg); const byId = new Map(metas.map((m) => [m.id, m])); const options = installed.map((s) => { const meta = byId.get(s.id); const newer = meta && meta.version !== s.version; return { value: s.id, label: `${s.name} (${s.version})`, hint: newer && meta ? `update to ${meta.version}` : "up to date", }; }); const selected = await multiselect({ message: "Select services to update", options, required: true, }); if (isCancel(selected)) return; for (const id of selected as string[]) { const meta = byId.get(id); const installedService = state.services[id]; if (!meta || !installedService) continue; await runWithSpinner(`Updating ${meta.name}...`, meta, cfg, async () => { const vars = await resolveEnv(meta, cfg, readEnvFile(serviceDir(id))); return updateService(installedService, meta, cfg, vars); }); } } async function tuiUninstall(cfg: Config): Promise { const state = loadState(); const installed = Object.values(state.services); if (installed.length === 0) { ui.info("No services installed yet."); return; } const selected = await multiselect({ message: "Select services to uninstall", options: installed.map((s) => ({ value: s.id, label: s.name, hint: s.version })), required: true, }); if (isCancel(selected)) return; const confirmed = await confirm({ message: "Uninstall selected services? (data volumes are preserved)", }); if (isCancel(confirmed) || !confirmed) return; for (const id of selected as string[]) { await runWithSpinner(`Uninstalling ${id}...`, null, cfg, async () => { return uninstallService(id, cfg); }); } } async function runWithSpinner( label: string, meta: ServiceMetadata | null, cfg: Config, fn: () => Promise<{ message: string }>, ): Promise { const s = spinner(); s.start(label); try { const result = await fn(); s.stop(ui.green(result.message)); } catch (err) { s.stop(ui.red(`Failed: ${(err as Error).message}`)); } }