Add homelab installer and services (allprox, authelia, lldap)
This commit is contained in:
parent
68b23f1cbe
commit
bb754cdd8c
32 changed files with 2356 additions and 1 deletions
196
installer/src/tui.ts
Normal file
196
installer/src/tui.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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}`));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue