feat(FN-4266): complete Step 1 — add shared auto-registration helper

Ref: Runfusion/Fusion#216
Fusion-Task-Id: FN-4266
Fusion-Task-Lineage: 4105d2eb-f3d8-4297-9b49-bf7e5a2906be
This commit is contained in:
Fusion
2026-05-12 22:49:16 -07:00
committed by gsxdsm
parent 8e0f3d448d
commit 366816ea79
2 changed files with 223 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
import { mkdtempSync, existsSync, rmSync, statSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { CentralCore } from "@fusion/core";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ensureCwdProjectRegistered } from "../ensure-project-registered.js";
const tempPaths: string[] = [];
function makeTempDir(prefix: string): string {
const path = mkdtempSync(join(tmpdir(), prefix));
tempPaths.push(path);
return path;
}
afterEach(() => {
for (const path of tempPaths.splice(0)) {
rmSync(path, { recursive: true, force: true });
}
vi.restoreAllMocks();
});
describe("ensureCwdProjectRegistered", () => {
it("returns existing registered project without writing files", async () => {
const globalDir = makeTempDir("fn-4266-global-");
const cwd = makeTempDir("fn-4266-project-");
const central = new CentralCore(globalDir);
await central.init();
const existing = await central.registerProject({
name: "existing-project",
path: cwd,
isolationMode: "in-process",
});
const registerSpy = vi.spyOn(central, "registerProject");
const updateSpy = vi.spyOn(central, "updateProject");
const result = await ensureCwdProjectRegistered({
cwd,
central,
logPrefix: "serve",
autoRegister: true,
});
expect(result?.id).toBe(existing.id);
expect(existsSync(join(cwd, ".fusion"))).toBe(false);
expect(registerSpy).not.toHaveBeenCalled();
expect(updateSpy).not.toHaveBeenCalled();
await central.close();
});
it("auto-registers unregistered project when enabled", async () => {
const globalDir = makeTempDir("fn-4266-global-");
const cwd = makeTempDir("fn-4266-project-");
const central = new CentralCore(globalDir);
await central.init();
const registerSpy = vi.spyOn(central, "registerProject");
const updateSpy = vi.spyOn(central, "updateProject");
const result = await ensureCwdProjectRegistered({
cwd,
central,
logPrefix: "serve",
autoRegister: true,
});
expect(result).not.toBeNull();
expect(existsSync(join(cwd, ".fusion"))).toBe(true);
expect(existsSync(join(cwd, ".fusion", "fusion.db"))).toBe(true);
expect(statSync(join(cwd, ".fusion", "fusion.db")).size).toBe(0);
expect(registerSpy).toHaveBeenCalledWith(
expect.objectContaining({
path: cwd,
isolationMode: "in-process",
}),
);
expect(updateSpy).toHaveBeenCalledWith(expect.any(String), { status: "active" });
await central.close();
});
it("returns null and does not write when autoRegister is false", async () => {
const globalDir = makeTempDir("fn-4266-global-");
const cwd = makeTempDir("fn-4266-project-");
const central = new CentralCore(globalDir);
await central.init();
const registerSpy = vi.spyOn(central, "registerProject");
const result = await ensureCwdProjectRegistered({
cwd,
central,
logPrefix: "daemon",
autoRegister: false,
});
expect(result).toBeNull();
expect(existsSync(join(cwd, ".fusion"))).toBe(false);
expect(registerSpy).not.toHaveBeenCalled();
await central.close();
});
it("returns null and logs error when registration throws", async () => {
const globalDir = makeTempDir("fn-4266-global-");
const cwd = makeTempDir("fn-4266-project-");
const central = new CentralCore(globalDir);
await central.init();
vi.spyOn(central, "registerProject").mockRejectedValueOnce(new Error("boom"));
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const result = await ensureCwdProjectRegistered({
cwd,
central,
logPrefix: "serve",
autoRegister: true,
});
expect(result).toBeNull();
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining("[serve] Failed to auto-register current project: boom"),
);
await central.close();
});
});

View File

@@ -0,0 +1,90 @@
import { exec } from "node:child_process";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { basename, join } from "node:path";
import { promisify } from "node:util";
import type { CentralCore, RegisteredProject } from "@fusion/core";
const execAsync = promisify(exec);
export interface EnsureCwdProjectRegisteredOptions {
cwd: string;
central: CentralCore;
logPrefix: string;
autoRegister: boolean;
}
export async function ensureCwdProjectRegistered(
options: EnsureCwdProjectRegisteredOptions,
): Promise<RegisteredProject | null> {
const { cwd, central, logPrefix, autoRegister } = options;
const existing = await central.getProjectByPath(cwd);
if (existing) {
return existing;
}
if (!autoRegister) {
logManualRegistrationHint(logPrefix, cwd);
return null;
}
try {
const fusionDir = join(cwd, ".fusion");
const dbPath = join(fusionDir, "fusion.db");
if (!existsSync(fusionDir)) {
mkdirSync(fusionDir, { recursive: true });
}
if (!existsSync(dbPath)) {
writeFileSync(dbPath, "");
}
const projectName = await detectProjectName(cwd);
const project = await central.registerProject({
name: projectName,
path: cwd,
isolationMode: "in-process",
});
await central.updateProject(project.id, { status: "active" });
console.log(`[${logPrefix}] Auto-registered project "${project.name}" at ${cwd}`);
return project;
} catch (error) {
console.error(
`[${logPrefix}] Failed to auto-register current project: ${error instanceof Error ? error.message : String(error)}`,
);
logManualRegistrationHint(logPrefix, cwd);
return null;
}
}
async function detectProjectName(dir: string): Promise<string> {
if (!existsSync(join(dir, ".git"))) {
return basename(dir) || "my-project";
}
try {
const { stdout: remoteUrl } = await execAsync("git remote get-url origin", {
cwd: dir,
timeout: 10_000,
});
const trimmed = remoteUrl.trim();
if (trimmed) {
const match = trimmed.match(/[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
if (match) {
return match[2];
}
}
} catch {
// ignore
}
return basename(dir) || "my-project";
}
function logManualRegistrationHint(logPrefix: string, cwd: string): void {
console.error(`[${logPrefix}] Run 'fn init' to register this project, or 'fn project add <name> <path>' (${cwd})`);
}