Files
fusion/packages/dashboard/src/devserver-persistence.ts
Fusion e24c87866d feat(FN-2183): add dev server management modules
- Add typed dev server models and constants for configs, session state, runtime metadata, and bounded log history
- Add JSON persistence helpers for .fusion/devserver.json with tolerant parsing and session reconstruction
- Add dev command auto-detection across root and workspace package.json scripts with framework inference and priority sorting
- Implement DevServerManager lifecycle controls, preview URL detection, port probing fallback, and singleton manager helpers
- Add Vitest coverage for devserver types, detection/persistence behavior, and manager lifecycle/events
2026-04-20 22:45:26 -07:00

101 lines
2.8 KiB
TypeScript

import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import {
createDevServerId,
type DevServerConfig,
type DevServerSession,
} from "./devserver-types.js";
interface PersistenceData {
configs: DevServerConfig[];
}
export function projectDevServerFile(projectDir: string): string {
return join(resolve(projectDir), ".fusion", "devserver.json");
}
function parseEnv(value: unknown): Record<string, string> | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
const parsed: Record<string, string> = {};
for (const [key, candidate] of Object.entries(value)) {
if (typeof candidate === "string") {
parsed[key] = candidate;
}
}
return Object.keys(parsed).length > 0 ? parsed : undefined;
}
function parseConfig(candidate: unknown): DevServerConfig | null {
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
return null;
}
const value = candidate as Record<string, unknown>;
if (typeof value.id !== "string") {
return null;
}
if (typeof value.name !== "string" || typeof value.command !== "string" || typeof value.cwd !== "string") {
return null;
}
return {
id: createDevServerId(value.id),
name: value.name,
command: value.command,
cwd: value.cwd,
env: parseEnv(value.env),
autoStart: typeof value.autoStart === "boolean" ? value.autoStart : undefined,
};
}
export async function loadDevServerConfigs(projectDir: string): Promise<DevServerConfig[]> {
const filePath = projectDevServerFile(projectDir);
try {
const raw = await readFile(filePath, "utf-8");
const parsed = JSON.parse(raw) as Partial<PersistenceData>;
if (!Array.isArray(parsed.configs)) {
return [];
}
return parsed.configs
.map((config) => parseConfig(config))
.filter((config): config is DevServerConfig => config !== null);
} catch {
return [];
}
}
export async function saveDevServerConfigs(projectDir: string, configs: DevServerConfig[]): Promise<void> {
const filePath = projectDevServerFile(projectDir);
const folder = dirname(filePath);
try {
await mkdir(folder, { recursive: true });
const payload: PersistenceData = { configs };
await writeFile(filePath, JSON.stringify(payload, null, 2), "utf-8");
} catch {
// Graceful no-op: callers can continue operating with in-memory state.
}
}
export function reconstructSessions(configs: DevServerConfig[]): Map<string, DevServerSession> {
const sessions = new Map<string, DevServerSession>();
for (const config of configs) {
sessions.set(config.id, {
config,
status: "stopped",
logHistory: [],
});
}
return sessions;
}
export type { PersistenceData };