diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index 7ae6ed46..6ddab7bc 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -10,6 +10,7 @@ import { init as initFirefliesSync } from "@x/core/dist/knowledge/sync_fireflies import { init as initGranolaSync } from "@x/core/dist/knowledge/granola/sync.js"; import { init as initGraphBuilder } from "@x/core/dist/knowledge/build_graph.js"; import { init as initPreBuiltRunner } from "@x/core/dist/pre_built/runner.js"; +import { initConfigs } from "@x/core/dist/config/initConfigs.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -96,7 +97,7 @@ function createWindow() { } } -app.whenReady().then(() => { +app.whenReady().then(async () => { // Register custom protocol before creating window (for production builds) if (app.isPackaged) { registerAppProtocol(); @@ -113,6 +114,9 @@ app.whenReady().then(() => { }); } + // Initialize all config files before UI can access them + await initConfigs(); + setupIpcHandlers(); createWindow(); diff --git a/apps/x/packages/core/src/config/initConfigs.ts b/apps/x/packages/core/src/config/initConfigs.ts new file mode 100644 index 00000000..1c447e37 --- /dev/null +++ b/apps/x/packages/core/src/config/initConfigs.ts @@ -0,0 +1,20 @@ +import container from "../di/container.js"; +import type { IModelConfigRepo } from "../models/repo.js"; +import type { IMcpConfigRepo } from "../mcp/repo.js"; +import { ensureSecurityConfig } from "./security.js"; + +/** + * Initialize all config files at app startup. + * Ensures config files exist before the UI might access them. + */ +export async function initConfigs(): Promise { + // Resolve repos and explicitly call their ensureConfig methods + const modelConfigRepo = container.resolve("modelConfigRepo"); + const mcpConfigRepo = container.resolve("mcpConfigRepo"); + + await Promise.all([ + modelConfigRepo.ensureConfig(), + mcpConfigRepo.ensureConfig(), + ensureSecurityConfig(), + ]); +} diff --git a/apps/x/packages/core/src/config/security.ts b/apps/x/packages/core/src/config/security.ts index 9419e76e..d69eb241 100644 --- a/apps/x/packages/core/src/config/security.ts +++ b/apps/x/packages/core/src/config/security.ts @@ -1,5 +1,6 @@ import path from "path"; import fs from "fs"; +import fsPromises from "fs/promises"; import { WorkDir } from "./config.js"; export const SECURITY_CONFIG_PATH = path.join(WorkDir, "config", "security.json"); @@ -19,7 +20,26 @@ const DEFAULT_ALLOW_LIST = [ let cachedAllowList: string[] | null = null; let cachedMtimeMs: number | null = null; -function ensureSecurityConfig() { +/** + * Async function to ensure security config file exists. + * Called explicitly at app startup via initConfigs(). + */ +export async function ensureSecurityConfig(): Promise { + try { + await fsPromises.access(SECURITY_CONFIG_PATH); + } catch { + await fsPromises.writeFile( + SECURITY_CONFIG_PATH, + JSON.stringify(DEFAULT_ALLOW_LIST, null, 2) + "\n", + "utf8", + ); + } +} + +/** + * Sync version for internal use by getSecurityAllowList() and readAllowList(). + */ +function ensureSecurityConfigSync() { if (!fs.existsSync(SECURITY_CONFIG_PATH)) { fs.writeFileSync( SECURITY_CONFIG_PATH, @@ -63,7 +83,7 @@ function parseSecurityPayload(payload: unknown): string[] { } function readAllowList(): string[] { - ensureSecurityConfig(); + ensureSecurityConfigSync(); try { const configContent = fs.readFileSync(SECURITY_CONFIG_PATH, "utf8"); @@ -76,7 +96,7 @@ function readAllowList(): string[] { } export function getSecurityAllowList(): string[] { - ensureSecurityConfig(); + ensureSecurityConfigSync(); try { const stats = fs.statSync(SECURITY_CONFIG_PATH); if (cachedAllowList && cachedMtimeMs === stats.mtimeMs) { diff --git a/apps/x/packages/core/src/index.ts b/apps/x/packages/core/src/index.ts index b004d263..b7718cba 100644 --- a/apps/x/packages/core/src/index.ts +++ b/apps/x/packages/core/src/index.ts @@ -2,4 +2,7 @@ export * as workspace from './workspace/workspace.js'; // Workspace watcher -export * as watcher from './workspace/watcher.js'; \ No newline at end of file +export * as watcher from './workspace/watcher.js'; + +// Config initialization +export { initConfigs } from './config/initConfigs.js'; \ No newline at end of file diff --git a/apps/x/packages/core/src/mcp/repo.ts b/apps/x/packages/core/src/mcp/repo.ts index b3ebee33..841f162b 100644 --- a/apps/x/packages/core/src/mcp/repo.ts +++ b/apps/x/packages/core/src/mcp/repo.ts @@ -5,6 +5,7 @@ import path from "path"; import z from "zod"; export interface IMcpConfigRepo { + ensureConfig(): Promise; getConfig(): Promise>; upsert(serverName: string, config: z.infer): Promise; delete(serverName: string): Promise; @@ -13,11 +14,7 @@ export interface IMcpConfigRepo { export class FSMcpConfigRepo implements IMcpConfigRepo { private readonly configPath = path.join(WorkDir, "config", "mcp.json"); - constructor() { - this.ensureDefaultConfig(); - } - - private async ensureDefaultConfig(): Promise { + async ensureConfig(): Promise { try { await fs.access(this.configPath); } catch { diff --git a/apps/x/packages/core/src/models/repo.ts b/apps/x/packages/core/src/models/repo.ts index cc60937e..33ad2502 100644 --- a/apps/x/packages/core/src/models/repo.ts +++ b/apps/x/packages/core/src/models/repo.ts @@ -5,6 +5,7 @@ import path from "path"; import z from "zod"; export interface IModelConfigRepo { + ensureConfig(): Promise; getConfig(): Promise>; upsert(providerName: string, config: z.infer): Promise; delete(providerName: string): Promise; @@ -26,11 +27,7 @@ const defaultConfig: z.infer = { export class FSModelConfigRepo implements IModelConfigRepo { private readonly configPath = path.join(WorkDir, "config", "models.json"); - constructor() { - this.ensureDefaultConfig(); - } - - private async ensureDefaultConfig(): Promise { + async ensureConfig(): Promise { try { await fs.access(this.configPath); } catch {