repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/bak/state.ts
Rawimport { 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);
}