Attempt 2 of a management ui.
This commit is contained in:
54 files changed
+8804
-267
No files matched your search
@@ -0,0 +1,74 @@
|
||||
import { readdir, readFile, access } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { composeStatus, type StackRunState } from "./docker";
|
||||
import { RESERVED_SLUGS } from "./slug";
|
||||
|
||||
export { isValidSlug, suggestSlug } from "./slug";
|
||||
|
||||
export interface BusinessStackSummary {
|
||||
slug: string;
|
||||
domain: string | null;
|
||||
containerName: string;
|
||||
status: StackRunState;
|
||||
}
|
||||
|
||||
export function stacksRoot(): string {
|
||||
const dir = process.env.STACKS_DIR;
|
||||
if (!dir) throw new Error("STACKS_DIR must be set.");
|
||||
return dir;
|
||||
}
|
||||
|
||||
export function stackDir(slug: string): string {
|
||||
return path.join(stacksRoot(), slug);
|
||||
}
|
||||
|
||||
/** Only ever reads the DOMAIN line — deliberately never returns the rest of a business's .env (database password, AUTH_SECRET, etc.) to the UI. */
|
||||
async function readDomain(envPath: string): Promise<string | null> {
|
||||
try {
|
||||
const content = await readFile(envPath, "utf8");
|
||||
for (const line of content.split("\n")) {
|
||||
const match = /^DOMAIN\s*=\s*"?([^"\n]*)"?\s*$/.exec(line.trim());
|
||||
if (match?.[1]) return match[1];
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listBusinessStacks(): Promise<BusinessStackSummary[]> {
|
||||
const root = stacksRoot();
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = await readdir(root);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stacks: BusinessStackSummary[] = [];
|
||||
for (const slug of entries) {
|
||||
if (RESERVED_SLUGS.has(slug)) continue;
|
||||
const dir = path.join(root, slug);
|
||||
const composePath = path.join(dir, "compose.yaml");
|
||||
try {
|
||||
await access(composePath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const domain = await readDomain(path.join(dir, ".env"));
|
||||
const status = await composeStatus(dir);
|
||||
stacks.push({ slug, domain, containerName: `next-chair-${slug}`, status });
|
||||
}
|
||||
|
||||
return stacks.sort((a, b) => a.slug.localeCompare(b.slug));
|
||||
}
|
||||
|
||||
export async function stackExists(slug: string): Promise<boolean> {
|
||||
try {
|
||||
await access(path.join(stackDir(slug), "compose.yaml"));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user