mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-15 21:11:08 +02:00
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:
parent
7a59b28651
commit
f5cc803340
6 changed files with 56 additions and 15 deletions
|
|
@ -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 initGranolaSync } from "@x/core/dist/knowledge/granola/sync.js";
|
||||||
import { init as initGraphBuilder } from "@x/core/dist/knowledge/build_graph.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 { 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 __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
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)
|
// Register custom protocol before creating window (for production builds)
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
registerAppProtocol();
|
registerAppProtocol();
|
||||||
|
|
@ -113,6 +114,9 @@ app.whenReady().then(() => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize all config files before UI can access them
|
||||||
|
await initConfigs();
|
||||||
|
|
||||||
setupIpcHandlers();
|
setupIpcHandlers();
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
|
||||||
20
apps/x/packages/core/src/config/initConfigs.ts
Normal file
20
apps/x/packages/core/src/config/initConfigs.ts
Normal 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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import fsPromises from "fs/promises";
|
||||||
import { WorkDir } from "./config.js";
|
import { WorkDir } from "./config.js";
|
||||||
|
|
||||||
export const SECURITY_CONFIG_PATH = path.join(WorkDir, "config", "security.json");
|
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 cachedAllowList: string[] | null = null;
|
||||||
let cachedMtimeMs: number | 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)) {
|
if (!fs.existsSync(SECURITY_CONFIG_PATH)) {
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
SECURITY_CONFIG_PATH,
|
SECURITY_CONFIG_PATH,
|
||||||
|
|
@ -63,7 +83,7 @@ function parseSecurityPayload(payload: unknown): string[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
function readAllowList(): string[] {
|
function readAllowList(): string[] {
|
||||||
ensureSecurityConfig();
|
ensureSecurityConfigSync();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const configContent = fs.readFileSync(SECURITY_CONFIG_PATH, "utf8");
|
const configContent = fs.readFileSync(SECURITY_CONFIG_PATH, "utf8");
|
||||||
|
|
@ -76,7 +96,7 @@ function readAllowList(): string[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSecurityAllowList(): string[] {
|
export function getSecurityAllowList(): string[] {
|
||||||
ensureSecurityConfig();
|
ensureSecurityConfigSync();
|
||||||
try {
|
try {
|
||||||
const stats = fs.statSync(SECURITY_CONFIG_PATH);
|
const stats = fs.statSync(SECURITY_CONFIG_PATH);
|
||||||
if (cachedAllowList && cachedMtimeMs === stats.mtimeMs) {
|
if (cachedAllowList && cachedMtimeMs === stats.mtimeMs) {
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,6 @@ export * as workspace from './workspace/workspace.js';
|
||||||
|
|
||||||
// Workspace watcher
|
// Workspace watcher
|
||||||
export * as watcher from './workspace/watcher.js';
|
export * as watcher from './workspace/watcher.js';
|
||||||
|
|
||||||
|
// Config initialization
|
||||||
|
export { initConfigs } from './config/initConfigs.js';
|
||||||
|
|
@ -5,6 +5,7 @@ import path from "path";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
export interface IMcpConfigRepo {
|
export interface IMcpConfigRepo {
|
||||||
|
ensureConfig(): Promise<void>;
|
||||||
getConfig(): Promise<z.infer<typeof McpServerConfig>>;
|
getConfig(): Promise<z.infer<typeof McpServerConfig>>;
|
||||||
upsert(serverName: string, config: z.infer<typeof McpServerDefinition>): Promise<void>;
|
upsert(serverName: string, config: z.infer<typeof McpServerDefinition>): Promise<void>;
|
||||||
delete(serverName: string): Promise<void>;
|
delete(serverName: string): Promise<void>;
|
||||||
|
|
@ -13,11 +14,7 @@ export interface IMcpConfigRepo {
|
||||||
export class FSMcpConfigRepo implements IMcpConfigRepo {
|
export class FSMcpConfigRepo implements IMcpConfigRepo {
|
||||||
private readonly configPath = path.join(WorkDir, "config", "mcp.json");
|
private readonly configPath = path.join(WorkDir, "config", "mcp.json");
|
||||||
|
|
||||||
constructor() {
|
async ensureConfig(): Promise<void> {
|
||||||
this.ensureDefaultConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensureDefaultConfig(): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
await fs.access(this.configPath);
|
await fs.access(this.configPath);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import path from "path";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
export interface IModelConfigRepo {
|
export interface IModelConfigRepo {
|
||||||
|
ensureConfig(): Promise<void>;
|
||||||
getConfig(): Promise<z.infer<typeof ModelConfig>>;
|
getConfig(): Promise<z.infer<typeof ModelConfig>>;
|
||||||
upsert(providerName: string, config: z.infer<typeof Provider>): Promise<void>;
|
upsert(providerName: string, config: z.infer<typeof Provider>): Promise<void>;
|
||||||
delete(providerName: string): Promise<void>;
|
delete(providerName: string): Promise<void>;
|
||||||
|
|
@ -26,11 +27,7 @@ const defaultConfig: z.infer<typeof ModelConfig> = {
|
||||||
export class FSModelConfigRepo implements IModelConfigRepo {
|
export class FSModelConfigRepo implements IModelConfigRepo {
|
||||||
private readonly configPath = path.join(WorkDir, "config", "models.json");
|
private readonly configPath = path.join(WorkDir, "config", "models.json");
|
||||||
|
|
||||||
constructor() {
|
async ensureConfig(): Promise<void> {
|
||||||
this.ensureDefaultConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensureDefaultConfig(): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
await fs.access(this.configPath);
|
await fs.access(this.configPath);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue