feat(FN-1644): rename default global data directory from ~/.pi/fusion to ~/.fusion

- Change default global directory from ~/.pi/fusion to ~/.fusion
- Add migration logic to copy existing data from old directory to new location
- Update all core packages (store, settings, central-core, central-db) to use new default path
- Update all documentation references from ~/.pi/fusion to ~/.fusion
- Add test for ~/.pi/fusion migration path with updated mock paths
- Include changeset for @gsxdsm/fusion minor version bump
This commit is contained in:
Fusion
2026-04-15 13:33:27 -07:00
committed by gsxdsm
parent d6681d0dc8
commit e874a3ca06
24 changed files with 107 additions and 48 deletions

View File

@@ -2,4 +2,4 @@
"@gsxdsm/fusion": minor
---
Rename project storage to `.fusion/fusion.db` and global storage to `~/.pi/fusion`.
Rename project storage to `.fusion/fusion.db` and global storage to `~/.fusion`.

View File

@@ -40,7 +40,7 @@ See [docs/storage.md](./docs/storage.md) for the full storage architecture docum
## Multi-Project Support
Fusion supports multiple projects with a central registry at `~/.pi/fusion/fusion-central.db`. Each project has its own SQLite database at `.fusion/fusion.db`. See [docs/multi-project.md](./docs/multi-project.md) for details on:
Fusion supports multiple projects with a central registry at `~/.fusion/fusion-central.db`. Each project has its own SQLite database at `.fusion/fusion.db`. See [docs/multi-project.md](./docs/multi-project.md) for details on:
- CentralCore API and project registration
- Isolation modes (in-process, child-process)
- Global concurrency management
@@ -220,7 +220,7 @@ See [docs/architecture.md](./docs/architecture.md) for the full reference includ
## Settings
fn uses a two-tier settings hierarchy:
- **Global settings** — User preferences in `~/.pi/fusion/settings.json` (theme, models, notifications)
- **Global settings** — User preferences in `~/.fusion/settings.json` (theme, models, notifications)
- **Project settings** — Project-specific settings in `.fusion/config.json` (concurrency, worktrees, commands)
Project settings override global settings. Configure via the dashboard **Settings** modal or `fn settings` CLI.

View File

