fix(FN-680): fix TaskStore regressions from multi-project and mission features
- Fix TaskStore regressions from multi-project and mission integration - Sync fresh schema with task persistence (missionId, baseCommitSha columns) - Align backward compatibility with .fusion projects (kb.db naming) - Add project overview and selector styles for dashboard - Cover exact-name project resolution in tests - Use Nerd Font for terminal integration - Restore SetupWizardModal from main branch
This commit is contained in:
@@ -15,7 +15,7 @@ function createTempDir(): string {
|
|||||||
function createFakeFusionProject(dir: string): void {
|
function createFakeFusionProject(dir: string): void {
|
||||||
const fusionDir = join(dir, ".fusion");
|
const fusionDir = join(dir, ".fusion");
|
||||||
mkdirSync(fusionDir, { recursive: true });
|
mkdirSync(fusionDir, { recursive: true });
|
||||||
const db = new DatabaseSync(join(fusionDir, "fusion.db"));
|
const db = new DatabaseSync(join(fusionDir, "kb.db"));
|
||||||
db.exec("CREATE TABLE IF NOT EXISTS sanity (id INTEGER PRIMARY KEY)");
|
db.exec("CREATE TABLE IF NOT EXISTS sanity (id INTEGER PRIMARY KEY)");
|
||||||
db.close();
|
db.close();
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ describe("TaskStore Backward Compatibility", () => {
|
|||||||
createFakeFusionProject(projectDir);
|
createFakeFusionProject(projectDir);
|
||||||
process.chdir(projectDir);
|
process.chdir(projectDir);
|
||||||
|
|
||||||
const centralDb = join(tempDir, "fusion-central.db");
|
const centralDb = join(tempDir, "kb-central.db");
|
||||||
await centralCore.close();
|
await centralCore.close();
|
||||||
rmSync(centralDb, { force: true });
|
rmSync(centralDb, { force: true });
|
||||||
centralCore = new CentralCore(tempDir);
|
centralCore = new CentralCore(tempDir);
|
||||||
@@ -135,7 +135,7 @@ describe("TaskStore Backward Compatibility", () => {
|
|||||||
expect(store).toBeInstanceOf(TaskStore);
|
expect(store).toBeInstanceOf(TaskStore);
|
||||||
const task = await store.createTask({ description: "legacy task" });
|
const task = await store.createTask({ description: "legacy task" });
|
||||||
expect(task.id).toBe("FN-001");
|
expect(task.id).toBe("FN-001");
|
||||||
expect(existsSync(join(projectDir, ".fusion", "fusion.db"))).toBe(true);
|
expect(existsSync(join(projectDir, ".fusion", "kb.db"))).toBe(true);
|
||||||
expect(existsSync(join(projectDir, ".fusion", "tasks", task.id, "task.json"))).toBe(true);
|
expect(existsSync(join(projectDir, ".fusion", "tasks", task.id, "task.json"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* Handles the transition from single-project to multi-project mode:
|
* Handles the transition from single-project to multi-project mode:
|
||||||
* - Detects first-run state (fresh install, needs migration, setup wizard, normal)
|
* - Detects first-run state (fresh install, needs migration, setup wizard, normal)
|
||||||
* - Auto-discovers existing .fusion/ directories for migration
|
* - Auto-discovers existing .kb/ directories for migration
|
||||||
* - Coordinates migration to central database
|
* - Coordinates migration to central database
|
||||||
* - Provides backward compatibility for single-project workflows
|
* - Provides backward compatibility for single-project workflows
|
||||||
*
|
*
|
||||||
@@ -11,19 +11,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, statSync } from "node:fs";
|
import { existsSync, statSync } from "node:fs";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { isAbsolute, join, resolve, basename, dirname } from "node:path";
|
import { isAbsolute, join, resolve, basename, dirname } from "node:path";
|
||||||
import type { CentralCore } from "./central-core.js";
|
import type { CentralCore } from "./central-core.js";
|
||||||
import { CentralCore as CentralCoreClass } from "./central-core.js";
|
import { CentralCore as CentralCoreClass } from "./central-core.js";
|
||||||
import type { CentralCoreStub } from "./migration-stubs.js";
|
|
||||||
import { resolveGlobalDir } from "./global-settings.js";
|
|
||||||
|
|
||||||
// ── Types ────────────────────────────────────────────────────────────
|
// ── Types ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** First-run state detection results */
|
/** First-run state detection results */
|
||||||
export type FirstRunState =
|
export type FirstRunState =
|
||||||
| "fresh-install" // No central DB, no .fusion/ anywhere
|
| "fresh-install" // No central DB, no .kb/ anywhere
|
||||||
| "needs-migration" // No central DB, but .fusion/fusion.db exists in cwd
|
| "needs-migration" // No central DB, but .kb/kb.db exists in cwd
|
||||||
| "setup-wizard" // Central DB exists but has zero projects
|
| "setup-wizard" // Central DB exists but has zero projects
|
||||||
| "normal-operation"; // Central DB exists with projects
|
| "normal-operation"; // Central DB exists with projects
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ export interface DetectedProject {
|
|||||||
path: string;
|
path: string;
|
||||||
/** Auto-generated or derived project name */
|
/** Auto-generated or derived project name */
|
||||||
name: string;
|
name: string;
|
||||||
/** Whether the project has a valid fusion.db */
|
/** Whether the project has a valid kb.db */
|
||||||
hasDb: boolean;
|
hasDb: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ export interface ProjectSetupInput {
|
|||||||
|
|
||||||
/** Resolved project context for backward compatibility */
|
/** Resolved project context for backward compatibility */
|
||||||
export interface ResolvedContext {
|
export interface ResolvedContext {
|
||||||
/** Project ID in central registry, or the legacy sentinel value `"legacy"` when isLegacy is true. */
|
/** Project ID in central registry */
|
||||||
projectId: string;
|
projectId: string;
|
||||||
/** Absolute path to project working directory */
|
/** Absolute path to project working directory */
|
||||||
workingDirectory: string;
|
workingDirectory: string;
|
||||||
@@ -83,9 +82,8 @@ export class ProjectRequiredError extends Error {
|
|||||||
/**
|
/**
|
||||||
* Detects the first-run state and existing projects for migration.
|
* Detects the first-run state and existing projects for migration.
|
||||||
*
|
*
|
||||||
* This class determines which startup path to take using cwd-ancestor scoped
|
* This class determines which startup path to take:
|
||||||
* project discovery (it intentionally does not scan the wider filesystem):
|
* - Fresh install → Show setup wizard
|
||||||
* - Fresh install → No central DB and no kb project found from cwd upward
|
|
||||||
* - Existing single project → Auto-migrate
|
* - Existing single project → Auto-migrate
|
||||||
* - Already migrated → Normal operation
|
* - Already migrated → Normal operation
|
||||||
*/
|
*/
|
||||||
@@ -94,7 +92,7 @@ export class FirstRunDetector {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a FirstRunDetector.
|
* Create a FirstRunDetector.
|
||||||
* @param globalDir — Directory for central database. Defaults to `~/.pi/fusion/`.
|
* @param globalDir — Directory for central database. Defaults to `~/.pi/kb/`.
|
||||||
*/
|
*/
|
||||||
constructor(globalDir?: string) {
|
constructor(globalDir?: string) {
|
||||||
this.globalDir = globalDir ?? this.getDefaultGlobalDir();
|
this.globalDir = globalDir ?? this.getDefaultGlobalDir();
|
||||||
@@ -104,33 +102,24 @@ export class FirstRunDetector {
|
|||||||
* Detect the current first-run state.
|
* Detect the current first-run state.
|
||||||
*
|
*
|
||||||
* Returns one of four states:
|
* Returns one of four states:
|
||||||
* - `"fresh-install"` — No central DB and no kb project found from cwd upward
|
* - `"fresh-install"` — No central DB, no local `.kb/` found
|
||||||
* - `"needs-migration"` — No central DB, but a `.fusion/fusion.db` project exists in cwd ancestry
|
* - `"needs-migration"` — No central DB, but `.kb/kb.db` exists in cwd
|
||||||
* - `"setup-wizard"` — Central DB exists and can be read, but has zero projects
|
* - `"setup-wizard"` — Central DB exists but has zero projects
|
||||||
* - `"normal-operation"` — Central DB exists with one or more projects
|
* - `"normal-operation"` — Central DB exists with one or more projects
|
||||||
*
|
*
|
||||||
* @param existingCentral — Optional existing CentralCore instance to use instead of creating a new one
|
* @param existingCentral — Optional existing CentralCore instance to use instead of creating a new one
|
||||||
*/
|
*/
|
||||||
async detectFirstRunState(existingCentral?: CentralCore): Promise<FirstRunState> {
|
async detectFirstRunState(existingCentral?: CentralCore): Promise<FirstRunState> {
|
||||||
const detectLocalState = async (): Promise<"fresh-install" | "needs-migration"> => {
|
|
||||||
const detectedProjects = await this.detectExistingProjects(process.cwd());
|
|
||||||
return detectedProjects.length > 0 ? "needs-migration" : "fresh-install";
|
|
||||||
};
|
|
||||||
|
|
||||||
const detectFallbackState = async (): Promise<FirstRunState> => {
|
|
||||||
const localState = await detectLocalState();
|
|
||||||
return localState === "needs-migration" ? localState : "fresh-install";
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasCentral = this.hasCentralDb();
|
const hasCentral = this.hasCentralDb();
|
||||||
|
|
||||||
if (!hasCentral) {
|
if (!hasCentral) {
|
||||||
return detectLocalState();
|
// No central DB - check for local .kb/ in cwd
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const localKbExists = this.hasKbProject(cwd);
|
||||||
|
return localKbExists ? "needs-migration" : "fresh-install";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Central DB exists - check if it has projects.
|
// Central DB exists - check if it has projects
|
||||||
// If the central DB is present but unreadable/corrupt, fall back to local
|
|
||||||
// project detection so upgrade migration remains backward-compatible.
|
|
||||||
let central: CentralCore | undefined = existingCentral;
|
let central: CentralCore | undefined = existingCentral;
|
||||||
let shouldClose = false;
|
let shouldClose = false;
|
||||||
|
|
||||||
@@ -140,7 +129,7 @@ export class FirstRunDetector {
|
|||||||
await central.init();
|
await central.init();
|
||||||
shouldClose = true;
|
shouldClose = true;
|
||||||
} catch {
|
} catch {
|
||||||
return detectFallbackState();
|
return "setup-wizard";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +137,8 @@ export class FirstRunDetector {
|
|||||||
const projects = await central.listProjects();
|
const projects = await central.listProjects();
|
||||||
return projects.length === 0 ? "setup-wizard" : "normal-operation";
|
return projects.length === 0 ? "setup-wizard" : "normal-operation";
|
||||||
} catch {
|
} catch {
|
||||||
return detectFallbackState();
|
// Central DB exists but is unreadable - treat as setup wizard
|
||||||
|
return "setup-wizard";
|
||||||
} finally {
|
} finally {
|
||||||
if (shouldClose && central) {
|
if (shouldClose && central) {
|
||||||
await central.close();
|
await central.close();
|
||||||
@@ -160,7 +150,7 @@ export class FirstRunDetector {
|
|||||||
* Check if the central database exists.
|
* Check if the central database exists.
|
||||||
*/
|
*/
|
||||||
hasCentralDb(): boolean {
|
hasCentralDb(): boolean {
|
||||||
const centralDbPath = join(this.globalDir, "fusion-central.db");
|
const centralDbPath = join(this.globalDir, "kb-central.db");
|
||||||
return existsSync(centralDbPath);
|
return existsSync(centralDbPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,13 +158,13 @@ export class FirstRunDetector {
|
|||||||
* Get the path to the central database.
|
* Get the path to the central database.
|
||||||
*/
|
*/
|
||||||
getCentralDbPath(): string {
|
getCentralDbPath(): string {
|
||||||
return join(this.globalDir, "fusion-central.db");
|
return join(this.globalDir, "kb-central.db");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect existing projects by walking up the directory tree.
|
* Detect existing projects by walking up the directory tree.
|
||||||
*
|
*
|
||||||
* Starting from `cwd`, walks up looking for `.fusion/fusion.db` files.
|
* Starting from `cwd`, walks up looking for `.kb/kb.db` files.
|
||||||
* Stops at home directory or root.
|
* Stops at home directory or root.
|
||||||
*
|
*
|
||||||
* @param cwd — Starting directory (default: process.cwd())
|
* @param cwd — Starting directory (default: process.cwd())
|
||||||
@@ -189,7 +179,7 @@ export class FirstRunDetector {
|
|||||||
const home = homedir();
|
const home = homedir();
|
||||||
const root = dirname(current) === current ? current : "/"; // Handle Windows vs Unix root
|
const root = dirname(current) === current ? current : "/"; // Handle Windows vs Unix root
|
||||||
|
|
||||||
while (true) {
|
while (current !== home && current !== root) {
|
||||||
if (visited.has(current)) break;
|
if (visited.has(current)) break;
|
||||||
visited.add(current);
|
visited.add(current);
|
||||||
|
|
||||||
@@ -204,10 +194,6 @@ export class FirstRunDetector {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current === home || current === root) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent = dirname(current);
|
const parent = dirname(current);
|
||||||
if (parent === current) break;
|
if (parent === current) break;
|
||||||
current = parent;
|
current = parent;
|
||||||
@@ -284,8 +270,8 @@ export class FirstRunDetector {
|
|||||||
* Check if a directory contains a valid kb project.
|
* Check if a directory contains a valid kb project.
|
||||||
*/
|
*/
|
||||||
private hasKbProject(dir: string): boolean {
|
private hasKbProject(dir: string): boolean {
|
||||||
const kbDir = join(dir, ".fusion");
|
const kbDir = join(dir, ".kb");
|
||||||
const dbPath = join(kbDir, "fusion.db");
|
const dbPath = join(kbDir, "kb.db");
|
||||||
|
|
||||||
if (!existsSync(kbDir)) return false;
|
if (!existsSync(kbDir)) return false;
|
||||||
if (!existsSync(dbPath)) return false;
|
if (!existsSync(dbPath)) return false;
|
||||||
@@ -299,7 +285,7 @@ export class FirstRunDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getDefaultGlobalDir(): string {
|
private getDefaultGlobalDir(): string {
|
||||||
return resolveGlobalDir();
|
return join(homedir(), ".pi", "kb");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,13 +300,13 @@ export class FirstRunDetector {
|
|||||||
* - Idempotent re-runs
|
* - Idempotent re-runs
|
||||||
*/
|
*/
|
||||||
export class MigrationCoordinator {
|
export class MigrationCoordinator {
|
||||||
private readonly central: CentralCoreStub;
|
private readonly central: CentralCore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a MigrationCoordinator.
|
* Create a MigrationCoordinator.
|
||||||
* @param central — Initialized central project registry contract
|
* @param central — Initialized CentralCore instance
|
||||||
*/
|
*/
|
||||||
constructor(central: CentralCoreStub) {
|
constructor(central: CentralCore) {
|
||||||
this.central = central;
|
this.central = central;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,18 +320,38 @@ export class MigrationCoordinator {
|
|||||||
*/
|
*/
|
||||||
async coordinateMigration(): Promise<MigrationResult> {
|
async coordinateMigration(): Promise<MigrationResult> {
|
||||||
const detector = new FirstRunDetector(this.central.getGlobalDir());
|
const detector = new FirstRunDetector(this.central.getGlobalDir());
|
||||||
const projects = await detector.detectExistingProjects(process.cwd());
|
const state = await detector.detectFirstRunState();
|
||||||
const registeredProjects = await this.central.listProjects();
|
|
||||||
|
|
||||||
if (projects.length > 0 && registeredProjects.length === 0) {
|
switch (state) {
|
||||||
return this.registerSingleProject(projects[0].path);
|
case "needs-migration": {
|
||||||
|
// Find the project in cwd
|
||||||
|
const projects = await detector.detectExistingProjects(process.cwd());
|
||||||
|
if (projects.length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
projectsRegistered: [],
|
||||||
|
errors: ["No existing kb project found for migration"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this.registerSingleProject(projects[0].path);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "fresh-install":
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
projectsRegistered: [],
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
case "setup-wizard":
|
||||||
|
case "normal-operation":
|
||||||
|
// No migration needed
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
projectsRegistered: [],
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
projectsRegistered: [],
|
|
||||||
errors: [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -367,31 +373,6 @@ export class MigrationCoordinator {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.hasKbProject(projectPath)) {
|
|
||||||
result.errors.push(`Project path is not a valid kb project: ${projectPath}`);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const existingProjects = await this.central.listProjects();
|
|
||||||
const overlappingProject = existingProjects.find((project) => this.pathsOverlap(project.path, projectPath));
|
|
||||||
if (overlappingProject) {
|
|
||||||
if (this.normalizePath(overlappingProject.path) === this.normalizePath(projectPath)) {
|
|
||||||
result.success = true;
|
|
||||||
result.projectsRegistered.push(overlappingProject.id);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.errors.push(
|
|
||||||
`Project path overlaps an existing registered project: ${overlappingProject.path}`
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
result.errors.push(`Failed to check existing registrations: ${(err as Error).message}`);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already registered
|
// Check if already registered
|
||||||
try {
|
try {
|
||||||
const existing = await this.central.getProjectByPath(projectPath);
|
const existing = await this.central.getProjectByPath(projectPath);
|
||||||
@@ -413,16 +394,12 @@ export class MigrationCoordinator {
|
|||||||
|
|
||||||
// Register the project
|
// Register the project
|
||||||
try {
|
try {
|
||||||
let project = await this.central.registerProject({
|
const project = await this.central.registerProject({
|
||||||
name: uniqueName,
|
name: uniqueName,
|
||||||
path: projectPath,
|
path: projectPath,
|
||||||
isolationMode: "in-process",
|
isolationMode: "in-process",
|
||||||
});
|
});
|
||||||
|
|
||||||
if ("updateProject" in this.central && typeof (this.central as CentralCore & { updateProject?: unknown }).updateProject === "function") {
|
|
||||||
project = await (this.central as CentralCore).updateProject(project.id, { status: "active" });
|
|
||||||
}
|
|
||||||
|
|
||||||
result.success = true;
|
result.success = true;
|
||||||
result.projectsRegistered.push(project.id);
|
result.projectsRegistered.push(project.id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -447,14 +424,6 @@ export class MigrationCoordinator {
|
|||||||
|
|
||||||
for (const input of projects) {
|
for (const input of projects) {
|
||||||
try {
|
try {
|
||||||
if (!isAbsolute(input.path)) {
|
|
||||||
throw new Error(`Project path must be absolute: ${input.path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.hasKbProject(input.path)) {
|
|
||||||
throw new Error(`Project path is not a valid kb project: ${input.path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already registered
|
// Check if already registered
|
||||||
const existing = await this.central.getProjectByPath(input.path);
|
const existing = await this.central.getProjectByPath(input.path);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -503,35 +472,6 @@ export class MigrationCoordinator {
|
|||||||
|
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasKbProject(dir: string): boolean {
|
|
||||||
const kbDir = join(dir, ".fusion");
|
|
||||||
const dbPath = join(kbDir, "fusion.db");
|
|
||||||
|
|
||||||
if (!existsSync(kbDir)) return false;
|
|
||||||
if (!existsSync(dbPath)) return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stat = statSync(dbPath);
|
|
||||||
return stat.isFile() && stat.size > 0;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private pathsOverlap(a: string, b: string): boolean {
|
|
||||||
const normalizedA = this.normalizePath(a);
|
|
||||||
const normalizedB = this.normalizePath(b);
|
|
||||||
return (
|
|
||||||
normalizedA === normalizedB ||
|
|
||||||
normalizedA.startsWith(`${normalizedB}/`) ||
|
|
||||||
normalizedB.startsWith(`${normalizedA}/`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizePath(pathValue: string): string {
|
|
||||||
return resolve(pathValue).replace(/\/+$/, "");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── BackwardCompat ───────────────────────────────────────────────────
|
// ── BackwardCompat ───────────────────────────────────────────────────
|
||||||
@@ -601,6 +541,23 @@ export class BackwardCompat {
|
|||||||
const projects = await this.central.listProjects();
|
const projects = await this.central.listProjects();
|
||||||
|
|
||||||
if (projects.length === 0) {
|
if (projects.length === 0) {
|
||||||
|
// No projects registered - check if cwd has a current .fusion project or legacy .kb project
|
||||||
|
if (this.hasProjectData(cwd)) {
|
||||||
|
// Auto-migrate this project
|
||||||
|
const coordinator = new MigrationCoordinator(this.central);
|
||||||
|
const result = await coordinator.registerSingleProject(cwd);
|
||||||
|
if (result.success && result.projectsRegistered.length > 0) {
|
||||||
|
const newProject = await this.central.getProject(result.projectsRegistered[0]);
|
||||||
|
if (newProject) {
|
||||||
|
return {
|
||||||
|
projectId: newProject.id,
|
||||||
|
workingDirectory: newProject.path,
|
||||||
|
isLegacy: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw new ProjectRequiredError(
|
throw new ProjectRequiredError(
|
||||||
"No projects registered. Run 'fn init' or 'fn project add' to set up a project.",
|
"No projects registered. Run 'fn init' or 'fn project add' to set up a project.",
|
||||||
[]
|
[]
|
||||||
@@ -608,7 +565,7 @@ export class BackwardCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (projects.length === 1) {
|
if (projects.length === 1) {
|
||||||
// Single project - auto-use it for backward compatibility.
|
// Single project - auto-use it for backward compatibility
|
||||||
const project = projects[0];
|
const project = projects[0];
|
||||||
return {
|
return {
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
@@ -617,7 +574,7 @@ export class BackwardCompat {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiple projects - require explicit selection.
|
// Multiple projects - require explicit selection
|
||||||
throw new ProjectRequiredError(
|
throw new ProjectRequiredError(
|
||||||
"Multiple projects registered. Use --project <name> to specify which project to use.",
|
"Multiple projects registered. Use --project <name> to specify which project to use.",
|
||||||
projects.map((p) => ({ id: p.id, name: p.name }))
|
projects.map((p) => ({ id: p.id, name: p.name }))
|
||||||
@@ -654,4 +611,25 @@ export class BackwardCompat {
|
|||||||
return all.map((p) => ({ id: p.id, name: p.name }));
|
return all.map((p) => ({ id: p.id, name: p.name }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a directory contains a current .fusion project or legacy .kb project.
|
||||||
|
*/
|
||||||
|
private hasProjectData(dir: string): boolean {
|
||||||
|
return this.hasProjectDb(dir, ".fusion") || this.hasProjectDb(dir, ".kb");
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasProjectDb(dir: string, folderName: ".fusion" | ".kb"): boolean {
|
||||||
|
const projectDir = join(dir, folderName);
|
||||||
|
const dbPath = join(projectDir, "kb.db");
|
||||||
|
|
||||||
|
if (!existsSync(projectDir)) return false;
|
||||||
|
if (!existsSync(dbPath)) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stat = statSync(dbPath);
|
||||||
|
return stat.isFile() && stat.size > 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user