feat: rename data directory, add global project settings, multi-project CLI commands, and provider badge in model selector
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -193,7 +193,7 @@ describe("CentralCore Integration", () => {
|
||||
|
||||
it("should verify database path and stats", async () => {
|
||||
const dbPath = central.getDatabasePath();
|
||||
expect(dbPath).toContain("kb-central.db");
|
||||
expect(dbPath).toContain("fusion-central.db");
|
||||
|
||||
const globalDir = central.getGlobalDir();
|
||||
expect(globalDir).toBe(tempDir);
|
||||
|
||||
@@ -13,8 +13,8 @@ function createTempDir(): string {
|
||||
|
||||
// Helper to create a fake kb project structure
|
||||
function createFakeKbProject(dir: string): void {
|
||||
mkdirSync(join(dir, ".kb"), { recursive: true });
|
||||
writeFileSync(join(dir, ".kb", "kb.db"), "");
|
||||
mkdirSync(join(dir, ".fusion"), { recursive: true });
|
||||
writeFileSync(join(dir, ".fusion", "fusion.db"), "");
|
||||
}
|
||||
|
||||
describe("FirstRunExperience", () => {
|
||||
|
||||
@@ -12,9 +12,9 @@ function createTempDir(): string {
|
||||
|
||||
// Helper to create a fake kb project structure
|
||||
function createFakeKbProject(dir: string): void {
|
||||
mkdirSync(join(dir, ".kb"), { recursive: true });
|
||||
mkdirSync(join(dir, ".fusion"), { recursive: true });
|
||||
// Create an empty file as the database (enough for detection)
|
||||
writeFileSync(join(dir, ".kb", "kb.db"), "");
|
||||
writeFileSync(join(dir, ".fusion", "fusion.db"), "");
|
||||
}
|
||||
|
||||
describe("MigrationOrchestrator", () => {
|
||||
@@ -164,7 +164,7 @@ describe("MigrationOrchestrator", () => {
|
||||
const projectDir = join(tempDir, "incomplete-project");
|
||||
mkdirSync(projectDir, { recursive: true });
|
||||
mkdirSync(join(projectDir, ".fusion"), { recursive: true });
|
||||
// Create directory but no kb.db file
|
||||
// Create directory but no fusion.db file
|
||||
|
||||
const detected = await orchestrator.detectExistingProjects(projectDir);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ function createTempDir(): string {
|
||||
function createFakeFusionProject(dir: string): void {
|
||||
const fusionDir = join(dir, ".fusion");
|
||||
mkdirSync(fusionDir, { recursive: true });
|
||||
const db = new DatabaseSync(join(fusionDir, "kb.db"));
|
||||
const db = new DatabaseSync(join(fusionDir, "fusion.db"));
|
||||
db.exec("CREATE TABLE IF NOT EXISTS sanity (id INTEGER PRIMARY KEY)");
|
||||
db.close();
|
||||
}
|
||||
@@ -125,7 +125,7 @@ describe("TaskStore Backward Compatibility", () => {
|
||||
createFakeFusionProject(projectDir);
|
||||
process.chdir(projectDir);
|
||||
|
||||
const centralDb = join(tempDir, "kb-central.db");
|
||||
const centralDb = join(tempDir, "fusion-central.db");
|
||||
await centralCore.close();
|
||||
rmSync(centralDb, { force: true });
|
||||
centralCore = new CentralCore(tempDir);
|
||||
@@ -135,7 +135,7 @@ describe("TaskStore Backward Compatibility", () => {
|
||||
expect(store).toBeInstanceOf(TaskStore);
|
||||
const task = await store.createTask({ description: "legacy task" });
|
||||
expect(task.id).toBe("FN-001");
|
||||
expect(existsSync(join(projectDir, ".fusion", "kb.db"))).toBe(true);
|
||||
expect(existsSync(join(projectDir, ".fusion", "fusion.db"))).toBe(true);
|
||||
expect(existsSync(join(projectDir, ".fusion", "tasks", task.id, "task.json"))).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* AgentStore - Filesystem-based persistence for agent lifecycle management
|
||||
*
|
||||
* Agents are stored at `.kb/agents/{agentId}.json` with their metadata.
|
||||
* Heartbeat events are appended to `.kb/agents/{agentId}-heartbeats.jsonl`.
|
||||
* Agents are stored at `.fusion/agents/{agentId}.json` with their metadata.
|
||||
* Heartbeat events are appended to `.fusion/agents/{agentId}-heartbeats.jsonl`.
|
||||
*
|
||||
* File Structure:
|
||||
* - agents/{agentId}.json: Agent metadata (id, name, role, state, taskId, timestamps, metadata)
|
||||
@@ -51,7 +51,7 @@ type TypedEventEmitter<Events extends Record<string, unknown[]>> = {
|
||||
|
||||
/** Options for AgentStore constructor */
|
||||
export interface AgentStoreOptions {
|
||||
/** Root directory for kb data (default: .kb) */
|
||||
/** Root directory for kb data (default: .fusion) */
|
||||
rootDir?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("BackupManager", () => {
|
||||
kbDir = join(tempDir, ".fusion");
|
||||
await mkdir(kbDir, { recursive: true });
|
||||
// Create a dummy database file
|
||||
writeFileSync(join(kbDir, "kb.db"), "dummy database content");
|
||||
writeFileSync(join(kbDir, "fusion.db"), "dummy database content");
|
||||
backupManager = new BackupManager(kbDir);
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("BackupManager", () => {
|
||||
it("should copy database content correctly", async () => {
|
||||
const backup = await backupManager.createBackup();
|
||||
|
||||
const originalContent = readFileSync(join(kbDir, "kb.db"), "utf-8");
|
||||
const originalContent = readFileSync(join(kbDir, "fusion.db"), "utf-8");
|
||||
const backupContent = readFileSync(backup.path, "utf-8");
|
||||
|
||||
expect(backupContent).toBe(originalContent);
|
||||
@@ -186,13 +186,13 @@ describe("BackupManager", () => {
|
||||
const backup = await backupManager.createBackup();
|
||||
|
||||
// Modify the original database
|
||||
await writeFile(join(kbDir, "kb.db"), "modified content");
|
||||
await writeFile(join(kbDir, "fusion.db"), "modified content");
|
||||
|
||||
// Restore the backup
|
||||
await backupManager.restoreBackup(backup.filename, { createPreRestoreBackup: false });
|
||||
|
||||
// Verify the restore
|
||||
const restoredContent = readFileSync(join(kbDir, "kb.db"), "utf-8");
|
||||
const restoredContent = readFileSync(join(kbDir, "fusion.db"), "utf-8");
|
||||
expect(restoredContent).toBe("dummy database content");
|
||||
});
|
||||
|
||||
@@ -313,7 +313,7 @@ describe("createBackupManager", () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
|
||||
const kbDir = join(tempDir, ".fusion");
|
||||
await mkdir(kbDir, { recursive: true });
|
||||
writeFileSync(join(kbDir, "kb.db"), "test");
|
||||
writeFileSync(join(kbDir, "fusion.db"), "test");
|
||||
|
||||
const settings: Partial<ProjectSettings> = {
|
||||
autoBackupDir: "custom/backups",
|
||||
@@ -349,7 +349,7 @@ describe("runBackupCommand", () => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
|
||||
kbDir = join(tempDir, ".fusion");
|
||||
await mkdir(kbDir, { recursive: true });
|
||||
writeFileSync(join(kbDir, "kb.db"), "dummy database content");
|
||||
writeFileSync(join(kbDir, "fusion.db"), "dummy database content");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -437,7 +437,7 @@ describe("runBackupCommand", () => {
|
||||
|
||||
it("should return failure when database file is missing", async () => {
|
||||
// Remove the database
|
||||
await rm(join(kbDir, "kb.db"));
|
||||
await rm(join(kbDir, "fusion.db"));
|
||||
|
||||
const settings: ProjectSettings = {
|
||||
maxConcurrent: 2,
|
||||
|
||||
@@ -39,7 +39,7 @@ export class BackupManager {
|
||||
|
||||
/**
|
||||
* Creates a new BackupManager instance.
|
||||
* @param kbDir - Absolute path to the .kb directory
|
||||
* @param kbDir - Absolute path to the .fusion directory
|
||||
* @param options - Backup configuration options
|
||||
*/
|
||||
constructor(kbDir: string, options?: BackupOptions) {
|
||||
@@ -61,7 +61,7 @@ export class BackupManager {
|
||||
* @returns BackupInfo for the newly created backup
|
||||
*/
|
||||
async createBackup(): Promise<BackupInfo> {
|
||||
const sourcePath = join(this.kbDir, "kb.db");
|
||||
const sourcePath = join(this.kbDir, "fusion.db");
|
||||
const backupDirPath = this.getBackupDirPath();
|
||||
|
||||
// Ensure backup directory exists
|
||||
@@ -188,7 +188,7 @@ export class BackupManager {
|
||||
): Promise<void> {
|
||||
const backupDirPath = this.getBackupDirPath();
|
||||
const sourcePath = join(backupDirPath, filename);
|
||||
const targetPath = join(this.kbDir, "kb.db");
|
||||
const targetPath = join(this.kbDir, "fusion.db");
|
||||
|
||||
// Verify source exists
|
||||
try {
|
||||
@@ -281,7 +281,7 @@ export function validateBackupDir(dir: string): boolean {
|
||||
|
||||
/**
|
||||
* Factory function to create a BackupManager with project settings.
|
||||
* @param kbDir - Absolute path to the .kb directory
|
||||
* @param kbDir - Absolute path to the .fusion directory
|
||||
* @param settings - Project settings containing backup configuration
|
||||
* @returns Configured BackupManager instance
|
||||
*/
|
||||
@@ -303,7 +303,7 @@ export function createBackupManager(
|
||||
* at the automation/scheduler level. This allows manual backups via CLI even when
|
||||
* auto-backup is disabled.
|
||||
*
|
||||
* @param kbDir - Absolute path to the .kb directory
|
||||
* @param kbDir - Absolute path to the .fusion directory
|
||||
* @param settings - Project settings
|
||||
* @returns Result of the backup operation
|
||||
*/
|
||||
|
||||
@@ -18,9 +18,9 @@ function tempDir(prefix: string): string {
|
||||
|
||||
// Helper to create a fake kb project
|
||||
function createFakeKbProject(dir: string): void {
|
||||
const kbDir = join(dir, ".kb");
|
||||
const kbDir = join(dir, ".fusion");
|
||||
mkdirSync(kbDir, { recursive: true });
|
||||
writeFileSync(join(kbDir, "kb.db"), "SQLite format 3\x00");
|
||||
writeFileSync(join(kbDir, "fusion.db"), "SQLite format 3\x00");
|
||||
}
|
||||
|
||||
describe("Backward Compatibility Layer", () => {
|
||||
@@ -216,7 +216,7 @@ describe("Backward Compatibility Layer", () => {
|
||||
it("should return legacy mode when no central DB", async () => {
|
||||
// Close and remove central DB
|
||||
await central.close();
|
||||
rmSync(join(tempGlobalDir, "kb-central.db"), { force: true });
|
||||
rmSync(join(tempGlobalDir, "fusion-central.db"), { force: true });
|
||||
|
||||
// Re-create central but don't init
|
||||
central = new CentralCore(tempGlobalDir);
|
||||
@@ -232,7 +232,7 @@ describe("Backward Compatibility Layer", () => {
|
||||
it("should report legacy mode correctly", async () => {
|
||||
// Close and remove central DB
|
||||
await central.close();
|
||||
rmSync(join(tempGlobalDir, "kb-central.db"), { force: true });
|
||||
rmSync(join(tempGlobalDir, "fusion-central.db"), { force: true });
|
||||
|
||||
central = new CentralCore(tempGlobalDir);
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("CentralCore", () => {
|
||||
it("should initialize and create database", async () => {
|
||||
await central.init();
|
||||
expect(central.isInitialized()).toBe(true);
|
||||
expect(central.getDatabasePath()).toBe(join(tempDir, "kb-central.db"));
|
||||
expect(central.getDatabasePath()).toBe(join(tempDir, "fusion-central.db"));
|
||||
});
|
||||
|
||||
it("should be idempotent on multiple init calls", async () => {
|
||||
@@ -919,7 +919,7 @@ describe("CentralCore", () => {
|
||||
|
||||
it("should get database path", async () => {
|
||||
const path = central.getDatabasePath();
|
||||
expect(path).toBe(join(tempDir, "kb-central.db"));
|
||||
expect(path).toBe(join(tempDir, "fusion-central.db"));
|
||||
});
|
||||
|
||||
it("should get global directory", async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Provides project registry, health tracking, unified activity feed,
|
||||
* and global concurrency management across all registered projects.
|
||||
*
|
||||
* The central database is located at `~/.pi/kb/kb-central.db`.
|
||||
* The central database is located at `~/.pi/fusion/fusion-central.db`.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -43,7 +43,7 @@ import type {
|
||||
ProjectSettings,
|
||||
} from "./types.js";
|
||||
import { CentralDatabase, toJson, toJsonNullable, fromJson } from "./central-db.js";
|
||||
import { defaultGlobalDir } from "./global-settings.js";
|
||||
import { resolveGlobalDir } from "./global-settings.js";
|
||||
|
||||
// ── Event Types ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -71,13 +71,13 @@ export class CentralCore extends EventEmitter<CentralCoreEvents> {
|
||||
|
||||
/**
|
||||
* Create a CentralCore instance.
|
||||
* @param globalDir — Directory for central database. Defaults to `~/.pi/kb/`.
|
||||
* @param globalDir — Directory for central database. Defaults to `~/.pi/fusion/`.
|
||||
* Accepts a custom path for testing.
|
||||
*/
|
||||
constructor(globalDir?: string) {
|
||||
super();
|
||||
this.setMaxListeners(100);
|
||||
this.globalDir = globalDir ?? defaultGlobalDir();
|
||||
this.globalDir = resolveGlobalDir(globalDir);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -818,10 +818,10 @@ export class CentralCore extends EventEmitter<CentralCoreEvents> {
|
||||
/**
|
||||
* Get the path to the central database file.
|
||||
*
|
||||
* @returns Absolute path to kb-central.db
|
||||
* @returns Absolute path to fusion-central.db
|
||||
*/
|
||||
getDatabasePath(): string {
|
||||
return this.db?.getPath() ?? join(this.globalDir, "kb-central.db");
|
||||
return this.db?.getPath() ?? join(this.globalDir, "fusion-central.db");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("CentralDatabase", () => {
|
||||
it("should create database at the specified path", () => {
|
||||
db.init();
|
||||
const dbPath = db.getPath();
|
||||
expect(dbPath).toBe(join(tempDir, "kb-central.db"));
|
||||
expect(dbPath).toBe(join(tempDir, "fusion-central.db"));
|
||||
// Verify file exists
|
||||
const stats = statSync(dbPath);
|
||||
expect(stats.isFile()).toBe(true);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* synchronous transaction handling. The database runs in WAL mode
|
||||
* for concurrent reader/writer access.
|
||||
*
|
||||
* This database is stored at `~/.pi/kb/kb-central.db` and serves as the
|
||||
* This database is stored at `~/.pi/fusion/fusion-central.db` and serves as the
|
||||
* coordination hub for all projects, storing the project registry,
|
||||
* unified activity feed, global concurrency limits, and project health.
|
||||
*/
|
||||
@@ -13,8 +13,8 @@
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { join } from "node:path";
|
||||
import { mkdirSync, existsSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import type { Statement } from "./db.js";
|
||||
import { resolveGlobalDir } from "./global-settings.js";
|
||||
|
||||
// ── JSON Helpers (reused from db.ts) ─────────────────────────────────────
|
||||
|
||||
@@ -95,13 +95,6 @@ CREATE TABLE IF NOT EXISTS __meta (
|
||||
|
||||
// ── Central Database Class ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Default directory for central kb data: `~/.pi/kb/`
|
||||
*/
|
||||
function defaultGlobalDir(): string {
|
||||
return join(homedir(), ".pi", "kb");
|
||||
}
|
||||
|
||||
export class CentralDatabase {
|
||||
private db: DatabaseSync;
|
||||
private readonly dbPath: string;
|
||||
@@ -110,8 +103,8 @@ export class CentralDatabase {
|
||||
private transactionDepth = 0;
|
||||
|
||||
constructor(globalDir?: string) {
|
||||
this.globalDir = globalDir ?? defaultGlobalDir();
|
||||
this.dbPath = join(this.globalDir, "kb-central.db");
|
||||
this.globalDir = resolveGlobalDir(globalDir);
|
||||
this.dbPath = join(this.globalDir, "fusion-central.db");
|
||||
|
||||
// Ensure directory exists
|
||||
if (!existsSync(this.globalDir)) {
|
||||
@@ -256,7 +249,7 @@ export class CentralDatabase {
|
||||
/**
|
||||
* Create a new CentralDatabase instance (does NOT initialize schema).
|
||||
* Callers must call `db.init()` separately.
|
||||
* @param globalDir - Path to the global kb directory (e.g., `~/.pi/kb/`)
|
||||
* @param globalDir - Path to the global fusion directory (e.g., `~/.pi/fusion/`)
|
||||
* @returns CentralDatabase instance (not yet initialized)
|
||||
*/
|
||||
export function createCentralDatabase(globalDir?: string): CentralDatabase {
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { Database } from "./db.js";
|
||||
import { toJson, toJsonNullable, normalizeTaskComments } from "./db.js";
|
||||
import type { Task, BoardConfig, ActivityLogEntry, ArchivedTaskEntry } from "./types.js";
|
||||
import type { ScheduledTask } from "./automation.js";
|
||||
import { resolveGlobalDir } from "./global-settings.js";
|
||||
|
||||
// ── Detection ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -24,7 +25,7 @@ import type { ScheduledTask } from "./automation.js";
|
||||
* Returns true if migration is needed.
|
||||
*/
|
||||
export function detectLegacyData(kbDir: string): boolean {
|
||||
const hasDb = existsSync(join(kbDir, "kb.db"));
|
||||
const hasDb = existsSync(join(kbDir, "fusion.db"));
|
||||
if (hasDb) return false;
|
||||
|
||||
return (
|
||||
@@ -45,7 +46,7 @@ export function getMigrationStatus(kbDir: string): {
|
||||
hasDatabase: boolean;
|
||||
needsMigration: boolean;
|
||||
} {
|
||||
const hasDatabase = existsSync(join(kbDir, "kb.db"));
|
||||
const hasDatabase = existsSync(join(kbDir, "fusion.db"));
|
||||
const hasLegacy =
|
||||
existsSync(join(kbDir, "tasks")) ||
|
||||
existsSync(join(kbDir, "config.json")) ||
|
||||
@@ -539,13 +540,13 @@ async function createBackups(kbDir: string): Promise<void> {
|
||||
*
|
||||
* Returns true if:
|
||||
* - Central DB doesn't exist AND
|
||||
* - cwd has `.kb/kb.db` (existing single-project)
|
||||
* - cwd has `.fusion/fusion.db` or `.fusion/fusion.db` (existing single-project)
|
||||
*
|
||||
* @param cwd — Current working directory to check
|
||||
* @param globalDir — Directory for central database. Defaults to `~/.pi/kb/`.
|
||||
* @param globalDir — Directory for central database. Defaults to `~/.pi/fusion/`.
|
||||
*/
|
||||
export function needsCentralMigration(cwd: string, globalDir?: string): boolean {
|
||||
const centralDbPath = join(globalDir ?? join(homedir(), ".pi", "kb"), "kb-central.db");
|
||||
const centralDbPath = join(resolveGlobalDir(globalDir), "fusion-central.db");
|
||||
if (existsSync(centralDbPath)) {
|
||||
return false;
|
||||
}
|
||||
@@ -555,7 +556,14 @@ export function needsCentralMigration(cwd: string, globalDir?: string): boolean
|
||||
const root = dirname(current) === current ? current : "/";
|
||||
|
||||
while (true) {
|
||||
const dbPath = join(current, ".kb", "kb.db");
|
||||
const fusionDbPath = join(current, ".fusion", "fusion.db");
|
||||
if (existsSync(fusionDbPath)) {
|
||||
try {
|
||||
const stat = statSync(fusionDbPath);
|
||||
if (stat.isFile() && stat.size > 0) return true;
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
const dbPath = join(current, ".fusion", "fusion.db");
|
||||
if (existsSync(dbPath)) {
|
||||
try {
|
||||
const stat = statSync(dbPath);
|
||||
@@ -581,7 +589,7 @@ export function needsCentralMigration(cwd: string, globalDir?: string): boolean
|
||||
* Detect existing projects by walking up from cwd.
|
||||
*
|
||||
* @param cwd — Starting directory (default: process.cwd())
|
||||
* @param globalDir — Directory for central database. Defaults to `~/.pi/kb/`.
|
||||
* @param globalDir — Directory for central database. Defaults to `~/.pi/fusion/`.
|
||||
* @returns Array of detected projects
|
||||
*/
|
||||
export async function detectExistingProjects(
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("Database", () => {
|
||||
|
||||
describe("initialization", () => {
|
||||
it("creates the database file", () => {
|
||||
expect(existsSync(join(kbDir, "kb.db"))).toBe(true);
|
||||
expect(existsSync(join(kbDir, "fusion.db"))).toBe(true);
|
||||
});
|
||||
|
||||
it("creates the .fusion directory if missing", () => {
|
||||
@@ -1024,7 +1024,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(kbDir);
|
||||
|
||||
// DB file exists (created on open) but schema not initialized
|
||||
expect(existsSync(join(kbDir, "kb.db"))).toBe(true);
|
||||
expect(existsSync(join(kbDir, "fusion.db"))).toBe(true);
|
||||
// Schema is NOT yet created — querying __meta would fail
|
||||
expect(() => db.getSchemaVersion()).toThrow();
|
||||
|
||||
@@ -1048,7 +1048,7 @@ describe("createDatabase factory", () => {
|
||||
const kbDir = join(tmpDir, ".fusion");
|
||||
const db = createDatabase(kbDir);
|
||||
|
||||
expect(db.getPath()).toBe(join(kbDir, "kb.db"));
|
||||
expect(db.getPath()).toBe(join(kbDir, "fusion.db"));
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
@@ -315,7 +315,7 @@ export class Database {
|
||||
private transactionDepth = 0;
|
||||
|
||||
constructor(private kbDir: string) {
|
||||
this.dbPath = join(kbDir, "kb.db");
|
||||
this.dbPath = join(kbDir, "fusion.db");
|
||||
|
||||
// Ensure .fusion directory exists
|
||||
if (!existsSync(kbDir)) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { GlobalSettingsStore } from "./global-settings.js";
|
||||
import { GlobalSettingsStore, defaultGlobalDir } from "./global-settings.js";
|
||||
import { DEFAULT_GLOBAL_SETTINGS } from "./types.js";
|
||||
import { readFile, rm, writeFile, mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
@@ -13,6 +13,7 @@ function makeTmpDir(): string {
|
||||
describe("GlobalSettingsStore", () => {
|
||||
let dir: string;
|
||||
let store: GlobalSettingsStore;
|
||||
const originalHome = process.env.HOME;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = makeTmpDir();
|
||||
@@ -21,6 +22,11 @@ describe("GlobalSettingsStore", () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
if (originalHome === undefined) {
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
process.env.HOME = originalHome;
|
||||
}
|
||||
});
|
||||
|
||||
describe("init()", () => {
|
||||
@@ -60,6 +66,30 @@ describe("GlobalSettingsStore", () => {
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.themeMode).toBe("light");
|
||||
});
|
||||
|
||||
it("adopts the legacy ~/.pi/kb directory when ~/.pi/fusion does not exist", async () => {
|
||||
const homeDir = makeTmpDir();
|
||||
process.env.HOME = homeDir;
|
||||
|
||||
const legacyDir = join(homeDir, ".pi", "kb");
|
||||
await mkdir(legacyDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(legacyDir, "settings.json"),
|
||||
JSON.stringify({ themeMode: "light" }),
|
||||
);
|
||||
|
||||
const defaultStore = new GlobalSettingsStore();
|
||||
await defaultStore.init();
|
||||
|
||||
expect(defaultStore.getSettingsPath()).toBe(join(defaultGlobalDir(), "settings.json"));
|
||||
expect(existsSync(join(homeDir, ".pi", "fusion", "settings.json"))).toBe(true);
|
||||
expect(existsSync(join(homeDir, ".pi", "kb"))).toBe(false);
|
||||
|
||||
const settings = await defaultStore.getSettings();
|
||||
expect(settings.themeMode).toBe("light");
|
||||
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSettings()", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Global settings store — manages user-level settings in `~/.pi/kb/settings.json`.
|
||||
* Global settings store — manages user-level settings in `~/.pi/fusion/settings.json`.
|
||||
*
|
||||
* Global settings persist across all kb projects for the current user.
|
||||
* They include UI theme preferences, default AI model selection, and
|
||||
@@ -9,17 +9,47 @@
|
||||
*/
|
||||
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { dirname, join } from "node:path";
|
||||
import { mkdir, readFile, writeFile, rename } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, renameSync } from "node:fs";
|
||||
import type { GlobalSettings } from "./types.js";
|
||||
import { DEFAULT_GLOBAL_SETTINGS } from "./types.js";
|
||||
|
||||
/** Default directory for global kb settings: `~/.pi/kb/` */
|
||||
export function defaultGlobalDir(): string {
|
||||
/** Legacy directory for global settings before the rename to fusion. */
|
||||
export function legacyGlobalDir(): string {
|
||||
return join(homedir(), ".pi", "kb");
|
||||
}
|
||||
|
||||
/** Default directory for global fusion settings: `~/.pi/fusion/` */
|
||||
export function defaultGlobalDir(): string {
|
||||
return join(homedir(), ".pi", "fusion");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the active global directory.
|
||||
*
|
||||
* If the new `~/.pi/fusion` directory does not exist but the legacy
|
||||
* `~/.pi/kb` directory does, move the legacy directory into place so
|
||||
* existing settings and central metadata continue to work after upgrade.
|
||||
*/
|
||||
export function resolveGlobalDir(dir?: string): string {
|
||||
if (dir) return dir;
|
||||
|
||||
const preferredDir = defaultGlobalDir();
|
||||
const legacyDir = legacyGlobalDir();
|
||||
|
||||
if (!existsSync(preferredDir) && existsSync(legacyDir)) {
|
||||
try {
|
||||
mkdirSync(dirname(preferredDir), { recursive: true });
|
||||
renameSync(legacyDir, preferredDir);
|
||||
} catch {
|
||||
return legacyDir;
|
||||
}
|
||||
}
|
||||
|
||||
return preferredDir;
|
||||
}
|
||||
|
||||
export class GlobalSettingsStore {
|
||||
private readonly settingsPath: string;
|
||||
private readonly dir: string;
|
||||
@@ -29,11 +59,11 @@ export class GlobalSettingsStore {
|
||||
|
||||
/**
|
||||
* Create a GlobalSettingsStore.
|
||||
* @param dir — Directory to store settings.json. Defaults to `~/.pi/kb/`.
|
||||
* @param dir — Directory to store settings.json. Defaults to `~/.pi/fusion/`.
|
||||
* Accepts a custom path for testing.
|
||||
*/
|
||||
constructor(dir?: string) {
|
||||
this.dir = dir ?? defaultGlobalDir();
|
||||
this.dir = resolveGlobalDir(dir);
|
||||
this.settingsPath = join(this.dir, "settings.json");
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ export class MigrationOrchestrator {
|
||||
* Detect existing kb projects by walking the filesystem.
|
||||
*
|
||||
* Scans from the starting path up to maxDepth levels deep, looking for
|
||||
* directories containing `.kb/kb.db`.
|
||||
* directories containing `.fusion/fusion.db` (or legacy `.fusion/fusion.db`)
|
||||
*
|
||||
* Security notes:
|
||||
* - Only scans from the specified startPath
|
||||
@@ -154,7 +154,7 @@ export class MigrationOrchestrator {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this directory is a kb project (has .kb/kb.db)
|
||||
// Check if this directory is a kb project (has .fusion/fusion.db or .fusion/fusion.db)
|
||||
const hasKbDb = this.isKbProject(dir);
|
||||
if (hasKbDb) {
|
||||
const name = this.generateProjectName(dir);
|
||||
@@ -201,15 +201,28 @@ export class MigrationOrchestrator {
|
||||
|
||||
/**
|
||||
* Check if a directory contains a valid kb project.
|
||||
* Validates that .kb/kb.db exists and is a file.
|
||||
* Validates that .fusion/fusion.db (or legacy .fusion/fusion.db) exists and is a file.
|
||||
*/
|
||||
private isKbProject(dir: string): boolean {
|
||||
const kbPath = join(dir, ".kb");
|
||||
// Check current layout: .fusion/fusion.db
|
||||
const fusionPath = join(dir, ".fusion");
|
||||
if (existsSync(fusionPath)) {
|
||||
const fusionDb = join(fusionPath, "fusion.db");
|
||||
if (existsSync(fusionDb)) {
|
||||
try {
|
||||
const stats = statSync(fusionDb);
|
||||
if (stats.isFile()) return true;
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to legacy layout: .fusion/fusion.db
|
||||
const kbPath = join(dir, ".fusion");
|
||||
if (!existsSync(kbPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dbPath = join(kbPath, "kb.db");
|
||||
const dbPath = join(kbPath, "fusion.db");
|
||||
if (!existsSync(dbPath)) {
|
||||
return false;
|
||||
}
|
||||
@@ -233,7 +246,7 @@ export class MigrationOrchestrator {
|
||||
/**
|
||||
* Auto-register detected projects in the central registry.
|
||||
*
|
||||
* - Filters to projects with valid kb.db
|
||||
* - Filters to projects with valid fusion.db
|
||||
* - Skips already-registered projects
|
||||
* - Generates unique names (appends number if conflict: name, name-2, name-3)
|
||||
* - Sets isolationMode to 'in-process' for migrated projects
|
||||
|
||||
@@ -23,10 +23,10 @@ function tempDir(prefix: string): string {
|
||||
|
||||
// Helper to create a fake kb project
|
||||
function createFakeKbProject(dir: string): void {
|
||||
const kbDir = join(dir, ".kb");
|
||||
const kbDir = join(dir, ".fusion");
|
||||
mkdirSync(kbDir, { recursive: true });
|
||||
// Create empty kb.db file (SQLite needs actual format, but for detection an empty file works)
|
||||
writeFileSync(join(kbDir, "kb.db"), "SQLite format 3\x00");
|
||||
// Create empty fusion.db file (SQLite needs actual format, but for detection an empty file works)
|
||||
writeFileSync(join(kbDir, "fusion.db"), "SQLite format 3\x00");
|
||||
}
|
||||
|
||||
// Helper to create a fake git remote
|
||||
@@ -64,7 +64,7 @@ describe("FirstRunDetector", () => {
|
||||
});
|
||||
|
||||
describe("detectFirstRunState", () => {
|
||||
it("should detect fresh-install when no central DB and no local .kb/", async () => {
|
||||
it("should detect fresh-install when no central DB and no local .fusion/", async () => {
|
||||
const tempProjectDir = tempDir("kb-fresh-");
|
||||
process.chdir(tempProjectDir);
|
||||
|
||||
@@ -76,7 +76,7 @@ describe("FirstRunDetector", () => {
|
||||
rmSync(tempProjectDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("should detect needs-migration when local .kb/ exists but no central DB", async () => {
|
||||
it("should detect needs-migration when local .fusion/ exists but no central DB", async () => {
|
||||
const tempProjectDir = tempDir("kb-needs-migration-");
|
||||
createFakeKbProject(tempProjectDir);
|
||||
process.chdir(tempProjectDir);
|
||||
@@ -159,7 +159,7 @@ describe("FirstRunDetector", () => {
|
||||
process.chdir(tempProjectDir);
|
||||
|
||||
mkdirSync(tempGlobalDir, { recursive: true });
|
||||
writeFileSync(join(tempGlobalDir, "kb-central.db"), "not a sqlite database");
|
||||
writeFileSync(join(tempGlobalDir, "fusion-central.db"), "not a sqlite database");
|
||||
|
||||
const detector = new FirstRunDetector(tempGlobalDir);
|
||||
const state = await detector.detectFirstRunState();
|
||||
@@ -174,7 +174,7 @@ describe("FirstRunDetector", () => {
|
||||
process.chdir(tempProjectDir);
|
||||
|
||||
mkdirSync(tempGlobalDir, { recursive: true });
|
||||
writeFileSync(join(tempGlobalDir, "kb-central.db"), "not a sqlite database");
|
||||
writeFileSync(join(tempGlobalDir, "fusion-central.db"), "not a sqlite database");
|
||||
|
||||
const detector = new FirstRunDetector(tempGlobalDir);
|
||||
const state = await detector.detectFirstRunState();
|
||||
@@ -216,7 +216,7 @@ describe("FirstRunDetector", () => {
|
||||
rmSync(tempProjectDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("should walk up directory tree to find .kb/", async () => {
|
||||
it("should walk up directory tree to find .fusion/", async () => {
|
||||
const tempProjectDir = tempDir("kb-parent-");
|
||||
createFakeKbProject(tempProjectDir);
|
||||
const nestedDir = join(tempProjectDir, "src", "components");
|
||||
@@ -304,7 +304,7 @@ describe("FirstRunDetector", () => {
|
||||
describe("getCentralDbPath", () => {
|
||||
it("should return correct path", () => {
|
||||
const detector = new FirstRunDetector(tempGlobalDir);
|
||||
expect(detector.getCentralDbPath()).toBe(join(tempGlobalDir, "kb-central.db"));
|
||||
expect(detector.getCentralDbPath()).toBe(join(tempGlobalDir, "fusion-central.db"));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -638,15 +638,15 @@ describe("MigrationCoordinator", () => {
|
||||
it("should return success for fresh-install state", async () => {
|
||||
// Close and remove central to simulate fresh state
|
||||
await central.close();
|
||||
rmSync(join(tempGlobalDir, "kb-central.db"), { force: true });
|
||||
rmSync(join(tempGlobalDir, "fusion-central.db"), { force: true });
|
||||
|
||||
// Create fresh temp dir with no .kb/
|
||||
// Create fresh temp dir with no .fusion/
|
||||
const tempFreshDir = tempDir("kb-fresh-coord-");
|
||||
|
||||
central = new CentralCore(tempGlobalDir);
|
||||
await central.init();
|
||||
|
||||
// Change to fresh dir (no .kb/)
|
||||
// Change to fresh dir (no .fusion/)
|
||||
const originalCwd = process.cwd();
|
||||
process.chdir(tempFreshDir);
|
||||
|
||||
@@ -820,7 +820,7 @@ describe("BackwardCompat", () => {
|
||||
it("should return true when no central DB", async () => {
|
||||
// Close and remove central DB
|
||||
await central.close();
|
||||
rmSync(join(tempGlobalDir, "kb-central.db"), { force: true });
|
||||
rmSync(join(tempGlobalDir, "fusion-central.db"), { force: true });
|
||||
|
||||
// Need to re-init CentralCore for it to work
|
||||
central = new CentralCore(tempGlobalDir);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Handles the transition from single-project to multi-project mode:
|
||||
* - Detects first-run state (fresh install, needs migration, setup wizard, normal)
|
||||
* - Auto-discovers existing .kb/ directories for migration
|
||||
* - Auto-discovers existing .fusion/ directories for migration
|
||||
* - Coordinates migration to central database
|
||||
* - Provides backward compatibility for single-project workflows
|
||||
*
|
||||
@@ -16,13 +16,14 @@ import { isAbsolute, join, resolve, basename, dirname } from "node:path";
|
||||
import type { CentralCore } 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 ────────────────────────────────────────────────────────────
|
||||
|
||||
/** First-run state detection results */
|
||||
export type FirstRunState =
|
||||
| "fresh-install" // No central DB, no .kb/ anywhere
|
||||
| "needs-migration" // No central DB, but .kb/kb.db exists in cwd
|
||||
| "fresh-install" // No central DB, no .fusion/ anywhere
|
||||
| "needs-migration" // No central DB, but .fusion/fusion.db exists in cwd
|
||||
| "setup-wizard" // Central DB exists but has zero projects
|
||||
| "normal-operation"; // Central DB exists with projects
|
||||
|
||||
@@ -32,7 +33,7 @@ export interface DetectedProject {
|
||||
path: string;
|
||||
/** Auto-generated or derived project name */
|
||||
name: string;
|
||||
/** Whether the project has a valid kb.db */
|
||||
/** Whether the project has a valid fusion.db */
|
||||
hasDb: boolean;
|
||||
}
|
||||
|
||||
@@ -93,7 +94,7 @@ export class FirstRunDetector {
|
||||
|
||||
/**
|
||||
* Create a FirstRunDetector.
|
||||
* @param globalDir — Directory for central database. Defaults to `~/.pi/kb/`.
|
||||
* @param globalDir — Directory for central database. Defaults to `~/.pi/fusion/`.
|
||||
*/
|
||||
constructor(globalDir?: string) {
|
||||
this.globalDir = globalDir ?? this.getDefaultGlobalDir();
|
||||
@@ -104,7 +105,7 @@ export class FirstRunDetector {
|
||||
*
|
||||
* Returns one of four states:
|
||||
* - `"fresh-install"` — No central DB and no kb project found from cwd upward
|
||||
* - `"needs-migration"` — No central DB, but a `.kb/kb.db` project exists in cwd ancestry
|
||||
* - `"needs-migration"` — No central DB, but a `.fusion/fusion.db` project exists in cwd ancestry
|
||||
* - `"setup-wizard"` — Central DB exists and can be read, but has zero projects
|
||||
* - `"normal-operation"` — Central DB exists with one or more projects
|
||||
*
|
||||
@@ -159,7 +160,7 @@ export class FirstRunDetector {
|
||||
* Check if the central database exists.
|
||||
*/
|
||||
hasCentralDb(): boolean {
|
||||
const centralDbPath = join(this.globalDir, "kb-central.db");
|
||||
const centralDbPath = join(this.globalDir, "fusion-central.db");
|
||||
return existsSync(centralDbPath);
|
||||
}
|
||||
|
||||
@@ -167,13 +168,13 @@ export class FirstRunDetector {
|
||||
* Get the path to the central database.
|
||||
*/
|
||||
getCentralDbPath(): string {
|
||||
return join(this.globalDir, "kb-central.db");
|
||||
return join(this.globalDir, "fusion-central.db");
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect existing projects by walking up the directory tree.
|
||||
*
|
||||
* Starting from `cwd`, walks up looking for `.kb/kb.db` files.
|
||||
* Starting from `cwd`, walks up looking for `.fusion/fusion.db` files.
|
||||
* Stops at home directory or root.
|
||||
*
|
||||
* @param cwd — Starting directory (default: process.cwd())
|
||||
@@ -283,8 +284,8 @@ export class FirstRunDetector {
|
||||
* Check if a directory contains a valid kb project.
|
||||
*/
|
||||
private hasKbProject(dir: string): boolean {
|
||||
const kbDir = join(dir, ".kb");
|
||||
const dbPath = join(kbDir, "kb.db");
|
||||
const kbDir = join(dir, ".fusion");
|
||||
const dbPath = join(kbDir, "fusion.db");
|
||||
|
||||
if (!existsSync(kbDir)) return false;
|
||||
if (!existsSync(dbPath)) return false;
|
||||
@@ -298,7 +299,7 @@ export class FirstRunDetector {
|
||||
}
|
||||
|
||||
private getDefaultGlobalDir(): string {
|
||||
return join(homedir(), ".pi", "kb");
|
||||
return resolveGlobalDir();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,8 +505,8 @@ export class MigrationCoordinator {
|
||||
}
|
||||
|
||||
private hasKbProject(dir: string): boolean {
|
||||
const kbDir = join(dir, ".kb");
|
||||
const dbPath = join(kbDir, "kb.db");
|
||||
const kbDir = join(dir, ".fusion");
|
||||
const dbPath = join(kbDir, "fusion.db");
|
||||
|
||||
if (!existsSync(kbDir)) return false;
|
||||
if (!existsSync(dbPath)) return false;
|
||||
|
||||
@@ -70,7 +70,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
/**
|
||||
* Creates a new MissionStore instance.
|
||||
*
|
||||
* @param kbDir - Path to the .kb directory (e.g., /path/to/project/.kb)
|
||||
* @param kbDir - Path to the .fusion directory (e.g., /path/to/project/.fusion)
|
||||
* @param db - Shared Database instance (same instance used by TaskStore)
|
||||
*/
|
||||
constructor(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Settings export and import functionality.
|
||||
*
|
||||
* This module provides utilities for exporting and importing kb settings,
|
||||
* supporting both global (~/.pi/kb/settings.json) and project-level (.kb/config.json)
|
||||
* supporting both global (~/.pi/fusion/settings.json) and project-level (.fusion/config.json)
|
||||
* settings for backup, migration, and sharing.
|
||||
*/
|
||||
|
||||
@@ -24,9 +24,9 @@ export interface SettingsExportData {
|
||||
exportedAt: string;
|
||||
/** Source identifier (e.g., hostname, project path) */
|
||||
source?: string;
|
||||
/** Global settings (user-level, ~/.pi/kb/settings.json) */
|
||||
/** Global settings (user-level, ~/.pi/fusion/settings.json) */
|
||||
global?: GlobalSettings;
|
||||
/** Project settings (project-level, .kb/config.json) */
|
||||
/** Project settings (project-level, .fusion/config.json) */
|
||||
project?: Partial<ProjectSettings>;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
/**
|
||||
* Hybrid storage note: task metadata lives in SQLite, while blob files remain on disk.
|
||||
* Any write to `.kb/tasks/{id}` must recreate the directory on demand, and any read from
|
||||
* Any write to `.fusion/tasks/{id}` must recreate the directory on demand, and any read from
|
||||
* optional blob files must tolerate missing files/directories because cleanup, migration,
|
||||
* or manual filesystem changes can remove them independently of the database row.
|
||||
*/
|
||||
@@ -81,7 +81,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
private taskLocks: Map<string, Promise<void>> = new Map();
|
||||
/** Promise chain for serializing config.json read-modify-write cycles */
|
||||
private configLock: Promise<void> = Promise.resolve();
|
||||
/** Global settings store (`~/.pi/kb/settings.json`) */
|
||||
/** Global settings store (`~/.pi/fusion/settings.json`) */
|
||||
private globalSettingsStore: GlobalSettingsStore;
|
||||
/** Polling interval for change detection */
|
||||
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -461,7 +461,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
* Get merged settings: global defaults ← global user prefs ← project overrides.
|
||||
*
|
||||
* Returns the combined view that most consumers should use. Project-level
|
||||
* values in `.kb/config.json` override global values from `~/.pi/kb/settings.json`.
|
||||
* values in `.fusion/config.json` override global values from `~/.pi/fusion/settings.json`.
|
||||
*/
|
||||
async getSettings(): Promise<Settings> {
|
||||
const [globalSettings, config] = await Promise.all([
|
||||
@@ -501,7 +501,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update project-level settings in `.kb/config.json`.
|
||||
* Update project-level settings in `.fusion/config.json`.
|
||||
*
|
||||
* Accepts `Partial<Settings>` for backward compatibility. Any global-only
|
||||
* fields in the patch are silently filtered out — they will not be persisted
|
||||
@@ -530,7 +530,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update global (user-level) settings in `~/.pi/kb/settings.json`.
|
||||
* Update global (user-level) settings in `~/.pi/fusion/settings.json`.
|
||||
*
|
||||
* These settings persist across all kb projects for the current user.
|
||||
* Only fields defined in `GlobalSettings` are accepted.
|
||||
@@ -1975,7 +1975,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
/**
|
||||
* Append an agent log entry to the task's agent log file (JSONL format).
|
||||
* Each entry is a single JSON line appended to `.kb/tasks/{ID}/agent.log`.
|
||||
* Each entry is a single JSON line appended to `.fusion/tasks/{ID}/agent.log`.
|
||||
* Also emits an `agent:log` event for live streaming.
|
||||
*
|
||||
* @param taskId - The task ID (e.g. "KB-001")
|
||||
|
||||
@@ -44,11 +44,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
constructor(private rootDir: string, globalSettingsDir?: string) {
|
||||
super();
|
||||
this.setMaxListeners(100);
|
||||
this.kbDir = join(rootDir, ".kb");
|
||||
this.tasksDir = join(this.kbDir, "tasks");
|
||||
this.configPath = join(this.kbDir, "config.json");
|
||||
this.archiveLogPath = join(this.kbDir, "archive.jsonl");
|
||||
this.activityLogPath = join(this.kbDir, "activity-log.jsonl");
|
||||
this.fusionDir = join(rootDir, ".fusion");
|
||||
this.tasksDir = join(this.fusionDir, "tasks");
|
||||
this.configPath = join(this.fusionDir, "config.json");
|
||||
this.archiveLogPath = join(this.fusionDir, "archive.jsonl");
|
||||
this.activityLogPath = join(this.fusionDir, "activity-log.jsonl");
|
||||
this.globalSettingsStore = new GlobalSettingsStore(globalSettingsDir);
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
* Get merged settings: global defaults ← global user prefs ← project overrides.
|
||||
*
|
||||
* Returns the combined view that most consumers should use. Project-level
|
||||
* values in `.kb/config.json` override global values from `~/.pi/kb/settings.json`.
|
||||
* values in `.fusion/config.json` override global values from `~/.pi/kb/settings.json`.
|
||||
*/
|
||||
async getSettings(): Promise<Settings> {
|
||||
const [globalSettings, config] = await Promise.all([
|
||||
@@ -280,7 +280,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update project-level settings in `.kb/config.json`.
|
||||
* Update project-level settings in `.fusion/config.json`.
|
||||
*
|
||||
* Accepts `Partial<Settings>` for backward compatibility. Any global-only
|
||||
* fields in the patch are silently filtered out — they will not be persisted
|
||||
@@ -1525,7 +1525,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
/**
|
||||
* Append an agent log entry to the task's agent log file (JSONL format).
|
||||
* Each entry is a single JSON line appended to `.kb/tasks/{ID}/agent.log`.
|
||||
* Each entry is a single JSON line appended to `.fusion/tasks/{ID}/agent.log`.
|
||||
* Also emits an `agent:log` event for live streaming.
|
||||
*
|
||||
* @param taskId - The task ID (e.g. "KB-001")
|
||||
@@ -2155,7 +2155,7 @@ ${notificationsSection}`;
|
||||
|
||||
/**
|
||||
* Record an activity log entry to the global activity log.
|
||||
* Appends to .kb/activity-log.jsonl (JSON Lines format).
|
||||
* Appends to .fusion/activity-log.jsonl (JSON Lines format).
|
||||
* Auto-generates ID and timestamp.
|
||||
*/
|
||||
async recordActivity(entry: Omit<ActivityLogEntry, "id" | "timestamp">): Promise<ActivityLogEntry> {
|
||||
|
||||
@@ -44,7 +44,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
constructor(private rootDir: string, globalSettingsDir?: string) {
|
||||
super();
|
||||
this.setMaxListeners(100);
|
||||
this.kbDir = join(rootDir, ".kb");
|
||||
this.tasksDir = join(this.kbDir, "tasks");
|
||||
this.configPath = join(this.kbDir, "config.json");
|
||||
this.archiveLogPath = join(this.kbDir, "archive.jsonl");
|
||||
this.fusionDir = join(rootDir, ".fusion");
|
||||
this.tasksDir = join(this.fusionDir, "tasks");
|
||||
this.configPath = join(this.fusionDir, "config.json");
|
||||
this.archiveLogPath = join(this.fusionDir, "archive.jsonl");
|
||||
|
||||
@@ -488,12 +488,12 @@ export interface TaskCreateInput {
|
||||
//
|
||||
// Settings are split into two scopes:
|
||||
//
|
||||
// 1. **GlobalSettings** — User preferences stored in `~/.pi/kb/settings.json`.
|
||||
// 1. **GlobalSettings** — User preferences stored in `~/.pi/fusion/settings.json`.
|
||||
// These persist across all kb projects for the current user (theme, default
|
||||
// AI models, notification preferences).
|
||||
//
|
||||
// 2. **ProjectSettings** — Project-specific workflow and resource settings stored
|
||||
// in `.kb/config.json`. These control how the engine operates for this
|
||||
// in `.fusion/config.json`. These control how the engine operates for this
|
||||
// particular project (concurrency, merge strategy, worktree management, etc.).
|
||||
//
|
||||
// The merged view (`Settings`) combines both scopes: project values override
|
||||
@@ -507,7 +507,7 @@ export interface TaskCreateInput {
|
||||
export type SettingsScope = "global" | "project";
|
||||
|
||||
/**
|
||||
* Global (user-level) settings stored in `~/.pi/kb/settings.json`.
|
||||
* Global (user-level) settings stored in `~/.pi/fusion/settings.json`.
|
||||
*
|
||||
* These are user preferences that persist across all kb projects.
|
||||
* The dashboard UI shows these under a "Global" section.
|
||||
@@ -556,7 +556,7 @@ export interface GlobalSettings {
|
||||
}
|
||||
|
||||
/**
|
||||
* Project-level settings stored in `.kb/config.json`.
|
||||
* Project-level settings stored in `.fusion/config.json`.
|
||||
*
|
||||
* These control how the engine operates for this particular project:
|
||||
* concurrency, merge strategy, worktree management, build/test commands, etc.
|
||||
@@ -672,7 +672,7 @@ export interface ProjectSettings {
|
||||
autoBackupSchedule?: string;
|
||||
/** Number of backup files to retain (oldest deleted when exceeded). Default: 7. */
|
||||
autoBackupRetention?: number;
|
||||
/** Directory for backup files, relative to project root. Default: ".kb/backups". */
|
||||
/** Directory for backup files, relative to project root. Default: ".fusion/backups". */
|
||||
autoBackupDir?: string;
|
||||
/** When true, tasks created without titles but with descriptions longer than 140
|
||||
* characters will automatically receive an AI-generated title (max 60 chars).
|
||||
@@ -754,7 +754,7 @@ export const DEFAULT_PROJECT_SETTINGS: ProjectSettings = {
|
||||
autoBackupEnabled: false,
|
||||
autoBackupSchedule: "0 2 * * *",
|
||||
autoBackupRetention: 7,
|
||||
autoBackupDir: ".kb/backups",
|
||||
autoBackupDir: ".fusion/backups",
|
||||
autoSummarizeTitles: false,
|
||||
titleSummarizerProvider: undefined,
|
||||
titleSummarizerModelId: undefined,
|
||||
@@ -1137,7 +1137,7 @@ export interface DetectedProject {
|
||||
path: string;
|
||||
/** Auto-generated or derived project name */
|
||||
name: string;
|
||||
/** Whether the project has a valid kb.db */
|
||||
/** Whether the project has a valid fusion.db */
|
||||
hasDb: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user