feat(FN-1798): merge fusion/fn-1798
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { mkdtempSync, existsSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { mkdtempSync, existsSync, rmSync, writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { runInit } from "./init.js";
|
||||
@@ -123,4 +123,38 @@ describe("init command", () => {
|
||||
expect(existsSync(fusionDir)).toBe(true);
|
||||
expect(existsSync(join(fusionDir, "fusion.db"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should add .fusion to .gitignore when it doesn't exist", async () => {
|
||||
const gitignorePath = join(tempProjectDir, ".gitignore");
|
||||
expect(existsSync(gitignorePath)).toBe(false);
|
||||
|
||||
await runInit({ path: tempProjectDir });
|
||||
|
||||
expect(existsSync(gitignorePath)).toBe(true);
|
||||
const content = readFileSync(gitignorePath, "utf-8");
|
||||
expect(content).toContain(".fusion");
|
||||
});
|
||||
|
||||
it("should append .fusion to existing .gitignore", async () => {
|
||||
const gitignorePath = join(tempProjectDir, ".gitignore");
|
||||
writeFileSync(gitignorePath, "node_modules\ndist\n");
|
||||
|
||||
await runInit({ path: tempProjectDir });
|
||||
|
||||
const content = readFileSync(gitignorePath, "utf-8");
|
||||
expect(content).toContain("node_modules");
|
||||
expect(content).toContain("dist");
|
||||
expect(content).toContain(".fusion");
|
||||
});
|
||||
|
||||
it("should not duplicate .fusion in .gitignore (idempotent)", async () => {
|
||||
const gitignorePath = join(tempProjectDir, ".gitignore");
|
||||
writeFileSync(gitignorePath, "node_modules\n.fusion\n");
|
||||
|
||||
await runInit({ path: tempProjectDir });
|
||||
|
||||
const content = readFileSync(gitignorePath, "utf-8");
|
||||
const fusionMatches = content.match(/\.fusion/g);
|
||||
expect(fusionMatches).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* Idempotent: if already initialized, reports success without recreating.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
||||
import { join, resolve, basename } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import { exec } from "node:child_process";
|
||||
@@ -75,6 +75,9 @@ export async function runInit(options: InitOptions = {}): Promise<void> {
|
||||
console.log(` ✓ Created .fusion/ directory`);
|
||||
}
|
||||
|
||||
// Add .fusion to .gitignore
|
||||
await addFusionToGitignore(cwd);
|
||||
|
||||
// Create fusion.db (empty SQLite file)
|
||||
if (!existsSync(dbPath)) {
|
||||
// SQLite database header for an empty database
|
||||
@@ -159,3 +162,36 @@ async function detectProjectName(dir: string): Promise<string> {
|
||||
// Fallback to directory name
|
||||
return basename(dir) || "my-project";
|
||||
}
|
||||
|
||||
/**
|
||||
* Add .fusion to .gitignore if not already present.
|
||||
* Idempotent: only adds if not already in the file.
|
||||
*/
|
||||
async function addFusionToGitignore(cwd: string): Promise<void> {
|
||||
const gitignorePath = join(cwd, ".gitignore");
|
||||
|
||||
let content = "";
|
||||
if (existsSync(gitignorePath)) {
|
||||
try {
|
||||
content = readFileSync(gitignorePath, "utf-8");
|
||||
} catch {
|
||||
// Best-effort: if we can't read, treat as empty
|
||||
}
|
||||
}
|
||||
|
||||
// Check if .fusion is already in the file
|
||||
const lines = content.split(/\r?\n/);
|
||||
if (lines.some((line) => line.trim() === ".fusion")) {
|
||||
return; // Already present, skip
|
||||
}
|
||||
|
||||
// Append .fusion to .gitignore
|
||||
const newContent = content.endsWith("\n") ? content + ".fusion\n" : content + "\n.fusion\n";
|
||||
try {
|
||||
writeFileSync(gitignorePath, newContent);
|
||||
console.log(` ✓ Added .fusion to .gitignore`);
|
||||
} catch {
|
||||
// Best-effort: don't fail init if we can't write to .gitignore
|
||||
console.log(` ⚠ Could not update .gitignore (best-effort)`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,11 @@ function writeJson(path: string, value: Record<string, unknown>): void {
|
||||
}
|
||||
|
||||
describe("createReadOnlyProviderSettingsView", () => {
|
||||
it("reads provider package settings from .pi and .fusion with .fusion taking precedence", () => {
|
||||
it("reads provider package settings from .fusion/settings.json", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "fusion-provider-settings-"));
|
||||
const cwd = join(root, "project");
|
||||
const agentDir = join(root, "agent");
|
||||
|
||||
mkdirSync(join(cwd, ".pi"), { recursive: true });
|
||||
mkdirSync(join(cwd, ".fusion"), { recursive: true });
|
||||
mkdirSync(agentDir, { recursive: true });
|
||||
|
||||
@@ -22,11 +21,6 @@ describe("createReadOnlyProviderSettingsView", () => {
|
||||
npmCommand: ["pnpm"],
|
||||
globalOnly: true,
|
||||
});
|
||||
writeJson(join(cwd, ".pi", "settings.json"), {
|
||||
npmCommand: ["npm"],
|
||||
extensions: [{ name: "pi-provider", enabled: true }],
|
||||
shared: "pi",
|
||||
});
|
||||
writeJson(join(cwd, ".fusion", "settings.json"), {
|
||||
extensions: [{ name: "fusion-provider", enabled: true }],
|
||||
shared: "fusion",
|
||||
@@ -42,26 +36,24 @@ describe("createReadOnlyProviderSettingsView", () => {
|
||||
extensions: [{ name: "fusion-provider", enabled: true }],
|
||||
shared: "fusion",
|
||||
});
|
||||
expect(view.getNpmCommand()).toEqual(["npm"]);
|
||||
expect(view.getNpmCommand()).toEqual(["pnpm"]);
|
||||
});
|
||||
|
||||
it("falls back to .pi settings when .fusion settings do not exist", () => {
|
||||
it("returns empty project settings when .fusion/settings.json does not exist", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "fusion-provider-settings-"));
|
||||
const cwd = join(root, "project");
|
||||
const agentDir = join(root, "agent");
|
||||
|
||||
mkdirSync(join(cwd, ".pi"), { recursive: true });
|
||||
mkdirSync(agentDir, { recursive: true });
|
||||
|
||||
writeJson(join(cwd, ".pi", "settings.json"), {
|
||||
extensions: [{ name: "pi-provider", enabled: true }],
|
||||
writeJson(join(agentDir, "settings.json"), {
|
||||
npmCommand: ["pnpm"],
|
||||
});
|
||||
|
||||
const view = createReadOnlyProviderSettingsView(cwd, agentDir);
|
||||
|
||||
expect(view.getProjectSettings()).toMatchObject({
|
||||
extensions: [{ name: "pi-provider", enabled: true }],
|
||||
});
|
||||
expect(view.getProjectSettings()).toEqual({});
|
||||
expect(view.getNpmCommand()).toEqual(["pnpm"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,26 +77,7 @@ describe("createProjectSettingsPersistence", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to .pi/settings.json when .fusion/settings.json does not exist", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "fusion-provider-settings-"));
|
||||
const cwd = join(root, "project");
|
||||
|
||||
mkdirSync(join(cwd, ".pi"), { recursive: true });
|
||||
writeJson(join(cwd, ".pi", "settings.json"), {
|
||||
skills: ["-other-skill"],
|
||||
npmCommand: ["npm"],
|
||||
});
|
||||
|
||||
const persistence = createProjectSettingsPersistence(cwd);
|
||||
const settings = persistence.read();
|
||||
|
||||
expect(settings).toEqual({
|
||||
skills: ["-other-skill"],
|
||||
npmCommand: ["npm"],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns empty object when neither settings file exists", () => {
|
||||
it("returns empty object when .fusion/settings.json does not exist", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "fusion-provider-settings-"));
|
||||
const cwd = join(root, "project");
|
||||
|
||||
|
||||
@@ -22,14 +22,12 @@ function readJsonObject(path: string): Record<string, any> {
|
||||
|
||||
export function createReadOnlyProviderSettingsView(cwd: string, agentDir: string): PackageManagerSettingsView {
|
||||
const globalSettings = readJsonObject(join(agentDir, "settings.json"));
|
||||
const legacyProjectSettings = readJsonObject(join(cwd, ".pi", "settings.json"));
|
||||
const fusionProjectSettings = readJsonObject(join(cwd, ".fusion", "settings.json"));
|
||||
const projectSettings = { ...legacyProjectSettings, ...fusionProjectSettings };
|
||||
const mergedSettings = { ...globalSettings, ...projectSettings };
|
||||
const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
|
||||
|
||||
return {
|
||||
getGlobalSettings: () => structuredClone(globalSettings),
|
||||
getProjectSettings: () => structuredClone(projectSettings),
|
||||
getProjectSettings: () => structuredClone(fusionProjectSettings),
|
||||
getNpmCommand: () => Array.isArray(mergedSettings.npmCommand)
|
||||
? [...mergedSettings.npmCommand]
|
||||
: undefined,
|
||||
@@ -39,8 +37,7 @@ export function createReadOnlyProviderSettingsView(cwd: string, agentDir: string
|
||||
/**
|
||||
* Project settings persistence helper.
|
||||
*
|
||||
* Reads from and writes to `.fusion/settings.json` with fallback to `.pi/settings.json`
|
||||
* for backward compatibility. Changes are always written to `.fusion/settings.json`.
|
||||
* Reads from and writes to `.fusion/settings.json`.
|
||||
*
|
||||
* @param projectPath - Absolute path to the project root
|
||||
* @returns Object with read/write methods for project settings
|
||||
@@ -54,23 +51,13 @@ export function createProjectSettingsPersistence(projectPath: string): {
|
||||
getSettingsPath(): string;
|
||||
} {
|
||||
const fusionSettingsPath = join(projectPath, ".fusion", "settings.json");
|
||||
const legacySettingsPath = join(projectPath, ".pi", "settings.json");
|
||||
|
||||
function readSettings(): Record<string, any> {
|
||||
// Try .fusion first
|
||||
if (existsSync(fusionSettingsPath)) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(fusionSettingsPath, "utf-8")) as Record<string, any>;
|
||||
} catch {
|
||||
// Fall through to legacy
|
||||
}
|
||||
}
|
||||
// Fall back to .pi
|
||||
if (existsSync(legacySettingsPath)) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(legacySettingsPath, "utf-8")) as Record<string, any>;
|
||||
} catch {
|
||||
// Return empty
|
||||
// Return empty on parse error
|
||||
}
|
||||
}
|
||||
return {};
|
||||
|
||||
Reference in New Issue
Block a user