Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/bak/state.ts

Raw
import { randomUUID } from "node:crypto";
import {
	mkdir,
	readdir,
	readFile,
	rename,
	unlink,
	writeFile,
} from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join, posix, win32 } from "node:path";

export interface HostIdentity {
	hostId: string;
	alias: string;
}

export interface SessionRecord {
	id: string;
	key: string;
	name?: string;
	cwd: string;
	created: string;
}

export interface HostIndex extends HostIdentity {
	version: 1;
	sessions: SessionRecord[];
}

export interface HostCatalog {
	version: 1;
	hosts: HostIdentity[];
}

export interface CatalogCache {
	refreshedAt: string;
	catalog: HostCatalog;
	indexes: HostIndex[];
}

export interface StatePaths {
	identity: string;
	cache: string;
	pendingDir: string;
}

function platformRoot(
	kind: "state" | "cache",
	env: NodeJS.ProcessEnv,
	platform: NodeJS.Platform,
	home: string,
): string {
	if (platform === "win32") {
		const paths = win32;
		const override = kind === "state" ? env.XDG_STATE_HOME : env.XDG_CACHE_HOME;
		if (override?.trim()) return paths.join(override, "pi", "bak");
		return paths.join(
			env.LOCALAPPDATA ?? paths.join(home, "AppData", "Local"),
			"pi",
			"bak",
			kind,
		);
	}
	if (platform === "darwin") {
		const override = kind === "state" ? env.XDG_STATE_HOME : env.XDG_CACHE_HOME;
		if (override?.trim()) return posix.join(override, "pi", "bak");
		return posix.join(
			home,
			"Library",
			kind === "state" ? "Application Support" : "Caches",
			"pi",
			"bak",
		);
	}
	const override = kind === "state" ? env.XDG_STATE_HOME : env.XDG_CACHE_HOME;
	const fallback =
		kind === "state"
			? posix.join(home, ".local", "state")
			: posix.join(home, ".cache");
	return posix.join(override?.trim() || fallback, "pi", "bak");
}

export function statePaths(
	env: NodeJS.ProcessEnv = process.env,
	platform: NodeJS.Platform = process.platform,
	home = homedir(),
): StatePaths {
	const paths = platform === "win32" ? win32 : posix;
	const state = platformRoot("state", env, platform, home);
	return {
		identity: paths.join(state, "identity.json"),
		cache: paths.join(
			platformRoot("cache", env, platform, home),
			"catalog.json",
		),
		pendingDir: paths.join(state, "pending"),
	};
}

export function normalizeAlias(value: string): string | undefined {
	const alias = value.trim().toLowerCase();
	return /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/.test(alias)
		? alias
		: undefined;
}

export function newIdentity(alias: string): HostIdentity {
	return { hostId: randomUUID(), alias };
}

export async function readJson<T>(path: string): Promise<T | undefined> {
	try {
		return JSON.parse(await readFile(path, "utf8")) as T;
	} catch (error) {
		if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
		throw error;
	}
}

export interface PendingEntry {
	id: string;
	path: string;
}

function pendingMarker(directory: string, id: string): string {
	return join(directory, `${id}.json`);
}

export async function markPending(
	directory: string,
	path: string,
): Promise<PendingEntry> {
	const entry = { id: randomUUID(), path };
	await atomicWriteJson(pendingMarker(directory, entry.id), entry);
	return entry;
}

export async function clearPending(
	directory: string,
	id: string,
): Promise<void> {
	await unlink(pendingMarker(directory, id)).catch((error) => {
		if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
	});
}

export async function readPending(directory: string): Promise<PendingEntry[]> {
	let names: string[];
	try {
		names = await readdir(directory);
	} catch (error) {
		if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
		throw error;
	}
	const entries: PendingEntry[] = [];
	for (const name of names) {
		if (!name.endsWith(".json")) continue;
		const marker = await readJson<{ id?: unknown; path?: unknown }>(
			join(directory, name),
		);
		if (
			typeof marker?.id === "string" &&
			`${marker.id}.json` === name &&
			typeof marker.path === "string"
		)
			entries.push({ id: marker.id, path: marker.path });
	}
	return entries;
}

export async function writeJsonExclusive(
	path: string,
	value: unknown,
): Promise<boolean> {
	await mkdir(dirname(path), { recursive: true, mode: 0o700 });
	try {
		await writeFile(path, `${JSON.stringify(value)}\n`, {
			mode: 0o600,
			flag: "wx",
		});
		return true;
	} catch (error) {
		if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
		throw error;
	}
}

export async function atomicWriteJson(
	path: string,
	value: unknown,
): Promise<void> {
	await mkdir(dirname(path), { recursive: true, mode: 0o700 });
	const temporary = join(dirname(path), `.${process.pid}-${randomUUID()}.tmp`);
	await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
	await rename(temporary, path);
}