feat: auto-initialize config files on Electron app startup

Ensure models.json, mcp.json, and security.json config files are created
when the app starts, before the Settings UI can access them.

- Add ensureConfig() method to IModelConfigRepo and IMcpConfigRepo interfaces
- Add async ensureSecurityConfig() function for security config initialization
- Create initConfigs.ts with centralized initialization that calls all config ensure methods
- Call initConfigs() in main.ts before setupIpcHandlers()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-01-29 16:17:51 +05:30
parent 7a59b28651
commit f5cc803340
6 changed files with 56 additions and 15 deletions

View file

@ -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<void> {
// Resolve repos and explicitly call their ensureConfig methods
const modelConfigRepo = container.resolve<IModelConfigRepo>("modelConfigRepo");
const mcpConfigRepo = container.resolve<IMcpConfigRepo>("mcpConfigRepo");
await Promise.all([
modelConfigRepo.ensureConfig(),
mcpConfigRepo.ensureConfig(),
ensureSecurityConfig(),
]);
}

View file

@ -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<void> {
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) {

View file

@ -2,4 +2,7 @@
export * as workspace from './workspace/workspace.js';
// Workspace watcher
export * as watcher from './workspace/watcher.js';
export * as watcher from './workspace/watcher.js';
// Config initialization
export { initConfigs } from './config/initConfigs.js';

View file

@ -5,6 +5,7 @@ import path from "path";
import z from "zod";
export interface IMcpConfigRepo {
ensureConfig(): Promise<void>;
getConfig(): Promise<z.infer<typeof McpServerConfig>>;
upsert(serverName: string, config: z.infer<typeof McpServerDefinition>): Promise<void>;
delete(serverName: string): Promise<void>;
@ -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<void> {
async ensureConfig(): Promise<void> {
try {
await fs.access(this.configPath);
} catch {

View file

@ -5,6 +5,7 @@ import path from "path";
import z from "zod";
export interface IModelConfigRepo {
ensureConfig(): Promise<void>;
getConfig(): Promise<z.infer<typeof ModelConfig>>;
upsert(providerName: string, config: z.infer<typeof Provider>): Promise<void>;
delete(providerName: string): Promise<void>;
@ -26,11 +27,7 @@ const defaultConfig: z.infer<typeof ModelConfig> = {
export class FSModelConfigRepo implements IModelConfigRepo {
private readonly configPath = path.join(WorkDir, "config", "models.json");
constructor() {
this.ensureDefaultConfig();
}
private async ensureDefaultConfig(): Promise<void> {
async ensureConfig(): Promise<void> {
try {
await fs.access(this.configPath);
} catch {