Merge pull request #1463 from Runfusion/gsxdsm/issue-1455-initialize-git-repository

fix(FN-1455): initialize git during project registration
This commit is contained in:
gsxdsm
2026-06-06 01:08:31 -07:00
committed by GitHub
19 changed files with 787 additions and 14 deletions

View File

@@ -70,6 +70,7 @@ describe("ensureCwdProjectRegistered", () => {
});
expect(result).not.toBeNull();
expect(existsSync(join(cwd, ".git"))).toBe(true);
expect(existsSync(join(cwd, ".fusion"))).toBe(true);
expect(existsSync(join(cwd, ".fusion", "fusion.db"))).toBe(true);
expect(ensureSpy).toHaveBeenCalledWith(
@@ -156,6 +157,7 @@ describe("ensureCwdProjectRegistered", () => {
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining("[serve] Failed to auto-register current project: boom"),
);
expect(readProjectIdentity(cwd)).toBeNull();
await central.close();
});

View File

@@ -9,6 +9,7 @@ import { join } from "node:path";
import { runInit } from "../init.js";
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { GitRepositoryInitializationError } from "@fusion/core";
const execAsync = promisify(exec);
@@ -158,6 +159,14 @@ describe("init command", () => {
);
});
it("propagates Git initialization failures instead of reporting local init success", async () => {
const error = new GitRepositoryInitializationError(tempProjectDir, "git is not installed");
mockEnsureProjectForPath.mockRejectedValueOnce(error);
await expect(runInit({ path: tempProjectDir })).rejects.toBe(error);
expect(mockCentralClose).toHaveBeenCalled();
});
it("should be idempotent - report already initialized", async () => {
// First init
await runInit({ path: tempProjectDir });
@@ -358,7 +367,7 @@ describe("init command", () => {
expect(Number(commitCount)).toBe(1);
});
it("does not create git repository without --git and logs a hint", async () => {
it("delegates registration without --git and does not log a manual git hint", async () => {
const originalLog = console.log;
const logs: string[] = [];
console.log = (...args: unknown[]) => {
@@ -372,6 +381,41 @@ describe("init command", () => {
}
expect(existsSync(join(tempProjectDir, ".git"))).toBe(false);
expect(logs.join("\n")).toContain("Not a git repository. Run 'fn init --git' to auto-initialize one.");
expect(mockEnsureProjectForPath).toHaveBeenCalledWith(
expect.objectContaining({
path: tempProjectDir,
}),
);
expect(logs.join("\n")).not.toContain("Not a git repository");
});
it("logs when shared registration initializes git without --git", async () => {
mockEnsureProjectForPath.mockResolvedValueOnce({
outcome: "registered",
gitRepository: "initialized",
project: {
id: "proj_test",
name: "test-project",
path: tempProjectDir,
isolationMode: "in-process",
status: "initializing",
createdAt: "",
updatedAt: "",
},
});
const originalLog = console.log;
const logs: string[] = [];
console.log = (...args: unknown[]) => {
logs.push(args.join(" "));
};
try {
await runInit({ path: tempProjectDir });
} finally {
console.log = originalLog;
}
expect(logs.join("\n")).toContain("Initialized git repository");
});
});

View File

@@ -164,7 +164,12 @@ describe("project commands", () => {
const { runProjectAdd } = await import("../project.js");
await runProjectAdd("demo", ".", { force: true });
expect(mockRegisterProject).toHaveBeenCalled();
expect(mockEnsureProjectForPath).toHaveBeenCalledWith(
expect.objectContaining({
name: "demo",
path: process.cwd(),
}),
);
const lines = consoleSpy.mock.calls.map((call) => String(call[0]));
expect(lines.some((line) => line.includes("Registered project 'demo'"))).toBe(true);
expect(lines.some((line) => line.includes("Location:"))).toBe(true);
@@ -339,9 +344,9 @@ describe("project commands", () => {
expect(output).toContain("Completed: 10");
});
it("validation exits on missing required args for runProjectAdd", async () => {
it("validation exits on invalid project name for runProjectAdd", async () => {
const { runProjectAdd } = await import("../project.js");
await expect(runProjectAdd("", "/tmp")).rejects.toThrow("process.exit:1");
await expect(runProjectAdd("bad name", "/tmp")).rejects.toThrow("process.exit:1");
});
it("validation exits on missing required args for runProjectRemove", async () => {
@@ -398,6 +403,32 @@ describe("project commands", () => {
expect(output).toContain("Memory: initialized");
});
it("shows git initialized message when shared registration creates a git repository", async () => {
mockListProjects.mockResolvedValue([]);
mockRegisterProject.mockResolvedValue({
id: "proj-1",
name: "demo",
path: "/fake/demo",
isolationMode: "in-process",
});
mockEnsureProjectForPath.mockResolvedValueOnce({
outcome: "registered",
gitRepository: "initialized",
project: {
id: "proj-1",
name: "demo",
path: "/fake/demo",
isolationMode: "in-process",
},
});
const { runProjectAdd } = await import("../project.js");
await runProjectAdd("demo", testPath, { force: true });
const output = consoleSpy.mock.calls.map((call) => String(call[0])).join("\n");
expect(output).toContain("Git: initialized");
});
it("does not show memory message when memory files already exist", async () => {
mockListProjects.mockResolvedValue([]);
mockRegisterProject.mockResolvedValue({

View File

@@ -15,6 +15,7 @@ import { promisify } from "node:util";
const execAsync = promisify(exec);
import {
CentralCore,
GitRepositoryInitializationError,
QMD_INSTALL_COMMAND,
isQmdAvailable,
isValidSqliteDatabaseFile,
@@ -105,12 +106,9 @@ export async function runInit(options: InitOptions = {}): Promise<void> {
console.log(` ✓ Created .fusion/ directory`);
}
const hasGitRepo = await isGitRepo(cwd);
if (!hasGitRepo && options.git) {
if (options.git && !(await isGitRepo(cwd))) {
await initializeGitRepo(cwd);
console.log(` ✓ Initialized git repository`);
} else if (!hasGitRepo) {
console.log(` ⚠ Not a git repository. Run 'fn init --git' to auto-initialize one.`);
}
// Add local Fusion/Pi storage directories to .gitignore
@@ -169,6 +167,9 @@ export async function runInit(options: InitOptions = {}): Promise<void> {
maybeInstallClaudeSkillForNewProject(cwd);
if (ensured.gitRepository === "initialized") {
console.log(` ✓ Initialized git repository`);
}
console.log(` ✓ Registered in central database`);
console.log(`\n✓ Project "${project.name}" initialized successfully!`);
console.log(`\n Next steps:`);
@@ -178,6 +179,10 @@ export async function runInit(options: InitOptions = {}): Promise<void> {
await central.close();
} catch (err) {
if (err instanceof GitRepositoryInitializationError) {
await central.close();
throw err;
}
// If central DB registration fails, still report success since local files are created
console.log(` ⚠ Could not register in central database: ${(err as Error).message}`);
console.log(`\n✓ Project initialized locally (central registration can be done later)`);

View File

@@ -394,6 +394,9 @@ export async function runProjectAdd(
console.log(` Location: ${formatDisplayPath(project.path)}`);
console.log(` ID: ${project.id}`);
console.log(` Isolation: ${project.isolationMode}`);
if (ensured.gitRepository === "initialized") {
console.log(` Git: initialized`);
}
if (memoryInitialized) {
console.log(` Memory: initialized`);
}