Files
moreminimore-vibe/src/ipc/handlers/app_handlers.ts

1075 lines
32 KiB
TypeScript

import { ipcMain } from "electron";
import { db, getDatabasePath } from "../../db";
import { apps, chats } from "../../db/schema";
import { desc, eq } from "drizzle-orm";
import type {
App,
CreateAppParams,
SandboxConfig,
Version,
} from "../ipc_types";
import fs from "node:fs";
import path from "node:path";
import { getDyadAppPath, getUserDataPath } from "../../paths/paths";
import { spawn } from "node:child_process";
import git from "isomorphic-git";
import { promises as fsPromises } from "node:fs";
import { extractCodebase } from "../../utils/codebase";
// Import our utility modules
import { withLock } from "../utils/lock_utils";
import {
copyDirectoryRecursive,
getFilesRecursively,
} from "../utils/file_utils";
import {
runningApps,
processCounter,
killProcess,
removeAppIfCurrentProcess,
RunningAppInfo,
} from "../utils/process_manager";
import { ALLOWED_ENV_VARS } from "../../constants/models";
import { getEnvVar } from "../utils/read_env";
import { readSettings } from "../../main/settings";
import { Worker } from "worker_threads";
import fixPath from "fix-path";
import { getGitAuthor } from "../utils/git_author";
// Needed, otherwise electron in MacOS/Linux will not be able
// to find "npm".
fixPath();
// Keep track of the static file server worker
let staticServerWorker: Worker | null = null;
let staticServerPort: number | null = null;
// let staticServerRootDir: string | null = null; // Store the root dir it's serving - Removed
async function executeApp({
appPath,
appId,
event, // Keep event for local-node case
}: {
appPath: string;
appId: number;
event: Electron.IpcMainInvokeEvent;
}): Promise<void> {
// Return type is void, communication happens via event.sender.send
const settings = readSettings();
if (settings.runtimeMode === "web-sandbox") {
// If server is already running, do nothing.
if (staticServerWorker) {
console.log(`Static server already running on port ${staticServerPort}`);
// No need to send app:output here
return;
}
// Start the worker if it's not running
console.log(`Starting static file server worker for the first time.`);
// No need to send starting status
const workerScriptPath = path.resolve(
__dirname,
"../../worker/static_file_server.js"
);
// Check if worker script exists
if (!fs.existsSync(workerScriptPath)) {
const errorMsg = `Worker script not found at ${workerScriptPath}. Build process might be incomplete.`;
console.error(errorMsg);
// No need to send error status via event
throw new Error(errorMsg);
}
staticServerWorker = new Worker(workerScriptPath, {
workerData: {
rootDir: path.join(__dirname, "..", "..", "sandpack-generated"), // Use the appPath of the first app run in this mode
// Optionally pass other config like port preference
// port: 3001 // Example
},
});
// staticServerRootDir = appPath; // Removed
staticServerWorker.on("message", (message) => {
console.log(
`Message from static server worker: ${JSON.stringify(message)}`
);
if (message.status === "ready" && message.port) {
staticServerPort = message.port;
console.log(`Static file server ready on port ${staticServerPort}`);
// No need to send ready status
} else if (message.status === "error") {
console.error(`Static file server worker error: ${message.message}`);
// No need to send error status
// Terminate the failed worker
staticServerWorker?.terminate();
staticServerWorker = null;
staticServerPort = null;
}
});
staticServerWorker.on("error", (error) => {
console.error(`Static file server worker encountered an error:`, error);
// No need to send error status
staticServerWorker = null; // Worker is likely unusable
staticServerPort = null;
});
staticServerWorker.on("exit", (code) => {
console.log(`Static file server worker exited with code ${code}`);
// Clear state if the worker exits unexpectedly
if (staticServerWorker) {
// Check avoids race condition if terminated intentionally
staticServerWorker = null;
staticServerPort = null;
// No need to send exit status
}
});
return; // Return void
}
if (settings.runtimeMode === "local-node") {
// Ensure worker isn't running if switching modes (optional, depends on desired behavior)
// if (staticServerWorker) { await staticServerWorker.terminate(); staticServerWorker = null; staticServerPort = null; }
await executeAppLocalNode({ appPath, appId, event });
return;
}
throw new Error(`Invalid runtime mode: ${settings.runtimeMode}`);
}
async function executeAppLocalNode({
appPath,
appId,
event,
}: {
appPath: string;
appId: number;
event: Electron.IpcMainInvokeEvent;
}): Promise<void> {
const process = spawn("npm install && npm run dev", [], {
cwd: appPath,
shell: true,
stdio: "pipe", // Ensure stdio is piped so we can capture output/errors and detect close
detached: false, // Ensure child process is attached to the main process lifecycle unless explicitly backgrounded
});
// Check if process spawned correctly
if (!process.pid) {
// Attempt to capture any immediate errors if possible
let errorOutput = "";
process.stderr?.on("data", (data) => (errorOutput += data));
await new Promise((resolve) => process.on("error", resolve)); // Wait for error event
throw new Error(
`Failed to spawn process for app ${appId}. Error: ${
errorOutput || "Unknown spawn error"
}`
);
}
// Increment the counter and store the process reference with its ID
const currentProcessId = processCounter.increment();
runningApps.set(appId, { process, processId: currentProcessId });
// Log output
process.stdout?.on("data", (data) => {
console.log(
`App ${appId} (PID: ${process.pid}) stdout: ${data.toString().trim()}`
);
event.sender.send("app:output", {
type: "stdout",
message: data.toString().trim(),
appId: appId,
});
});
process.stderr?.on("data", (data) => {
console.error(
`App ${appId} (PID: ${process.pid}) stderr: ${data.toString().trim()}`
);
event.sender.send("app:output", {
type: "stderr",
message: data.toString().trim(),
appId: appId,
});
});
// Handle process exit/close
process.on("close", (code, signal) => {
console.log(
`App ${appId} (PID: ${process.pid}) process closed with code ${code}, signal ${signal}.`
);
removeAppIfCurrentProcess(appId, process);
});
// Handle errors during process lifecycle (e.g., command not found)
process.on("error", (err) => {
console.error(
`Error in app ${appId} (PID: ${process.pid}) process: ${err.message}`
);
removeAppIfCurrentProcess(appId, process);
// Note: We don't throw here as the error is asynchronous. The caller got a success response already.
// Consider adding ipcRenderer event emission to notify UI of the error.
});
}
function checkCommandExists(command: string): Promise<string | null> {
return new Promise((resolve) => {
let output = "";
const process = spawn(command, ["--version"], {
shell: true,
stdio: ["ignore", "pipe", "pipe"], // ignore stdin, pipe stdout/stderr
});
process.stdout?.on("data", (data) => {
output += data.toString();
});
process.stderr?.on("data", (data) => {
// Log stderr but don't treat it as a failure unless the exit code is non-zero
console.warn(
`Stderr from "${command} --version": ${data.toString().trim()}`
);
});
process.on("error", (error) => {
console.error(`Error executing command "${command}":`, error.message);
resolve(null); // Command execution failed
});
process.on("close", (code) => {
if (code === 0) {
resolve(output.trim()); // Command succeeded, return trimmed output
} else {
console.error(
`Command "${command} --version" failed with code ${code}`
);
resolve(null); // Command failed
}
});
});
}
export function registerAppHandlers() {
ipcMain.handle(
"nodejs-status",
async (): Promise<{
nodeVersion: string | null;
npmVersion: string | null;
}> => {
// Run checks in parallel
const [nodeVersion, npmVersion] = await Promise.all([
checkCommandExists("node"),
checkCommandExists("npm"),
]);
return { nodeVersion, npmVersion };
}
);
ipcMain.handle(
"get-app-sandbox-config",
async (_, { appId }: { appId: number }): Promise<SandboxConfig> => {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
const appPath = getDyadAppPath(app.path);
const files = getFilesRecursively(appPath, appPath);
const filesMap = await Promise.all(
files.map(async (file) => {
const content = await fs.promises.readFile(
path.join(appPath, file),
"utf-8"
);
return { [file]: content };
})
);
// Get dependencies from package.json
const packageJsonPath = path.join(appPath, "package.json");
const packageJson = await fs.promises.readFile(packageJsonPath, "utf-8");
const dependencies = JSON.parse(packageJson).dependencies;
return {
files: filesMap.reduce((acc, file) => ({ ...acc, ...file }), {}),
dependencies,
entry: "src/main.tsx",
};
}
);
ipcMain.handle("create-app", async (_, params: CreateAppParams) => {
const appPath = params.name;
const fullAppPath = getDyadAppPath(appPath);
if (fs.existsSync(fullAppPath)) {
throw new Error(`App already exists at: ${fullAppPath}`);
}
// Create a new app
const [app] = await db
.insert(apps)
.values({
name: params.name,
// Use the name as the path for now
path: appPath,
})
.returning();
// Create an initial chat for this app
const [chat] = await db
.insert(chats)
.values({
appId: app.id,
})
.returning();
// Start async operations in background
try {
// Copy scaffold asynchronously
await copyDirectoryRecursive(
path.join(__dirname, "..", "..", "scaffold"),
fullAppPath
);
// Initialize git repo and create first commit
await git.init({
fs: fs,
dir: fullAppPath,
defaultBranch: "main",
});
// Stage all files
await git.add({
fs: fs,
dir: fullAppPath,
filepath: ".",
});
// Create initial commit
await git.commit({
fs: fs,
dir: fullAppPath,
message: "Init from react vite template",
author: await getGitAuthor(),
});
} catch (error) {
console.error("Error in background app initialization:", error);
}
// })();
return { app, chatId: chat.id };
});
ipcMain.handle("get-app", async (_, appId: number): Promise<App> => {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
// Get app files
const appPath = getDyadAppPath(app.path);
let files: string[] = [];
try {
files = getFilesRecursively(appPath, appPath);
} catch (error) {
console.error(`Error reading files for app ${appId}:`, error);
// Return app even if files couldn't be read
}
return {
...app,
files,
};
});
ipcMain.handle("list-apps", async () => {
const allApps = await db.query.apps.findMany({
orderBy: [desc(apps.createdAt)],
});
return {
apps: allApps,
appBasePath: getDyadAppPath("$APP_BASE_PATH"),
};
});
ipcMain.handle(
"read-app-file",
async (_, { appId, filePath }: { appId: number; filePath: string }) => {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
const appPath = getDyadAppPath(app.path);
const fullPath = path.join(appPath, filePath);
// Check if the path is within the app directory (security check)
if (!fullPath.startsWith(appPath)) {
throw new Error("Invalid file path");
}
if (!fs.existsSync(fullPath)) {
throw new Error("File not found");
}
try {
const contents = fs.readFileSync(fullPath, "utf-8");
return contents;
} catch (error) {
console.error(
`Error reading file ${filePath} for app ${appId}:`,
error
);
throw new Error("Failed to read file");
}
}
);
ipcMain.handle("get-env-vars", async () => {
const envVars: Record<string, string | undefined> = {};
for (const key of ALLOWED_ENV_VARS) {
envVars[key] = getEnvVar(key);
}
return envVars;
});
ipcMain.handle(
"run-app",
async (
event: Electron.IpcMainInvokeEvent,
{ appId }: { appId: number }
) => {
return withLock(appId, async () => {
// Check if app is already running
if (runningApps.has(appId)) {
console.debug(`App ${appId} is already running.`);
// Potentially return the existing process info or confirm status
return { success: true, message: "App already running." };
}
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
console.debug(`Starting app ${appId} in path ${app.path}`);
const appPath = getDyadAppPath(app.path);
try {
const currentProcessId = await executeApp({ appPath, appId, event });
return { success: true, processId: currentProcessId };
} catch (error: any) {
console.error(`Error running app ${appId}:`, error);
// Ensure cleanup if error happens during setup but before process events are handled
if (
runningApps.has(appId) &&
runningApps.get(appId)?.processId === processCounter.value
) {
runningApps.delete(appId);
}
throw new Error(`Failed to run app ${appId}: ${error.message}`);
}
});
}
);
ipcMain.handle("stop-app", async (_, { appId }: { appId: number }) => {
console.log(
`Attempting to stop app ${appId} (local-node only). Current running apps: ${runningApps.size}`
);
// Static server worker is NOT terminated here anymore
// Use withLock for local-node apps
return withLock(appId, async () => {
const appInfo = runningApps.get(appId);
if (!appInfo) {
console.log(
`App ${appId} not found in running apps map (local-node). Assuming already stopped or was web-sandbox.`
);
// If no local-node app was running, and we terminated the static server above, return success.
return {
success: true,
message: "App not running in local-node mode.", // Simplified message
};
}
const { process, processId } = appInfo;
console.log(
`Found running app ${appId} with processId ${processId} (PID: ${process.pid}). Attempting to stop.`
);
// Check if the process is already exited or closed
if (process.exitCode !== null || process.signalCode !== null) {
console.log(
`Process for app ${appId} (PID: ${process.pid}) already exited (code: ${process.exitCode}, signal: ${process.signalCode}). Cleaning up map.`
);
runningApps.delete(appId); // Ensure cleanup if somehow missed
return { success: true, message: "Process already exited." };
}
try {
// Use the killProcess utility to stop the process
await killProcess(process);
// Now, safely remove the app from the map *after* confirming closure
removeAppIfCurrentProcess(appId, process);
return { success: true };
} catch (error: any) {
console.error(
`Error stopping app ${appId} (PID: ${process.pid}, processId: ${processId}):`,
error
);
// Attempt cleanup even if an error occurred during the stop process
removeAppIfCurrentProcess(appId, process);
throw new Error(`Failed to stop app ${appId}: ${error.message}`);
}
});
});
ipcMain.handle(
"restart-app",
async (
event: Electron.IpcMainInvokeEvent,
{ appId }: { appId: number }
) => {
// Static server worker is NOT terminated here anymore
return withLock(appId, async () => {
try {
// First stop the app if it's running
const appInfo = runningApps.get(appId);
if (appInfo) {
const { process, processId } = appInfo;
console.log(
`Stopping local-node app ${appId} (processId ${processId}) before restart` // Adjusted log
);
// Use the killProcess utility to stop the process
await killProcess(process);
// Remove from running apps
runningApps.delete(appId);
} else {
console.log(
`App ${appId} not running in local-node mode, proceeding to start.`
);
}
// Now start the app again
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
const appPath = getDyadAppPath(app.path);
console.debug(
`Executing app ${appId} in path ${app.path} after restart request`
); // Adjusted log
await executeApp({ appPath, appId, event }); // This will handle starting either mode
return { success: true };
} catch (error) {
console.error(`Error restarting app ${appId}:`, error);
throw error;
}
});
}
);
ipcMain.handle("list-versions", async (_, { appId }: { appId: number }) => {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
const appPath = getDyadAppPath(app.path);
// Just return an empty array if the app is not a git repo.
if (!fs.existsSync(path.join(appPath, ".git"))) {
return [];
}
try {
const commits = await git.log({
fs,
dir: appPath,
depth: 1000, // Limit to last 1000 commits for performance
});
return commits.map((commit) => ({
oid: commit.oid,
message: commit.commit.message,
timestamp: commit.commit.author.timestamp,
})) satisfies Version[];
} catch (error: any) {
console.error(`Error listing versions for app ${appId}:`, error);
throw new Error(`Failed to list versions: ${error.message}`);
}
});
ipcMain.handle(
"revert-version",
async (
_,
{ appId, previousVersionId }: { appId: number; previousVersionId: string }
) => {
return withLock(appId, async () => {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
const appPath = getDyadAppPath(app.path);
try {
await git.checkout({
fs,
dir: appPath,
ref: "main",
force: true,
});
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
const matrix = await git.statusMatrix({
fs,
dir: appPath,
ref: previousVersionId,
});
// Process each file to revert to the state in previousVersionId
for (const [
filepath,
headStatus,
workdirStatus,
stageStatus,
] of matrix) {
const fullPath = path.join(appPath, filepath);
// If file exists in HEAD (previous version)
if (headStatus === 1) {
// If file doesn't exist or has changed in working directory, restore it from the target commit
if (workdirStatus !== 1) {
const { blob } = await git.readBlob({
fs,
dir: appPath,
oid: previousVersionId,
filepath,
});
await fsPromises.mkdir(path.dirname(fullPath), {
recursive: true,
});
await fsPromises.writeFile(fullPath, Buffer.from(blob));
}
}
// If file doesn't exist in HEAD but exists in working directory, delete it
else if (headStatus === 0 && workdirStatus !== 0) {
if (fs.existsSync(fullPath)) {
await fsPromises.unlink(fullPath);
await git.remove({
fs,
dir: appPath,
filepath: filepath,
});
}
}
}
// Stage all changes
await git.add({
fs,
dir: appPath,
filepath: ".",
});
// Create a revert commit
await git.commit({
fs,
dir: appPath,
message: `Reverted all changes back to version ${previousVersionId}`,
author: await getGitAuthor(),
});
return { success: true };
} catch (error: any) {
console.error(
`Error reverting to version ${previousVersionId} for app ${appId}:`,
error
);
throw new Error(`Failed to revert version: ${error.message}`);
}
});
}
);
ipcMain.handle(
"checkout-version",
async (_, { appId, versionId }: { appId: number; versionId: string }) => {
return withLock(appId, async () => {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
const appPath = getDyadAppPath(app.path);
try {
if (versionId !== "main") {
// First check if the version exists
const commits = await git.log({
fs,
dir: appPath,
depth: 100,
});
const targetCommit = commits.find((c) => c.oid === versionId);
if (!targetCommit) {
throw new Error("Target version not found");
}
}
// Checkout the target commit
await git.checkout({
fs,
dir: appPath,
ref: versionId,
force: true,
});
return { success: true };
} catch (error: any) {
console.error(
`Error checking out version ${versionId} for app ${appId}:`,
error
);
throw new Error(`Failed to checkout version: ${error.message}`);
}
});
}
);
// Extract codebase information
ipcMain.handle(
"extract-codebase",
async (_, { appId, maxFiles }: { appId: number; maxFiles?: number }) => {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
const appPath = getDyadAppPath(app.path);
try {
return await extractCodebase(appPath, maxFiles);
} catch (error) {
console.error(`Error extracting codebase for app ${appId}:`, error);
throw new Error(
`Failed to extract codebase: ${(error as any).message}`
);
}
}
);
ipcMain.handle(
"edit-app-file",
async (
_,
{
appId,
filePath,
content,
}: { appId: number; filePath: string; content: string }
) => {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
const appPath = getDyadAppPath(app.path);
const fullPath = path.join(appPath, filePath);
// Check if the path is within the app directory (security check)
if (!fullPath.startsWith(appPath)) {
throw new Error("Invalid file path");
}
// Ensure directory exists
const dirPath = path.dirname(fullPath);
await fsPromises.mkdir(dirPath, { recursive: true });
try {
await fsPromises.writeFile(fullPath, content, "utf-8");
// Check if git repository exists and commit the change
if (fs.existsSync(path.join(appPath, ".git"))) {
await git.add({
fs,
dir: appPath,
filepath: filePath,
});
await git.commit({
fs,
dir: appPath,
message: `Updated ${filePath}`,
author: await getGitAuthor(),
});
}
return { success: true };
} catch (error: any) {
console.error(
`Error writing file ${filePath} for app ${appId}:`,
error
);
throw new Error(`Failed to write file: ${error.message}`);
}
}
);
ipcMain.handle("delete-app", async (_, { appId }: { appId: number }) => {
// Static server worker is NOT terminated here anymore
return withLock(appId, async () => {
// Check if app exists
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
// Stop the app if it's running
if (runningApps.has(appId)) {
const appInfo = runningApps.get(appId)!;
try {
console.log(`Stopping local-node app ${appId} before deletion.`); // Adjusted log
await killProcess(appInfo.process);
runningApps.delete(appId);
} catch (error: any) {
console.error(
`Error stopping local-node app ${appId} before deletion:`,
error
); // Adjusted log
// Continue with deletion even if stopping fails
}
}
// Delete app files
const appPath = getDyadAppPath(app.path);
try {
await fsPromises.rm(appPath, { recursive: true, force: true });
} catch (error: any) {
console.error(`Error deleting app files for app ${appId}:`, error);
throw new Error(`Failed to delete app files: ${error.message}`);
}
// Delete app from database
try {
await db.delete(apps).where(eq(apps.id, appId));
// Note: Associated chats will cascade delete if that's set up in the schema
return { success: true };
} catch (error: any) {
console.error(`Error deleting app ${appId} from database:`, error);
throw new Error(`Failed to delete app from database: ${error.message}`);
}
});
});
ipcMain.handle(
"rename-app",
async (
_,
{
appId,
appName,
appPath,
}: { appId: number; appName: string; appPath: string }
) => {
return withLock(appId, async () => {
// Check if app exists
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
// Check for conflicts with existing apps
const nameConflict = await db.query.apps.findFirst({
where: eq(apps.name, appName),
});
const pathConflict = await db.query.apps.findFirst({
where: eq(apps.path, appPath),
});
if (nameConflict && nameConflict.id !== appId) {
throw new Error(`An app with the name '${appName}' already exists`);
}
if (pathConflict && pathConflict.id !== appId) {
throw new Error(`An app with the path '${appPath}' already exists`);
}
// Stop the app if it's running
if (runningApps.has(appId)) {
const appInfo = runningApps.get(appId)!;
try {
await killProcess(appInfo.process);
runningApps.delete(appId);
} catch (error: any) {
console.error(
`Error stopping app ${appId} before renaming:`,
error
);
throw new Error(
`Failed to stop app before renaming: ${error.message}`
);
}
}
const oldAppPath = getDyadAppPath(app.path);
const newAppPath = getDyadAppPath(appPath);
// Only move files if needed
if (newAppPath !== oldAppPath) {
// Move app files
try {
// Check if destination directory already exists
if (fs.existsSync(newAppPath)) {
throw new Error(
`Destination path '${newAppPath}' already exists`
);
}
// Create parent directory if it doesn't exist
await fsPromises.mkdir(path.dirname(newAppPath), {
recursive: true,
});
// Move the files
await fsPromises.rename(oldAppPath, newAppPath);
} catch (error: any) {
console.error(
`Error moving app files from ${oldAppPath} to ${newAppPath}:`,
error
);
throw new Error(`Failed to move app files: ${error.message}`);
}
}
// Update app in database
try {
const [updatedApp] = await db
.update(apps)
.set({
name: appName,
path: appPath,
})
.where(eq(apps.id, appId))
.returning();
return { success: true, app: updatedApp };
} catch (error: any) {
// Attempt to rollback the file move
if (newAppPath !== oldAppPath) {
try {
await fsPromises.rename(newAppPath, oldAppPath);
} catch (rollbackError) {
console.error(
`Failed to rollback file move during rename error:`,
rollbackError
);
}
}
console.error(`Error updating app ${appId} in database:`, error);
throw new Error(`Failed to update app in database: ${error.message}`);
}
});
}
);
ipcMain.handle("reset-all", async () => {
// Terminate static server worker if it's running
if (staticServerWorker) {
console.log(`Terminating static server worker on reset-all command.`);
await staticServerWorker.terminate();
staticServerWorker = null;
staticServerPort = null;
}
// Stop all running apps first
const runningAppIds = Array.from(runningApps.keys());
for (const appId of runningAppIds) {
try {
const appInfo = runningApps.get(appId)!;
await killProcess(appInfo.process);
runningApps.delete(appId);
} catch (error) {
console.error(`Error stopping app ${appId} during reset:`, error);
// Continue with reset even if stopping fails
}
}
// 1. Remove all app files recursively
const dyadAppPath = getDyadAppPath(".");
if (fs.existsSync(dyadAppPath)) {
await fsPromises.rm(dyadAppPath, { recursive: true, force: true });
// Recreate the base directory
await fsPromises.mkdir(dyadAppPath, { recursive: true });
}
// 2. Drop the database by deleting the SQLite file
const dbPath = getDatabasePath();
if (fs.existsSync(dbPath)) {
// Close database connections first
if (db.$client) {
db.$client.close();
}
await fsPromises.unlink(dbPath);
console.log(`Database file deleted: ${dbPath}`);
}
// 3. Remove settings
const userDataPath = getUserDataPath();
const settingsPath = path.join(userDataPath, "user-settings.json");
if (fs.existsSync(settingsPath)) {
await fsPromises.unlink(settingsPath);
console.log(`Settings file deleted: ${settingsPath}`);
}
return { success: true, message: "Successfully reset everything" };
});
}