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:
@@ -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();
|
||||
});
|
||||
});
|
||||
90
packages/cli/src/commands/ensure-project-registered.ts
Normal file
90
packages/cli/src/commands/ensure-project-registered.ts
Normal 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})`);
|
||||
}
|
||||
Reference in New Issue
Block a user