@@ -49,7 +49,7 @@ At a high level, Fusion is split into:
│ Persistence │
│ - .fusion/fusion.db (SQLite/WAL)
│ - .fusion/tasks/* (PROMPT/logs)
│ - ~/.pi/fusion/fusion-central.db │
│ - ~/.fusion/fusion-central.db │
└──────────────────────────────────┘
```
@@ -134,7 +134,7 @@ Concrete references:
- Exported from `@fusion/core` for downstream persistence/API/UI work
- **CentralCore**: `packages/core/src/central-core.ts`
- Global project registry, health, central activity feed, global concurrency
- Backed by `packages/core/src/central-db.ts` (`~/.pi/fusion/fusion-central.db`)
- Backed by `packages/core/src/central-db.ts` (`~/.fusion/fusion-central.db`)
- **Specialized stores**:
- `AgentStore` (`agent-store.ts`) — filesystem-based agent metadata + heartbeat run history
- `MissionStore` (`mission-store.ts`) — mission/milestone/slice/feature hierarchy
@@ -383,7 +383,7 @@ SQLite schema is initialized in `packages/core/src/db.ts` and uses:
- `__meta.lastModified` for change detection/polling
### Central storage (multi-project)
- **Central DB**: `~/.pi/fusion/fusion-central.db`
- **Central DB**: `~/.fusion/fusion-central.db`
- Schema in `packages/core/src/central-db.ts`
- `projects`, `projectHealth`, `centralActivityLog`, `globalConcurrency`, `nodes`, `__meta`
@@ -542,7 +542,7 @@ In `packages/engine/src/ipc/ipc-protocol.ts`:
Settings are split by scope.
### Global scope
- File: `~/.pi/fusion/settings.json`
- File: `~/.fusion/settings.json`
- Managed by `GlobalSettingsStore` (`packages/core/src/global-settings.ts`)
- Examples: `themeMode`, `colorTheme`, default model/provider, notification preferences

View File

@@ -44,7 +44,7 @@ Method used:
- **High**
- `packages/dashboard/src/subtask-breakdown.ts` — AI/session orchestration and persistence interactions; only route-level behavior is exercised.
- `packages/dashboard/src/terminal.ts` — command validation + process spawning module has no tests and currently appears unused (no imports found), increasing drift risk.
- `packages/dashboard/src/script-store.ts` — file persistence logic under `~/.pi/fusion/scripts.json` has no direct tests.
- `packages/dashboard/src/script-store.ts` — file persistence logic under `~/.fusion/scripts.json` has no direct tests.
- **Medium**
- `packages/dashboard/src/mission-routes.ts` — no dedicated unit test file, but substantial endpoint coverage exists via `mission-e2e.test.ts`.
- `packages/engine/src/github.ts` — git remote parsing helper has no direct test despite influencing scheduler GitHub linking behavior.

View File

@@ -16,7 +16,7 @@ Use multi-project mode when you need to:
Multi-project metadata is stored in:
`~/.pi/fusion/fusion-central.db`
`~/.fusion/fusion-central.db`
Core tables:
@@ -86,7 +86,7 @@ Migration is idempotent and designed to avoid repeated re-registration.
If central registry behavior needs to be reverted:
1. Delete `~/.pi/fusion/fusion-central.db`
1. Delete `~/.fusion/fusion-central.db`
2. Keep using per-project `.fusion/fusion.db` data
3. Fusion falls back to legacy/single-project behavior
4. Re-register projects later with `fn init` / `fn project add`

View File

@@ -8,7 +8,7 @@ This guide documents Fusion settings from `packages/core/src/types.ts`.
Fusion uses a two-tier settings system:
- **Global settings** (`~/.pi/fusion/settings.json`): user preferences shared across projects
- **Global settings** (`~/.fusion/settings.json`): user preferences shared across projects
- **Project settings** (`.fusion/config.json`): execution/runtime behavior for one project
At runtime, settings are merged. **Project settings override global settings** when keys overlap.

View File

@@ -56,7 +56,7 @@ API endpoints reviewed:
- `PUT /api/settings/global`
- `GET /api/settings/scopes`
### 3.1 Global settings (`~/.pi/fusion/settings.json`)
### 3.1 Global settings (`~/.fusion/settings.json`)
| Setting Key | Scope | API Endpoint | Description |
|---|---|---|---|

View File

@@ -138,7 +138,7 @@ What the task should accomplish.
## Global Settings
User-level settings at `~/.pi/fusion/settings.json`:
User-level settings at `~/.fusion/settings.json`:
```json
{
@@ -152,7 +152,7 @@ User-level settings at `~/.pi/fusion/settings.json`:
## Central Database (Multi-Project)
For multi-project setups: `~/.pi/fusion/fusion-central.db`
For multi-project setups: `~/.fusion/fusion-central.db`
- Project registry
- Unified activity feed
- Global concurrency management

View File

@@ -458,7 +458,7 @@ vi.mock("@fusion/core", () => ({
PluginStore: mocks.pluginStoreCtor,
PluginLoader: mocks.pluginLoaderCtor,
GlobalSettingsStore: vi.fn().mockImplementation(() => mocks.globalSettingsStoreInstance),
resolveGlobalDir: vi.fn().mockReturnValue("/home/user/.pi/fusion"),
resolveGlobalDir: vi.fn().mockReturnValue("/home/user/.fusion"),
DaemonTokenManager: vi.fn().mockImplementation(() => ({
getToken: vi.fn().mockImplementation(() => Promise.resolve(mocks.globalSettingsData.daemonToken as string | undefined)),
generateToken: vi.fn().mockImplementation(() => {

View File

@@ -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/fusion/fusion-central.db`.
* The central database is located at `~/.fusion/fusion-central.db`.
*
* @example
* ```typescript
@@ -149,7 +149,7 @@ export class CentralCore extends EventEmitter<CentralCoreEvents> {
/**
* Create a CentralCore instance.
* @param globalDir — Directory for central database. Defaults to `~/.pi/fusion/`.
* @param globalDir — Directory for central database. Defaults to `~/.fusion/`.
* Accepts a custom path for testing.
*/
constructor(globalDir?: string) {

View File

@@ -5,7 +5,7 @@
* synchronous transaction handling. The database runs in WAL mode
* for concurrent reader/writer access.
*
* This database is stored at `~/.pi/fusion/fusion-central.db` and serves as the
* This database is stored at `~/.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.
*/
@@ -412,7 +412,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 fusion directory (e.g., `~/.pi/fusion/`)
* @param globalDir - Path to the global fusion directory (e.g., `~/.fusion/`)
* @returns CentralDatabase instance (not yet initialized)
*/
export function createCentralDatabase(globalDir?: string): CentralDatabase {

View File

@@ -600,7 +600,7 @@ async function createBackups(kbDir: string): Promise<void> {
* - 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/fusion/`.
* @param globalDir — Directory for central database. Defaults to `~/.fusion/`.
*/
export function needsCentralMigration(cwd: string, globalDir?: string): boolean {
const centralDbPath = join(resolveGlobalDir(globalDir), "fusion-central.db");
@@ -646,7 +646,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/fusion/`.
* @param globalDir — Directory for central database. Defaults to `~/.fusion/`.
* @returns Array of detected projects
*/
export async function detectExistingProjects(

View File

@@ -67,7 +67,7 @@ describe("GlobalSettingsStore", () => {
expect(settings.themeMode).toBe("light");
});
it("adopts the legacy ~/.pi/kb directory when ~/.pi/fusion does not exist", async () => {
it("adopts the legacy ~/.pi/kb directory when ~/.fusion does not exist", async () => {
const homeDir = makeTmpDir();
process.env.HOME = homeDir;
@@ -82,7 +82,7 @@ describe("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, ".fusion", "settings.json"))).toBe(true);
expect(existsSync(join(homeDir, ".pi", "kb"))).toBe(false);
const settings = await defaultStore.getSettings();
@@ -90,6 +90,38 @@ describe("GlobalSettingsStore", () => {
await rm(homeDir, { recursive: true, force: true });
});
it("adopts the legacy ~/.pi/fusion directory when ~/.fusion does not exist", async () => {
const homeDir = makeTmpDir();
process.env.HOME = homeDir;
// Create the legacy ~/.pi/fusion directory with settings
const legacyDir = join(homeDir, ".pi", "fusion");
await mkdir(legacyDir, { recursive: true });
await writeFile(
join(legacyDir, "settings.json"),
JSON.stringify({ themeMode: "light" }),
);
// Verify legacy exists and new does not
expect(existsSync(join(homeDir, ".pi", "fusion", "settings.json"))).toBe(true);
expect(existsSync(join(homeDir, ".fusion"))).toBe(false);
// Instantiate GlobalSettingsStore with no argument (uses default resolution)
const defaultStore = new GlobalSettingsStore();
await defaultStore.init();
// Verify migration happened
expect(defaultStore.getSettingsPath()).toBe(join(defaultGlobalDir(), "settings.json"));
expect(existsSync(join(homeDir, ".fusion", "settings.json"))).toBe(true);
expect(existsSync(join(homeDir, ".pi", "fusion"))).toBe(false);
// Verify settings were preserved
const settings = await defaultStore.getSettings();
expect(settings.themeMode).toBe("light");
await rm(homeDir, { recursive: true, force: true });
});
});
describe("getSettings()", () => {

View File

@@ -1,5 +1,5 @@
/**
* Global settings store — manages user-level settings in `~/.pi/fusion/settings.json`.
* Global settings store — manages user-level settings in `~/.fusion/settings.json`.
*
* Global settings persist across all fn projects for the current user.
* They include UI theme preferences, default AI model selection, and
@@ -20,38 +20,65 @@ import { existsSync, mkdirSync, renameSync } from "node:fs";
import type { GlobalSettings } from "./types.js";
import { DEFAULT_GLOBAL_SETTINGS } from "./types.js";
/** Legacy directory for global settings before the rename to fusion. */
/** Legacy directory for global settings (original name before rename to `.fusion`). */
export function legacyGlobalDir(): string {
return join(homedir(), ".pi", "fusion");
}
/** Legacy directory for global settings from the earliest fn version (`.pi/kb`). */
export function legacyGlobalDirOriginal(): string {
return join(homedir(), ".pi", "kb");
}
/** Default directory for global fusion settings: `~/.pi/fusion/` */
/** Default directory for global fusion settings: `~/.fusion/` */
export function defaultGlobalDir(): string {
return join(homedir(), ".pi", "fusion");
return join(homedir(), ".fusion");
}
/**
* Resolve the active global directory.
*
* If the new `~/.pi/fusion` directory does not exist but the legacy
* `~/.pi/fusion` directory does, move the legacy directory into place so
* existing settings and central metadata continue to work after upgrade.
* Migration chain:
* 1. If `~/.fusion` exists → use it
* 2. Else if `~/.pi/fusion` exists → rename to `~/.fusion` and use it
* 3. Else if `~/.pi/kb` exists → rename to `~/.fusion` and use it
* 4. Else → return `~/.fusion` (will be created on first use)
*/
export function resolveGlobalDir(dir?: string): string {
if (dir) return dir;
const preferredDir = defaultGlobalDir();
const legacyDir = legacyGlobalDir();
if (!existsSync(preferredDir) && existsSync(legacyDir)) {
// Case 1: New directory already exists
if (existsSync(preferredDir)) {
return preferredDir;
}
// Case 2: Check for legacy ~/.pi/fusion directory
const legacyDir = legacyGlobalDir();
if (existsSync(legacyDir)) {
try {
mkdirSync(dirname(preferredDir), { recursive: true });
renameSync(legacyDir, preferredDir);
return preferredDir;
} catch {
return legacyDir;
}
}
// Case 3: Check for original legacy ~/.pi/kb directory
const legacyDirOriginal = legacyGlobalDirOriginal();
if (existsSync(legacyDirOriginal)) {
try {
mkdirSync(dirname(preferredDir), { recursive: true });
renameSync(legacyDirOriginal, preferredDir);
return preferredDir;
} catch {
return legacyDirOriginal;
}
}
// Case 4: Return the preferred directory (will be created on first use)
return preferredDir;
}
@@ -67,7 +94,7 @@ export class GlobalSettingsStore {
/**
* Create a GlobalSettingsStore.
* @param dir — Directory to store settings.json. Defaults to `~/.pi/fusion/`.
* @param dir — Directory to store settings.json. Defaults to `~/.fusion/`.
* Accepts a custom path for testing.
*/
constructor(dir?: string) {

View File

@@ -2,7 +2,7 @@
* Settings export and import functionality.
*
* This module provides utilities for exporting and importing fn settings,
* supporting both global (~/.pi/fusion/settings.json) and project-level (.fusion/config.json)
* supporting both global (~/.fusion/settings.json) and project-level (.fusion/config.json)
* settings for backup, migration, and sharing.
*/
@@ -24,7 +24,7 @@ export interface SettingsExportData {
exportedAt: string;
/** Source identifier (e.g., hostname, project path) */
source?: string;
/** Global settings (user-level, ~/.pi/fusion/settings.json) */
/** Global settings (user-level, ~/.fusion/settings.json) */
global?: GlobalSettings;
/** Project settings (project-level, .fusion/config.json) */
project?: Partial<ProjectSettings>;

View File

@@ -114,7 +114,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
private configLock: Promise<void> = Promise.resolve();
/** Cached workflow steps — invalidated on create/update/delete */
private workflowStepsCache: import("./types.js").WorkflowStep[] | null = null;
/** Global settings store (`~/.pi/fusion/settings.json`) */
/** Global settings store (`~/.fusion/settings.json`) */
private globalSettingsStore: GlobalSettingsStore;
/** Polling interval for change detection */
private pollInterval: ReturnType<typeof setInterval> | null = null;
@@ -652,7 +652,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 `.fusion/config.json` override global values from `~/.pi/fusion/settings.json`.
* values in `.fusion/config.json` override global values from `~/.fusion/settings.json`.
*
* Settings are canonicalized to resolve legacy defaults (e.g., `.kb/backups` → `.fusion/backups`).
*/
@@ -842,7 +842,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
/**
* Update global (user-level) settings in `~/.pi/fusion/settings.json`.
* Update global (user-level) settings in `~/.fusion/settings.json`.
*
* These settings persist across all fn projects for the current user.
* Only fields defined in `GlobalSettings` are accepted.

View File

@@ -807,7 +807,7 @@ export interface TaskCreateInput {
//
// Settings are split into two scopes:
//
// 1. **GlobalSettings** — User preferences stored in `~/.pi/fusion/settings.json`.
// 1. **GlobalSettings** — User preferences stored in `~/.fusion/settings.json`.
// These persist across all fn projects for the current user (theme, default
// AI models, notification preferences).
//
@@ -840,7 +840,7 @@ export interface DaemonTokenSettings {
}
/**
* Global (user-level) settings stored in `~/.pi/fusion/settings.json`.
* Global (user-level) settings stored in `~/.fusion/settings.json`.
*
* These are user preferences that persist across all fn projects.
* The dashboard UI shows these under a "Global" section.

View File

@@ -743,7 +743,7 @@ The dashboard exposes run-audit retrieval and correlation endpoints for inspecti
- `GET /api/config` - Server configuration
- `GET /api/settings` - Merged settings (project overrides global)
- `PUT /api/settings` - Update project-level settings (rejects global-only fields)
- `GET /api/settings/global` - Global user settings (~/.pi/fusion/settings.json)
- `GET /api/settings/global` - Global user settings (~/.fusion/settings.json)
- `PUT /api/settings/global` - Update global user settings
- `GET /api/settings/scopes` - Settings separated by scope: { global, project }
- `GET /api/models` - Available AI models

View File

@@ -344,7 +344,7 @@ export function fetchMemoryBackendStatus(projectId?: string): Promise<MemoryBack
return api<MemoryBackendStatus>(withProjectId("/memory/backend", projectId));
}
/** Fetch global (user-level) settings from ~/.pi/fusion/settings.json */
/** Fetch global (user-level) settings from ~/.fusion/settings.json */
export function fetchGlobalSettings(): Promise<GlobalSettings> {
return api<GlobalSettings>("/settings/global");
}

View File

@@ -18,7 +18,7 @@ import { applyPresetToSelection, generateUniquePresetId } from "../utils/modelPr
*
* Each section groups related settings fields under a sidebar nav item.
* Sections have a `scope` to indicate where their settings are stored:
* - "global": User-level settings stored in ~/.pi/fusion/settings.json (shared across projects)
* - "global": User-level settings stored in ~/.fusion/settings.json (shared across projects)
* - "project": Project-specific settings stored in .fusion/config.json
* - undefined: Section operates independently of settings storage (e.g. authentication)
*

View File

@@ -182,7 +182,7 @@ function unloadThemeDataStylesheet(): void {
/**
* Custom hook for theme management.
*
* Source of truth: backend global settings (`~/.pi/fusion/settings.json`).
* Source of truth: backend global settings (`~/.fusion/settings.json`).
*
* Behavior:
* - Initializes from localStorage cache to avoid pre-hydration theme flash

View File

@@ -91,7 +91,7 @@ function createMockGlobalSettingsStore() {
return {
getSettings: vi.fn().mockResolvedValue({}),
updateSettings: vi.fn().mockResolvedValue({}),
getSettingsPath: vi.fn().mockReturnValue("/fake/home/.pi/fusion/settings.json"),
getSettingsPath: vi.fn().mockReturnValue("/fake/home/.fusion/settings.json"),
init: vi.fn().mockResolvedValue(false),
};
}

View File

@@ -2147,7 +2147,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
/**
* GET /api/settings/global
* Returns the global (user-level) settings from ~/.pi/fusion/settings.json.
* Returns the global (user-level) settings from ~/.fusion/settings.json.
* Does NOT include computed/server-only fields like githubTokenConfigured.
*/
router.get("/settings/global", async (_req, res) => {
@@ -2165,7 +2165,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
/**
* PUT /api/settings/global
* Update global (user-level) settings in ~/.pi/fusion/settings.json.
* Update global (user-level) settings in ~/.fusion/settings.json.
* These settings persist across all fn projects for the current user.
*/
router.put("/settings/global", async (req, res) => {

View File

@@ -10,7 +10,7 @@ function createMockGlobalSettingsStore() {
return {
getSettings: vi.fn().mockResolvedValue({}),
updateSettings: vi.fn().mockResolvedValue({}),
getSettingsPath: vi.fn().mockReturnValue("/fake/home/.pi/fusion/settings.json"),
getSettingsPath: vi.fn().mockReturnValue("/fake/home/.fusion/settings.json"),
init: vi.fn().mockResolvedValue(false),
};
}