feat(FN-5411): add project identity recovery and identity-aware startup rea
Implements project identity tracking and recovery across the Fusion system (FN-5411), enabling persistent identity for projects across storage migrations, daemon reattaches, and CLI session management. Adds a project identity metadata API and central reattach ensure mechanism, wires identity stampin Fusion-Task-Id: FN-5411 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> Fusion-Task-Id: FN-5411
This commit is contained in:
@@ -22,6 +22,18 @@ vi.mock("@fusion/core", async () => {
|
||||
getProject = vi.fn().mockResolvedValue(undefined);
|
||||
getProjectByPath = vi.fn().mockResolvedValue(undefined);
|
||||
registerProject = vi.fn();
|
||||
ensureProjectForPath = vi.fn().mockImplementation(async ({ path, name }: { path: string; name?: string }) => ({
|
||||
outcome: "registered",
|
||||
project: {
|
||||
id: "proj_1234567890abcdef",
|
||||
name: name ?? "project",
|
||||
path,
|
||||
status: "initializing",
|
||||
isolationMode: "in-process",
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||
},
|
||||
}));
|
||||
updateProject = vi.fn().mockResolvedValue({});
|
||||
unregisterProject = vi.fn().mockResolvedValue(undefined);
|
||||
getProjectHealth = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -33,6 +45,8 @@ vi.mock("@fusion/core", async () => {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
listTasks: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
readProjectIdentity: vi.fn().mockReturnValue(undefined),
|
||||
writeProjectIdentity: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -156,6 +156,14 @@ const mocks = vi.hoisted(() => {
|
||||
projects.push(project);
|
||||
return Promise.resolve(project);
|
||||
}),
|
||||
ensureProjectForPath: vi.fn().mockImplementation(async ({ path, name, isolationMode }: { path: string; name?: string; isolationMode?: "in-process" | "child-process" }) => ({
|
||||
outcome: "registered",
|
||||
project: await instance.registerProject({
|
||||
name: name ?? "unnamed",
|
||||
path,
|
||||
isolationMode: isolationMode ?? "in-process",
|
||||
}),
|
||||
})),
|
||||
updateProject: vi.fn().mockImplementation((id: string, patch: { status?: string }) => {
|
||||
const index = projects.findIndex((project) => project.id === id);
|
||||
if (index >= 0) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mkdtempSync, existsSync, rmSync, statSync } from "node:fs";
|
||||
import { mkdtempSync, existsSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { CentralCore } from "@fusion/core";
|
||||
import { CentralCore, readProjectIdentity } from "@fusion/core";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ensureCwdProjectRegistered } from "../ensure-project-registered.js";
|
||||
|
||||
@@ -44,21 +44,22 @@ describe("ensureCwdProjectRegistered", () => {
|
||||
});
|
||||
|
||||
expect(result?.id).toBe(existing.id);
|
||||
expect(existsSync(join(cwd, ".fusion"))).toBe(false);
|
||||
expect(existsSync(join(cwd, ".fusion"))).toBe(true);
|
||||
expect(readProjectIdentity(cwd)?.id).toBe(existing.id);
|
||||
expect(registerSpy).not.toHaveBeenCalled();
|
||||
expect(updateSpy).not.toHaveBeenCalled();
|
||||
|
||||
await central.close();
|
||||
});
|
||||
|
||||
it("auto-registers unregistered project when enabled", async () => {
|
||||
it("auto-registers unregistered project when enabled and persists identity", 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 ensureSpy = vi.spyOn(central, "ensureProjectForPath");
|
||||
const updateSpy = vi.spyOn(central, "updateProject");
|
||||
|
||||
const result = await ensureCwdProjectRegistered({
|
||||
@@ -71,14 +72,42 @@ describe("ensureCwdProjectRegistered", () => {
|
||||
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(ensureSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
path: cwd,
|
||||
isolationMode: "in-process",
|
||||
}),
|
||||
);
|
||||
expect(updateSpy).toHaveBeenCalledWith(expect.any(String), { status: "active" });
|
||||
expect(readProjectIdentity(cwd)?.id).toBe(result?.id);
|
||||
|
||||
await central.close();
|
||||
});
|
||||
|
||||
it("reattaches using stored identity when central row was wiped", async () => {
|
||||
const globalDir = makeTempDir("fn-4266-global-");
|
||||
const cwd = makeTempDir("fn-4266-project-");
|
||||
|
||||
const central = new CentralCore(globalDir);
|
||||
await central.init();
|
||||
|
||||
const first = await ensureCwdProjectRegistered({
|
||||
cwd,
|
||||
central,
|
||||
logPrefix: "serve",
|
||||
autoRegister: true,
|
||||
});
|
||||
expect(first).not.toBeNull();
|
||||
|
||||
await central.unregisterProject(first!.id);
|
||||
|
||||
const second = await ensureCwdProjectRegistered({
|
||||
cwd,
|
||||
central,
|
||||
logPrefix: "serve",
|
||||
autoRegister: true,
|
||||
});
|
||||
|
||||
expect(second?.id).toBe(first?.id);
|
||||
|
||||
await central.close();
|
||||
});
|
||||
@@ -90,7 +119,7 @@ describe("ensureCwdProjectRegistered", () => {
|
||||
const central = new CentralCore(globalDir);
|
||||
await central.init();
|
||||
|
||||
const registerSpy = vi.spyOn(central, "registerProject");
|
||||
const ensureSpy = vi.spyOn(central, "ensureProjectForPath");
|
||||
|
||||
const result = await ensureCwdProjectRegistered({
|
||||
cwd,
|
||||
@@ -101,7 +130,7 @@ describe("ensureCwdProjectRegistered", () => {
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(existsSync(join(cwd, ".fusion"))).toBe(false);
|
||||
expect(registerSpy).not.toHaveBeenCalled();
|
||||
expect(ensureSpy).not.toHaveBeenCalled();
|
||||
|
||||
await central.close();
|
||||
});
|
||||
@@ -113,7 +142,7 @@ describe("ensureCwdProjectRegistered", () => {
|
||||
const central = new CentralCore(globalDir);
|
||||
await central.init();
|
||||
|
||||
vi.spyOn(central, "registerProject").mockRejectedValueOnce(new Error("boom"));
|
||||
vi.spyOn(central, "ensureProjectForPath").mockRejectedValueOnce(new Error("boom"));
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
const result = await ensureCwdProjectRegistered({
|
||||
|
||||
@@ -16,6 +16,7 @@ const mockCentralInit = vi.fn();
|
||||
const mockCentralClose = vi.fn();
|
||||
const mockGetProjectByPath = vi.fn();
|
||||
const mockRegisterProject = vi.fn();
|
||||
const mockEnsureProjectForPath = vi.fn();
|
||||
const mockUpdateProject = vi.fn().mockResolvedValue({});
|
||||
const { mockIsValidSqliteDatabaseFile } = vi.hoisted(() => ({
|
||||
mockIsValidSqliteDatabaseFile: vi.fn(),
|
||||
@@ -30,6 +31,7 @@ vi.mock("@fusion/core", async () => {
|
||||
close: mockCentralClose,
|
||||
getProjectByPath: mockGetProjectByPath,
|
||||
registerProject: mockRegisterProject,
|
||||
ensureProjectForPath: mockEnsureProjectForPath,
|
||||
updateProject: mockUpdateProject,
|
||||
})),
|
||||
isQmdAvailable: vi.fn(() => Promise.resolve(true)),
|
||||
@@ -68,13 +70,29 @@ describe("init command", () => {
|
||||
name: "test-project",
|
||||
path: tempProjectDir,
|
||||
isolationMode: "in-process",
|
||||
status: "initializing",
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
});
|
||||
mockEnsureProjectForPath.mockResolvedValue({
|
||||
outcome: "registered",
|
||||
project: {
|
||||
id: "proj_test",
|
||||
name: "test-project",
|
||||
path: tempProjectDir,
|
||||
isolationMode: "in-process",
|
||||
status: "initializing",
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
},
|
||||
});
|
||||
mockIsValidSqliteDatabaseFile.mockImplementation((dbPath: string) => {
|
||||
if (!existsSync(dbPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return readFileSync(dbPath).length === 0;
|
||||
const content = readFileSync(dbPath);
|
||||
return content.subarray(0, 15).toString("utf8") === "SQLite format 3";
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,7 +132,7 @@ describe("init command", () => {
|
||||
await runInit({ path: tempProjectDir });
|
||||
|
||||
expect(existsSync(dbPath)).toBe(true);
|
||||
expect(statSync(dbPath).size).toBe(0);
|
||||
expect(statSync(dbPath).size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should reject existing invalid fusion.db files", async () => {
|
||||
|
||||
@@ -5,6 +5,10 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
const mockListProjects = vi.fn();
|
||||
const mockRegisterProject = vi.fn();
|
||||
const mockEnsureProjectForPath = vi.fn(async (...args: unknown[]) => ({
|
||||
outcome: "registered",
|
||||
project: await mockRegisterProject(...args),
|
||||
}));
|
||||
const mockUpdateProject = vi.fn().mockResolvedValue({});
|
||||
const mockUnregisterProject = vi.fn();
|
||||
const mockGetProject = vi.fn();
|
||||
@@ -30,6 +34,7 @@ vi.mock("@fusion/core", () => ({
|
||||
close: mockClose.mockResolvedValue(undefined),
|
||||
listProjects: mockListProjects,
|
||||
registerProject: mockRegisterProject,
|
||||
ensureProjectForPath: mockEnsureProjectForPath,
|
||||
updateProject: mockUpdateProject,
|
||||
unregisterProject: mockUnregisterProject,
|
||||
getProject: mockGetProject,
|
||||
@@ -45,6 +50,8 @@ vi.mock("@fusion/core", () => ({
|
||||
listTasks: mockTaskStoreListTasks,
|
||||
})),
|
||||
ensureMemoryFileWithBackend: mockEnsureMemoryFileWithBackend,
|
||||
readProjectIdentity: vi.fn().mockReturnValue(undefined),
|
||||
writeProjectIdentity: vi.fn(),
|
||||
COLUMNS: ["triage", "todo", "in-progress", "in-review", "done", "archived"],
|
||||
COLUMN_LABELS: {
|
||||
triage: "Triage",
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
processPullRequestMergeTask,
|
||||
} from "./task-lifecycle.js";
|
||||
import { promptForPort } from "./port-prompt.js";
|
||||
import { ensureCwdProjectRegistered } from "./ensure-project-registered.js";
|
||||
import { createReadOnlyProviderSettingsView } from "./provider-settings.js";
|
||||
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
|
||||
import { getClaudeCodeCredentialPaths, getCodexCliAuthPath, getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
|
||||
@@ -1589,7 +1590,12 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
// Phase 5 removes this fallback entirely.
|
||||
let cwdEngine: ReturnType<typeof engineManager.getEngine>;
|
||||
try {
|
||||
const registered = await centralCoreForEngine.getProjectByPath(cwd).catch(() => null);
|
||||
const registered = await ensureCwdProjectRegistered({
|
||||
cwd,
|
||||
central: centralCoreForEngine,
|
||||
logPrefix: "dashboard",
|
||||
autoRegister: true,
|
||||
});
|
||||
if (registered) {
|
||||
// Ensure the cwd project's engine exists before handing HTTP defaults to
|
||||
// createServer; background startAll may still be warming other projects.
|
||||
|
||||
@@ -2,7 +2,13 @@ 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";
|
||||
import {
|
||||
type CentralCore,
|
||||
type RegisteredProject,
|
||||
type ProjectIdentity,
|
||||
readProjectIdentity,
|
||||
writeProjectIdentity,
|
||||
} from "@fusion/core";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -13,6 +19,23 @@ export interface EnsureCwdProjectRegisteredOptions {
|
||||
autoRegister: boolean;
|
||||
}
|
||||
|
||||
function stampProjectIdentityBestEffort(
|
||||
cwd: string,
|
||||
project: RegisteredProject,
|
||||
logPrefix: string,
|
||||
): void {
|
||||
try {
|
||||
writeProjectIdentity(join(cwd, ".fusion"), {
|
||||
id: project.id,
|
||||
createdAt: project.createdAt,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[${logPrefix}] Could not persist project identity for ${cwd}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureCwdProjectRegistered(
|
||||
options: EnsureCwdProjectRegisteredOptions,
|
||||
): Promise<RegisteredProject | null> {
|
||||
@@ -20,6 +43,7 @@ export async function ensureCwdProjectRegistered(
|
||||
|
||||
const existing = await central.getProjectByPath(cwd);
|
||||
if (existing) {
|
||||
stampProjectIdentityBestEffort(cwd, existing, logPrefix);
|
||||
return existing;
|
||||
}
|
||||
|
||||
@@ -41,14 +65,25 @@ export async function ensureCwdProjectRegistered(
|
||||
}
|
||||
|
||||
const projectName = await detectProjectName(cwd);
|
||||
const project = await central.registerProject({
|
||||
name: projectName,
|
||||
const identity: ProjectIdentity | null = existsSync(dbPath) ? readProjectIdentity(fusionDir) : null;
|
||||
|
||||
const ensured = await central.ensureProjectForPath({
|
||||
path: cwd,
|
||||
isolationMode: "in-process",
|
||||
identity: identity ?? undefined,
|
||||
name: projectName,
|
||||
});
|
||||
|
||||
const project = ensured.project;
|
||||
await central.updateProject(project.id, { status: "active" });
|
||||
console.log(`[${logPrefix}] Auto-registered project "${project.name}" at ${cwd}`);
|
||||
stampProjectIdentityBestEffort(cwd, project, logPrefix);
|
||||
|
||||
if (ensured.outcome === "reattached") {
|
||||
console.log(
|
||||
`[${logPrefix}] Recovered project identity ${project.id} from ${dbPath} (central had no row)`,
|
||||
);
|
||||
} else if (ensured.outcome === "registered") {
|
||||
console.log(`[${logPrefix}] Auto-registered project "${project.name}" at ${cwd}`);
|
||||
}
|
||||
|
||||
return project;
|
||||
} catch (error) {
|
||||
|
||||
@@ -13,7 +13,14 @@ import { join, resolve, basename } from "node:path";
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
const execAsync = promisify(exec);
|
||||
import { CentralCore, QMD_INSTALL_COMMAND, isQmdAvailable, isValidSqliteDatabaseFile } from "@fusion/core";
|
||||
import {
|
||||
CentralCore,
|
||||
QMD_INSTALL_COMMAND,
|
||||
isQmdAvailable,
|
||||
isValidSqliteDatabaseFile,
|
||||
readProjectIdentity,
|
||||
writeProjectIdentity,
|
||||
} from "@fusion/core";
|
||||
import { maybeInstallClaudeSkillForNewProject } from "./claude-skills-runner.js";
|
||||
import { isGitRepo } from "./git.js";
|
||||
import {
|
||||
@@ -52,6 +59,14 @@ export async function runInit(options: InitOptions = {}): Promise<void> {
|
||||
|
||||
const existing = await central.getProjectByPath(cwd);
|
||||
if (existing) {
|
||||
try {
|
||||
writeProjectIdentity(join(cwd, ".fusion"), {
|
||||
id: existing.id,
|
||||
createdAt: existing.createdAt,
|
||||
});
|
||||
} catch {
|
||||
// Best-effort backfill only.
|
||||
}
|
||||
console.log(`✓ fn project already initialized: "${existing.name}"`);
|
||||
console.log(` Path: ${cwd}`);
|
||||
console.log(`\n Project is registered in the central registry.`);
|
||||
@@ -131,16 +146,27 @@ export async function runInit(options: InitOptions = {}): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register new project
|
||||
const project = await central.registerProject({
|
||||
name: projectName,
|
||||
const identity = existsSync(dbPath) ? readProjectIdentity(fusionDir) : null;
|
||||
const ensured = await central.ensureProjectForPath({
|
||||
path: cwd,
|
||||
isolationMode: "in-process",
|
||||
identity: identity ?? undefined,
|
||||
name: projectName,
|
||||
});
|
||||
|
||||
const project = ensured.project;
|
||||
|
||||
// Activate the project (registration sets it to 'initializing')
|
||||
await central.updateProject(project.id, { status: "active" });
|
||||
|
||||
try {
|
||||
writeProjectIdentity(join(cwd, ".fusion"), {
|
||||
id: project.id,
|
||||
createdAt: project.createdAt,
|
||||
});
|
||||
} catch (identityError) {
|
||||
console.warn(` ⚠ Could not persist project identity: ${identityError instanceof Error ? identityError.message : String(identityError)}`);
|
||||
}
|
||||
|
||||
maybeInstallClaudeSkillForNewProject(cwd);
|
||||
|
||||
console.log(` ✓ Registered in central database`);
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
COLUMNS,
|
||||
COLUMN_LABELS,
|
||||
type Column,
|
||||
readProjectIdentity,
|
||||
writeProjectIdentity,
|
||||
} from "@fusion/core";
|
||||
import { resolve, isAbsolute, relative, basename } from "node:path";
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
@@ -356,16 +358,27 @@ export async function runProjectAdd(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Register the project
|
||||
const project = await central.registerProject({
|
||||
name: projectName,
|
||||
const identity = existsSync(kbDbPath) ? readProjectIdentity(join(absolutePath, ".fusion")) : null;
|
||||
const ensured = await central.ensureProjectForPath({
|
||||
path: absolutePath,
|
||||
isolationMode,
|
||||
identity: identity ?? undefined,
|
||||
name: projectName,
|
||||
});
|
||||
|
||||
const project = ensured.project;
|
||||
|
||||
// Activate the project (registration sets it to 'initializing')
|
||||
await central.updateProject(project.id, { status: "active" });
|
||||
|
||||
try {
|
||||
writeProjectIdentity(join(absolutePath, ".fusion"), {
|
||||
id: project.id,
|
||||
createdAt: project.createdAt,
|
||||
});
|
||||
} catch (identityError) {
|
||||
console.warn(` ⚠ Warning: Could not persist project identity: ${identityError instanceof Error ? identityError.message : String(identityError)}`);
|
||||
}
|
||||
|
||||
// Bootstrap memory files (non-fatal if it fails)
|
||||
let memoryInitialized = false;
|
||||
try {
|
||||
|
||||
@@ -11,7 +11,14 @@
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { basename, dirname, resolve, normalize } from "node:path";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { CentralCore, isValidSqliteDatabaseFile, type RegisteredProject, type TaskStore } from "@fusion/core";
|
||||
import {
|
||||
CentralCore,
|
||||
isValidSqliteDatabaseFile,
|
||||
readProjectIdentity,
|
||||
writeProjectIdentity,
|
||||
type RegisteredProject,
|
||||
type TaskStore,
|
||||
} from "@fusion/core";
|
||||
import { ProjectManager } from "@fusion/engine";
|
||||
|
||||
// Singleton instances for reuse across commands
|
||||
@@ -246,6 +253,44 @@ export async function resolveProject(options: ResolveOptions = {}): Promise<Reso
|
||||
// 4. Has .fusion/ but not registered
|
||||
if (interactive) {
|
||||
console.log(`\n Found fn project at ${fusionDir} but it's not registered.`);
|
||||
const identity = readProjectIdentity(fusionDir);
|
||||
|
||||
if (identity) {
|
||||
const recover = await promptConfirm(
|
||||
`Found orphaned project data for this path under id ${identity.id} (createdAt=${identity.createdAt}). Restore it?`,
|
||||
true,
|
||||
);
|
||||
|
||||
if (recover) {
|
||||
try {
|
||||
const ensured = await central.ensureProjectForPath({
|
||||
path: fusionDir,
|
||||
identity,
|
||||
name: basename(fusionDir) || "unnamed",
|
||||
});
|
||||
const recoveredProject = ensured.project;
|
||||
await central.updateProject(recoveredProject.id, { status: "active" });
|
||||
try {
|
||||
writeProjectIdentity(fusionDir, {
|
||||
id: recoveredProject.id,
|
||||
createdAt: recoveredProject.createdAt,
|
||||
});
|
||||
} catch {
|
||||
// Best-effort stamp only.
|
||||
}
|
||||
console.log(`\n ✓ Restored project "${recoveredProject.name}" (${recoveredProject.id})`);
|
||||
return createResolvedProject(recoveredProject);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
throw new ProjectResolutionError(
|
||||
`Failed to restore project identity: ${errMsg}`,
|
||||
"NOT_REGISTERED",
|
||||
{ directory: fusionDir, error: errMsg },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shouldRegister = await promptConfirm("Register this project now?", true);
|
||||
|
||||
if (shouldRegister) {
|
||||
@@ -257,14 +302,39 @@ export async function resolveProject(options: ResolveOptions = {}): Promise<Reso
|
||||
const finalName = name.trim() || defaultName;
|
||||
|
||||
try {
|
||||
const newProject = await central.registerProject({
|
||||
name: finalName,
|
||||
const ensured = await central.ensureProjectForPath({
|
||||
path: fusionDir,
|
||||
isolationMode: "in-process",
|
||||
identity: undefined,
|
||||
name: finalName,
|
||||
});
|
||||
const newProject = ensured.project;
|
||||
|
||||
// Activate the project (registration sets it to 'initializing')
|
||||
await central.updateProject(newProject.id, { status: "active" });
|
||||
try {
|
||||
writeProjectIdentity(fusionDir, {
|
||||
id: newProject.id,
|
||||
createdAt: newProject.createdAt,
|
||||
});
|
||||
} catch (error) {
|
||||
if (identity && error instanceof Error && error.name === "ProjectIdentityConflictError") {
|
||||
const overwrite = await promptConfirm(
|
||||
"Stored identity differs from newly registered id. Overwrite stored identity with new id?",
|
||||
false,
|
||||
);
|
||||
if (!overwrite) {
|
||||
throw new ProjectResolutionError(
|
||||
"Registration cancelled to preserve existing stored identity.",
|
||||
"CANCELLED",
|
||||
{ directory: fusionDir },
|
||||
);
|
||||
}
|
||||
writeProjectIdentity(fusionDir, {
|
||||
id: newProject.id,
|
||||
createdAt: newProject.createdAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n ✓ Registered project "${newProject.name}"`);
|
||||
return createResolvedProject(newProject);
|
||||
@@ -602,15 +672,25 @@ export async function registerProjectInteractive(
|
||||
);
|
||||
}
|
||||
|
||||
// Register the project
|
||||
const project = await central.registerProject({
|
||||
name,
|
||||
const identity = readProjectIdentity(join(absPath, ".fusion"));
|
||||
const ensured = await central.ensureProjectForPath({
|
||||
path: absPath,
|
||||
isolationMode: options.isolation ?? "in-process",
|
||||
identity: identity ?? undefined,
|
||||
name,
|
||||
});
|
||||
|
||||
const project = ensured.project;
|
||||
|
||||
// Activate the project (registration sets it to 'initializing')
|
||||
await central.updateProject(project.id, { status: "active" });
|
||||
try {
|
||||
writeProjectIdentity(join(absPath, ".fusion"), {
|
||||
id: project.id,
|
||||
createdAt: project.createdAt,
|
||||
});
|
||||
} catch {
|
||||
// Best-effort stamp only.
|
||||
}
|
||||
|
||||
return createResolvedProject(project);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user