refactor(electrobun): simplify bun backend — extract db-utils, merge handlers
- db-utils.ts: shared openDb() (WAL, busy_timeout, foreign_keys, mkdirSync) - 5 DB modules use openDb() instead of duplicated PRAGMA boilerplate - bttask-db shares btmsg-db's Database handle (was duplicate connection) - misc-handlers.ts: 14 inline handlers extracted from index.ts - index.ts: 349→195 lines (only window controls remain inline) - updater.ts: removed dead getLastKnownVersion() - Net reduction: ~700 lines of duplicated boilerplate
This commit is contained in:
parent
2b1194c809
commit
f2e8b07d7f
10 changed files with 291 additions and 239 deletions
198
ui-electrobun/src/bun/handlers/misc-handlers.ts
Normal file
198
ui-electrobun/src/bun/handlers/misc-handlers.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
/**
|
||||
* Miscellaneous RPC handlers — memora, keybindings, updater, diagnostics, telemetry,
|
||||
* files.pickDirectory, files.homeDir, project.templates, project.clone.
|
||||
*
|
||||
* These are small handlers that don't warrant their own file.
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
import { randomUUID } from "crypto";
|
||||
import type { SettingsDb } from "../settings-db.ts";
|
||||
import type { PtyClient } from "../pty-client.ts";
|
||||
import type { RelayClient } from "../relay-client.ts";
|
||||
import type { SidecarManager } from "../sidecar-manager.ts";
|
||||
import { checkForUpdates, getLastCheckTimestamp } from "../updater.ts";
|
||||
import type { TelemetryManager } from "../telemetry.ts";
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const BRANCH_RE = /^[a-zA-Z0-9/_.-]+$/;
|
||||
|
||||
async function gitWorktreeAdd(
|
||||
mainRepoPath: string,
|
||||
worktreePath: string,
|
||||
branchName: string,
|
||||
): Promise<{ ok: boolean; error?: string }> {
|
||||
const proc = Bun.spawn(
|
||||
["git", "worktree", "add", worktreePath, "-b", branchName],
|
||||
{ cwd: mainRepoPath, stderr: "pipe", stdout: "pipe" },
|
||||
);
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
const errText = await new Response(proc.stderr).text();
|
||||
return { ok: false, error: errText.trim() || `git exited with code ${exitCode}` };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// ── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
interface MiscDeps {
|
||||
settingsDb: SettingsDb;
|
||||
ptyClient: PtyClient;
|
||||
relayClient: RelayClient;
|
||||
sidecarManager: SidecarManager;
|
||||
telemetry: TelemetryManager;
|
||||
appVersion: string;
|
||||
}
|
||||
|
||||
// ── Handler factory ─────────────────────────────────────────────────────────
|
||||
|
||||
export function createMiscHandlers(deps: MiscDeps) {
|
||||
const { settingsDb, ptyClient, relayClient, sidecarManager, telemetry, appVersion } = deps;
|
||||
|
||||
return {
|
||||
// ── Files: picker + homeDir ─────────────────────────────────────────
|
||||
"files.pickDirectory": async ({ startingFolder }: { startingFolder?: string }) => {
|
||||
try {
|
||||
const { execSync } = await import("child_process");
|
||||
const start = startingFolder?.replace(/^~/, process.env.HOME || "/home") || process.env.HOME || "/home";
|
||||
const result = execSync(
|
||||
`zenity --file-selection --directory --title="Select Project Folder" --filename="${start}/"`,
|
||||
{ encoding: "utf-8", timeout: 120_000 },
|
||||
).trim();
|
||||
return { path: result || null };
|
||||
} catch {
|
||||
return { path: null };
|
||||
}
|
||||
},
|
||||
|
||||
"files.homeDir": async () => ({ path: process.env.HOME || "/home" }),
|
||||
|
||||
// ── Project templates ───────────────────────────────────────────────
|
||||
"project.templates": async () => ({
|
||||
templates: [
|
||||
{ id: "blank", name: "Blank Project", description: "Empty directory with no scaffolding", icon: "📁" },
|
||||
{ id: "web-app", name: "Web App", description: "HTML/CSS/JS web application starter", icon: "🌐" },
|
||||
{ id: "api-server", name: "API Server", description: "Node.js/Bun HTTP API server", icon: "⚡" },
|
||||
{ id: "cli-tool", name: "CLI Tool", description: "Command-line tool with argument parser", icon: "🔧" },
|
||||
],
|
||||
}),
|
||||
|
||||
// ── Project clone ───────────────────────────────────────────────────
|
||||
"project.clone": async ({ projectId, branchName }: { projectId: string; branchName: string }) => {
|
||||
try {
|
||||
if (!BRANCH_RE.test(branchName)) {
|
||||
return { ok: false, error: "Invalid branch name. Use only letters, numbers, /, _, -, ." };
|
||||
}
|
||||
const source = settingsDb.getProject(projectId);
|
||||
if (!source) return { ok: false, error: `Project not found: ${projectId}` };
|
||||
|
||||
const mainRepoPath = source.mainRepoPath ?? source.cwd;
|
||||
const allProjects = settingsDb.listProjects();
|
||||
const existingClones = allProjects.filter(
|
||||
(p) => p.cloneOf === projectId || (source.cloneOf && p.cloneOf === source.cloneOf),
|
||||
);
|
||||
if (existingClones.length >= 3) return { ok: false, error: "Maximum 3 clones per project reached" };
|
||||
|
||||
const cloneIndex = existingClones.length + 1;
|
||||
const wtSuffix = randomUUID().slice(0, 8);
|
||||
const worktreePath = `${mainRepoPath}-wt-${wtSuffix}`;
|
||||
|
||||
const gitResult = await gitWorktreeAdd(mainRepoPath, worktreePath, branchName);
|
||||
if (!gitResult.ok) return { ok: false, error: gitResult.error };
|
||||
|
||||
const cloneId = `${projectId}-clone-${cloneIndex}-${randomUUID().slice(0, 8)}`;
|
||||
const cloneConfig = {
|
||||
id: cloneId, name: `${source.name} [${branchName}]`,
|
||||
cwd: worktreePath, accent: source.accent, provider: source.provider,
|
||||
profile: source.profile, model: source.model,
|
||||
groupId: source.groupId ?? "dev", mainRepoPath,
|
||||
cloneOf: projectId, worktreePath, worktreeBranch: branchName, cloneIndex,
|
||||
};
|
||||
settingsDb.setProject(cloneId, cloneConfig);
|
||||
return { ok: true, project: { id: cloneId, config: JSON.stringify(cloneConfig) } };
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
console.error("[project.clone]", err);
|
||||
return { ok: false, error };
|
||||
}
|
||||
},
|
||||
|
||||
// ── Keybindings ─────────────────────────────────────────────────────
|
||||
"keybindings.getAll": () => {
|
||||
try { return { keybindings: settingsDb.getKeybindings() }; }
|
||||
catch (err) { console.error("[keybindings.getAll]", err); return { keybindings: {} }; }
|
||||
},
|
||||
"keybindings.set": ({ id, chord }: { id: string; chord: string }) => {
|
||||
try { settingsDb.setKeybinding(id, chord); return { ok: true }; }
|
||||
catch (err) { console.error("[keybindings.set]", err); return { ok: false }; }
|
||||
},
|
||||
"keybindings.reset": ({ id }: { id: string }) => {
|
||||
try { settingsDb.deleteKeybinding(id); return { ok: true }; }
|
||||
catch (err) { console.error("[keybindings.reset]", err); return { ok: false }; }
|
||||
},
|
||||
|
||||
// ── Updater ─────────────────────────────────────────────────────────
|
||||
"updater.check": async () => {
|
||||
try {
|
||||
const result = await checkForUpdates(appVersion);
|
||||
return { ...result, error: undefined };
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
console.error("[updater.check]", err);
|
||||
return { available: false, version: "", downloadUrl: "", releaseNotes: "", checkedAt: Date.now(), error };
|
||||
}
|
||||
},
|
||||
"updater.getVersion": () => ({ version: appVersion, lastCheck: getLastCheckTimestamp() }),
|
||||
|
||||
// ── Memora (read-only) ──────────────────────────────────────────────
|
||||
"memora.search": ({ query, limit }: { query: string; limit?: number }) => {
|
||||
try {
|
||||
const dbPath = join(homedir(), ".local", "share", "memora", "memories.db");
|
||||
if (!fs.existsSync(dbPath)) return { memories: [] };
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
try {
|
||||
const rows = db.query(
|
||||
"SELECT id, content, tags, metadata, created_at as createdAt, updated_at as updatedAt FROM memories WHERE content LIKE ? ORDER BY updated_at DESC LIMIT ?",
|
||||
).all(`%${query}%`, limit ?? 20);
|
||||
return { memories: rows as Array<{ id: number; content: string; tags: string; metadata: string; createdAt: string; updatedAt: string }> };
|
||||
} finally { db.close(); }
|
||||
} catch (err) { console.error("[memora.search]", err); return { memories: [] }; }
|
||||
},
|
||||
"memora.list": ({ limit, tag }: { limit?: number; tag?: string }) => {
|
||||
try {
|
||||
const dbPath = join(homedir(), ".local", "share", "memora", "memories.db");
|
||||
if (!fs.existsSync(dbPath)) return { memories: [] };
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
try {
|
||||
let sql = "SELECT id, content, tags, metadata, created_at as createdAt, updated_at as updatedAt FROM memories";
|
||||
const params: unknown[] = [];
|
||||
if (tag) { sql += " WHERE tags LIKE ?"; params.push(`%${tag}%`); }
|
||||
sql += " ORDER BY updated_at DESC LIMIT ?";
|
||||
params.push(limit ?? 20);
|
||||
const rows = db.query(sql).all(...params);
|
||||
return { memories: rows as Array<{ id: number; content: string; tags: string; metadata: string; createdAt: string; updatedAt: string }> };
|
||||
} finally { db.close(); }
|
||||
} catch (err) { console.error("[memora.list]", err); return { memories: [] }; }
|
||||
},
|
||||
|
||||
// ── Diagnostics ─────────────────────────────────────────────────────
|
||||
"diagnostics.stats": () => ({
|
||||
ptyConnected: ptyClient.isConnected,
|
||||
relayConnections: relayClient.listMachines().filter((m) => m.status === "connected").length,
|
||||
activeSidecars: sidecarManager.listSessions().filter((s) => s.status === "running").length,
|
||||
rpcCallCount: 0,
|
||||
droppedEvents: 0,
|
||||
}),
|
||||
|
||||
// ── Telemetry ───────────────────────────────────────────────────────
|
||||
"telemetry.log": ({ level, message, attributes }: { level: string; message: string; attributes?: Record<string, string | number | boolean> }) => {
|
||||
try { telemetry.log(level as "info" | "warn" | "error", `[frontend] ${message}`, attributes ?? {}); return { ok: true }; }
|
||||
catch (err) { console.error("[telemetry.log]", err); return { ok: false }; }
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue