diff --git a/scripts/extract-codebase.ts b/scripts/extract-codebase.ts index 841382e..b641ed0 100644 --- a/scripts/extract-codebase.ts +++ b/scripts/extract-codebase.ts @@ -11,10 +11,9 @@ import path from "path"; import { fileURLToPath } from "url"; import { dirname } from "path"; import { isIgnored } from "isomorphic-git"; +import log from "electron-log"; -// Setup ESM compatibility -// const __filename = fileURLToPath(import.meta.url); -// const __dirname = dirname(__filename); +const logger = log.scope("extract-codebase"); // File extensions to include const ALLOWED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".css"]; @@ -28,7 +27,7 @@ async function isGitIgnored( const relativePath = path.relative(baseDir, filePath); return await isIgnored({ fs, dir: baseDir, filepath: relativePath }); } catch (error) { - console.error(`Error checking if path is git ignored: ${filePath}`, error); + logger.error(`Error checking if path is git ignored: ${filePath}`, error); return false; } } @@ -84,7 +83,7 @@ function formatFile(filePath: string, baseDir: string): string { .extname(filePath) .substring(1)}\n${content}\n\`\`\`\n\n`; } catch (error) { - console.error(`Error reading file: ${filePath}`, error); + logger.error(`Error reading file: ${filePath}`, error); return `## File: ${filePath}\nError reading file: ${error}\n\n`; } } @@ -115,9 +114,3 @@ async function main() { fs.writeFileSync(outputFile, output); console.log(`Extraction complete. Output written to ${outputFile}`); } - -// Run the script -main().catch((error) => { - console.error("Error:", error); - process.exit(1); -}); diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index a7775fe..4cc70aa 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -9,6 +9,7 @@ import { import { Button } from "@/components/ui/button"; import { BookOpenIcon, BugIcon } from "lucide-react"; import { IpcClient } from "@/ipc/ipc_client"; +import { useState } from "react"; interface HelpDialogProps { isOpen: boolean; @@ -16,6 +17,60 @@ interface HelpDialogProps { } export function HelpDialog({ isOpen, onClose }: HelpDialogProps) { + const [isLoading, setIsLoading] = useState(false); + + const handleReportBug = async () => { + setIsLoading(true); + try { + // Get system debug info + const debugInfo = await IpcClient.getInstance().getSystemDebugInfo(); + + // Create a formatted issue body with the debug info + const issueBody = ` +## Bug Description + + +## Steps to Reproduce + + +## Expected Behavior + + +## Actual Behavior + + +## System Information +- Dyad Version: ${debugInfo.dyadVersion} +- Platform: ${debugInfo.platform} +- Architecture: ${debugInfo.architecture} +- Node Version: ${debugInfo.nodeVersion || "Not available"} +- PNPM Version: ${debugInfo.pnpmVersion || "Not available"} +- Node Path: ${debugInfo.nodePath || "Not available"} + +## Logs +\`\`\` +${debugInfo.logs.slice(-3_500) || "No logs available"} +\`\`\` +`; + + // Create the GitHub issue URL with the pre-filled body + const encodedBody = encodeURIComponent(issueBody); + const encodedTitle = encodeURIComponent("[bug] "); + const githubIssueUrl = `https://github.com/dyad-sh/dyad/issues/new?title=${encodedTitle}&labels=bug,filed-from-app&body=${encodedBody}`; + + // Open the pre-filled GitHub issue page + IpcClient.getInstance().openExternalUrl(githubIssueUrl); + } catch (error) { + console.error("Failed to prepare bug report:", error); + // Fallback to opening the regular GitHub issue page + IpcClient.getInstance().openExternalUrl( + "https://github.com/dyad-sh/dyad/issues/new" + ); + } finally { + setIsLoading(false); + } + }; + return ( @@ -46,17 +101,15 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {

- We’ll auto-fill your report with system info and logs. You can + We'll auto-fill your report with system info and logs. You can review it for any sensitive info before submitting.

diff --git a/src/components/ProviderSettings.tsx b/src/components/ProviderSettings.tsx index 1cf6892..56afe66 100644 --- a/src/components/ProviderSettings.tsx +++ b/src/components/ProviderSettings.tsx @@ -20,7 +20,6 @@ export function ProviderSettingsGrid({ const navigate = useNavigate(); const handleProviderClick = (provider: ModelProvider) => { - console.log("PROVIDER", provider); navigate({ to: providerSettingsRoute.id, params: { provider }, diff --git a/src/db/index.ts b/src/db/index.ts index e01f953..69fe70e 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -9,6 +9,9 @@ import path from "node:path"; import fs from "node:fs"; import { getDyadAppPath, getUserDataPath } from "../paths/paths"; import { eq } from "drizzle-orm"; +import log from "electron-log"; + +const logger = log.scope("db"); // Database connection factory let _db: ReturnType | null = null; @@ -29,7 +32,7 @@ export function initializeDatabase(): BetterSQLite3Database & { if (_db) return _db as any; const dbPath = getDatabasePath(); - console.log("Initializing database at:", dbPath); + logger.log("Initializing database at:", dbPath); // Check if the database file exists and remove it if it has issues try { @@ -38,14 +41,12 @@ export function initializeDatabase(): BetterSQLite3Database & { const stats = fs.statSync(dbPath); // If the file is very small, it might be corrupted if (stats.size < 100) { - console.log( - "Database file exists but may be corrupted. Removing it..." - ); + logger.log("Database file exists but may be corrupted. Removing it..."); fs.unlinkSync(dbPath); } } } catch (error) { - console.error("Error checking database file:", error); + logger.error("Error checking database file:", error); } fs.mkdirSync(getUserDataPath(), { recursive: true }); @@ -62,21 +63,15 @@ export function initializeDatabase(): BetterSQLite3Database & { _db = drizzle(sqlite, { schema }); try { - // Run migrations programmatically - const migrationsFolder = path.join(__dirname, "..", "..", "drizzle"); - - console.log("MIGRATIONS FOLDER INITIALIZE", migrationsFolder); - - // Verify migrations folder exists if (!fs.existsSync(migrationsFolder)) { - console.error("Migrations folder not found:", migrationsFolder); + logger.error("Migrations folder not found:", migrationsFolder); } else { - console.log("Running migrations from:", migrationsFolder); + logger.log("Running migrations from:", migrationsFolder); migrate(_db, { migrationsFolder }); } } catch (error) { - console.error("Migration error:", error); + logger.error("Migration error:", error); } return _db as any; @@ -86,7 +81,7 @@ export function initializeDatabase(): BetterSQLite3Database & { try { initializeDatabase(); } catch (error) { - console.error("Failed to initialize database:", error); + logger.error("Failed to initialize database:", error); } export const db = _db as any as BetterSQLite3Database & { diff --git a/src/ipc/handlers/app_handlers.ts b/src/ipc/handlers/app_handlers.ts index 00a90f8..1723668 100644 --- a/src/ipc/handlers/app_handlers.ts +++ b/src/ipc/handlers/app_handlers.ts @@ -36,15 +36,14 @@ import fixPath from "fix-path"; import { getGitAuthor } from "../utils/git_author"; import killPort from "kill-port"; import util from "util"; +import log from "electron-log"; + +const logger = log.scope("app_handlers"); + // Needed, otherwise electron in MacOS/Linux will not be able // to find node/pnpm. 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, @@ -96,7 +95,7 @@ async function executeAppLocalNode({ // Log output process.stdout?.on("data", (data) => { const message = util.stripVTControlCharacters(data.toString()); - console.log(`App ${appId} (PID: ${process.pid}) stdout: ${message}`); + logger.debug(`App ${appId} (PID: ${process.pid}) stdout: ${message}`); event.sender.send("app:output", { type: "stdout", message, @@ -106,7 +105,7 @@ async function executeAppLocalNode({ process.stderr?.on("data", (data) => { const message = util.stripVTControlCharacters(data.toString()); - console.error(`App ${appId} (PID: ${process.pid}) stderr: ${message}`); + logger.error(`App ${appId} (PID: ${process.pid}) stderr: ${message}`); event.sender.send("app:output", { type: "stderr", message, @@ -116,7 +115,7 @@ async function executeAppLocalNode({ // Handle process exit/close process.on("close", (code, signal) => { - console.log( + logger.log( `App ${appId} (PID: ${process.pid}) process closed with code ${code}, signal ${signal}.` ); removeAppIfCurrentProcess(appId, process); @@ -124,7 +123,7 @@ async function executeAppLocalNode({ // Handle errors during process lifecycle (e.g., command not found) process.on("error", (err) => { - console.error( + logger.error( `Error in app ${appId} (PID: ${process.pid}) process: ${err.message}` ); removeAppIfCurrentProcess(appId, process); @@ -196,7 +195,7 @@ export function registerAppHandlers() { author: await getGitAuthor(), }); } catch (error) { - console.error("Error in background app initialization:", error); + logger.error("Error in background app initialization:", error); } // })(); @@ -219,7 +218,7 @@ export function registerAppHandlers() { try { files = getFilesRecursively(appPath, appPath); } catch (error) { - console.error(`Error reading files for app ${appId}:`, error); + logger.error(`Error reading files for app ${appId}:`, error); // Return app even if files couldn't be read } @@ -266,10 +265,7 @@ export function registerAppHandlers() { const contents = fs.readFileSync(fullPath, "utf-8"); return contents; } catch (error) { - console.error( - `Error reading file ${filePath} for app ${appId}:`, - error - ); + logger.error(`Error reading file ${filePath} for app ${appId}:`, error); throw new Error("Failed to read file"); } } @@ -292,7 +288,7 @@ export function registerAppHandlers() { return withLock(appId, async () => { // Check if app is already running if (runningApps.has(appId)) { - console.debug(`App ${appId} is already running.`); + logger.debug(`App ${appId} is already running.`); // Potentially return the existing process info or confirm status return { success: true, message: "App already running." }; } @@ -305,7 +301,7 @@ export function registerAppHandlers() { throw new Error("App not found"); } - console.debug(`Starting app ${appId} in path ${app.path}`); + logger.debug(`Starting app ${appId} in path ${app.path}`); const appPath = getDyadAppPath(app.path); try { @@ -313,7 +309,7 @@ export function registerAppHandlers() { return { success: true, processId: currentProcessId }; } catch (error: any) { - console.error(`Error running app ${appId}:`, error); + logger.error(`Error running app ${appId}:`, error); // Ensure cleanup if error happens during setup but before process events are handled if ( runningApps.has(appId) && @@ -328,15 +324,15 @@ export function registerAppHandlers() { ); ipcMain.handle("stop-app", async (_, { appId }: { appId: number }) => { - console.log( - `Attempting to stop app ${appId} (local-node only). Current running apps: ${runningApps.size}` + logger.log( + `Attempting to stop app ${appId}. Current running apps: ${runningApps.size}` ); 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.` + logger.log( + `App ${appId} not found in running apps map. Assuming already stopped.` ); return { success: true, @@ -345,13 +341,13 @@ export function registerAppHandlers() { } const { process, processId } = appInfo; - console.log( + logger.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( + logger.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 @@ -367,7 +363,7 @@ export function registerAppHandlers() { return { success: true }; } catch (error: any) { - console.error( + logger.error( `Error stopping app ${appId} (PID: ${process.pid}, processId: ${processId}):`, error ); @@ -384,24 +380,21 @@ export function registerAppHandlers() { event: Electron.IpcMainInvokeEvent, { appId }: { appId: number } ) => { - // Static server worker is NOT terminated here anymore - + logger.log(`Restarting app ${appId}`); 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 + logger.log( + `Stopping app ${appId} (processId ${processId}) before restart` ); await killProcess(process); runningApps.delete(appId); } else { - console.log( - `App ${appId} not running in local-node mode, proceeding to start.` - ); + logger.log(`App ${appId} not running. Proceeding to start.`); } // Kill any orphaned process on port 32100 (in case previous run left it) @@ -417,7 +410,7 @@ export function registerAppHandlers() { } const appPath = getDyadAppPath(app.path); - console.debug( + logger.debug( `Executing app ${appId} in path ${app.path} after restart request` ); // Adjusted log @@ -425,7 +418,7 @@ export function registerAppHandlers() { return { success: true }; } catch (error) { - console.error(`Error restarting app ${appId}:`, error); + logger.error(`Error restarting app ${appId}:`, error); throw error; } }); @@ -461,7 +454,7 @@ export function registerAppHandlers() { timestamp: commit.commit.author.timestamp, })) satisfies Version[]; } catch (error: any) { - console.error(`Error listing versions for app ${appId}:`, error); + logger.error(`Error listing versions for app ${appId}:`, error); throw new Error(`Failed to list versions: ${error.message}`); } }); @@ -552,7 +545,7 @@ export function registerAppHandlers() { return { success: true }; } catch (error: any) { - console.error( + logger.error( `Error reverting to version ${previousVersionId} for app ${appId}:`, error ); @@ -587,7 +580,7 @@ export function registerAppHandlers() { return { success: true }; } catch (error: any) { - console.error( + logger.error( `Error checking out version ${versionId} for app ${appId}:`, error ); @@ -614,7 +607,7 @@ export function registerAppHandlers() { try { return await extractCodebase(appPath, maxFiles); } catch (error) { - console.error(`Error extracting codebase for app ${appId}:`, error); + logger.error(`Error extracting codebase for app ${appId}:`, error); throw new Error( `Failed to extract codebase: ${(error as any).message}` ); @@ -673,10 +666,7 @@ export function registerAppHandlers() { return { success: true }; } catch (error: any) { - console.error( - `Error writing file ${filePath} for app ${appId}:`, - error - ); + logger.error(`Error writing file ${filePath} for app ${appId}:`, error); throw new Error(`Failed to write file: ${error.message}`); } } @@ -699,14 +689,11 @@ export function registerAppHandlers() { if (runningApps.has(appId)) { const appInfo = runningApps.get(appId)!; try { - console.log(`Stopping local-node app ${appId} before deletion.`); // Adjusted log + logger.log(`Stopping 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 + logger.error(`Error stopping app ${appId} before deletion:`, error); // Adjusted log // Continue with deletion even if stopping fails } } @@ -716,7 +703,7 @@ export function registerAppHandlers() { try { await fsPromises.rm(appPath, { recursive: true, force: true }); } catch (error: any) { - console.error(`Error deleting app files for app ${appId}:`, error); + logger.error(`Error deleting app files for app ${appId}:`, error); throw new Error(`Failed to delete app files: ${error.message}`); } @@ -726,7 +713,7 @@ export function registerAppHandlers() { // 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); + logger.error(`Error deleting app ${appId} from database:`, error); throw new Error(`Failed to delete app from database: ${error.message}`); } }); @@ -776,10 +763,7 @@ export function registerAppHandlers() { await killProcess(appInfo.process); runningApps.delete(appId); } catch (error: any) { - console.error( - `Error stopping app ${appId} before renaming:`, - error - ); + logger.error(`Error stopping app ${appId} before renaming:`, error); throw new Error( `Failed to stop app before renaming: ${error.message}` ); @@ -807,7 +791,7 @@ export function registerAppHandlers() { // Move the files await fsPromises.rename(oldAppPath, newAppPath); } catch (error: any) { - console.error( + logger.error( `Error moving app files from ${oldAppPath} to ${newAppPath}:`, error ); @@ -833,14 +817,14 @@ export function registerAppHandlers() { try { await fsPromises.rename(newAppPath, oldAppPath); } catch (rollbackError) { - console.error( + logger.error( `Failed to rollback file move during rename error:`, rollbackError ); } } - console.error(`Error updating app ${appId} in database:`, error); + logger.error(`Error updating app ${appId} in database:`, error); throw new Error(`Failed to update app in database: ${error.message}`); } }); @@ -848,16 +832,9 @@ export function registerAppHandlers() { ); ipcMain.handle("reset-all", async () => { - console.log("start: resetting all apps and settings."); - // 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; - } + logger.log("start: resetting all apps and settings."); // Stop all running apps first - console.log("stopping all running apps..."); + logger.log("stopping all running apps..."); const runningAppIds = Array.from(runningApps.keys()); for (const appId of runningAppIds) { try { @@ -865,12 +842,12 @@ export function registerAppHandlers() { await killProcess(appInfo.process); runningApps.delete(appId); } catch (error) { - console.error(`Error stopping app ${appId} during reset:`, error); + logger.error(`Error stopping app ${appId} during reset:`, error); // Continue with reset even if stopping fails } } - console.log("all running apps stopped."); - console.log("deleting database..."); + logger.log("all running apps stopped."); + logger.log("deleting database..."); // 1. Drop the database by deleting the SQLite file const dbPath = getDatabasePath(); if (fs.existsSync(dbPath)) { @@ -879,31 +856,31 @@ export function registerAppHandlers() { db.$client.close(); } await fsPromises.unlink(dbPath); - console.log(`Database file deleted: ${dbPath}`); + logger.log(`Database file deleted: ${dbPath}`); } - console.log("database deleted."); - console.log("deleting settings..."); + logger.log("database deleted."); + logger.log("deleting settings..."); // 2. 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}`); + logger.log(`Settings file deleted: ${settingsPath}`); } - console.log("settings deleted."); + logger.log("settings deleted."); // 3. Remove all app files recursively // Doing this last because it's the most time-consuming and the least important // in terms of resetting the app state. - console.log("removing all app files..."); + logger.log("removing all app files..."); 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 }); } - console.log("all app files removed."); - console.log("reset all complete."); + logger.log("all app files removed."); + logger.log("reset all complete."); return { success: true, message: "Successfully reset everything" }; }); diff --git a/src/ipc/handlers/chat_stream_handlers.ts b/src/ipc/handlers/chat_stream_handlers.ts index 3fcb7d6..b4039e6 100644 --- a/src/ipc/handlers/chat_stream_handlers.ts +++ b/src/ipc/handlers/chat_stream_handlers.ts @@ -12,6 +12,9 @@ import { processFullResponseActions } from "../processors/response_processor"; import { streamTestResponse } from "./testing_chat_handlers"; import { getTestResponse } from "./testing_chat_handlers"; import { getModelClient } from "../utils/get_model_client"; +import log from "electron-log"; + +const logger = log.scope("chat_stream_handlers"); // Track active streams for cancellation const activeStreams = new Map(); @@ -125,12 +128,12 @@ export function registerChatStreamHandlers() { const appPath = getDyadAppPath(updatedChat.app.path); try { codebaseInfo = await extractCodebase(appPath); - console.log(`Extracted codebase information from ${appPath}`); + logger.log(`Extracted codebase information from ${appPath}`); } catch (error) { - console.error("Error extracting codebase:", error); + logger.error("Error extracting codebase:", error); } } - console.log( + logger.log( "codebaseInfo: length", codebaseInfo.length, "estimated tokens", @@ -170,7 +173,7 @@ export function registerChatStreamHandlers() { }, ], onError: (error) => { - console.error("Error streaming text:", error); + logger.error("Error streaming text:", error); const message = (error as any)?.error?.message || JSON.stringify(error); event.sender.send( @@ -204,7 +207,7 @@ export function registerChatStreamHandlers() { // If the stream was aborted, exit early if (abortController.signal.aborted) { - console.log(`Stream for chat ${req.chatId} was aborted`); + logger.log(`Stream for chat ${req.chatId} was aborted`); break; } } @@ -222,10 +225,10 @@ export function registerChatStreamHandlers() { role: "assistant", content: `${partialResponse}\n\n[Response cancelled by user]`, }); - console.log(`Saved partial response for chat ${chatId}`); + logger.log(`Saved partial response for chat ${chatId}`); partialResponses.delete(chatId); } catch (error) { - console.error( + logger.error( `Error saving partial response for chat ${chatId}:`, error ); @@ -305,7 +308,7 @@ export function registerChatStreamHandlers() { // Return the chat ID for backwards compatibility return req.chatId; } catch (error) { - console.error("[MAIN] API error:", error); + logger.error("Error calling LLM:", error); event.sender.send( "chat:response:error", `Sorry, there was an error processing your request: ${error}` @@ -324,9 +327,9 @@ export function registerChatStreamHandlers() { // Abort the stream abortController.abort(); activeStreams.delete(chatId); - console.log(`Aborted stream for chat ${chatId}`); + logger.log(`Aborted stream for chat ${chatId}`); } else { - console.warn(`No active stream found for chat ${chatId}`); + logger.warn(`No active stream found for chat ${chatId}`); } // Send the end event to the renderer diff --git a/src/ipc/handlers/debug_handlers.ts b/src/ipc/handlers/debug_handlers.ts new file mode 100644 index 0000000..10bcdd6 --- /dev/null +++ b/src/ipc/handlers/debug_handlers.ts @@ -0,0 +1,94 @@ +import { ipcMain, app } from "electron"; +import { platform, arch } from "os"; +import { SystemDebugInfo } from "../ipc_types"; +import { readSettings } from "../../main/settings"; +import { execSync } from "child_process"; +import log from "electron-log"; +import path from "path"; +import fs from "fs"; +import { runShellCommand } from "../utils/runShellCommand"; + +export function registerDebugHandlers() { + ipcMain.handle( + "get-system-debug-info", + async (): Promise => { + console.log("IPC: get-system-debug-info called"); + + // Get Node.js and pnpm versions + let nodeVersion: string | null = null; + let pnpmVersion: string | null = null; + let nodePath: string | null = null; + try { + nodeVersion = await runShellCommand("node --version"); + } catch (err) { + console.error("Failed to get Node.js version:", err); + } + + try { + pnpmVersion = await runShellCommand("pnpm --version"); + } catch (err) { + console.error("Failed to get pnpm version:", err); + } + + try { + if (platform() === "win32") { + nodePath = await runShellCommand("where.exe node"); + } else { + nodePath = await runShellCommand("which node"); + } + } catch (err) { + console.error("Failed to get node path:", err); + } + + // Get Dyad version from package.json + const packageJsonPath = path.resolve( + __dirname, + "..", + "..", + "package.json" + ); + let dyadVersion = "unknown"; + try { + const packageJson = JSON.parse( + fs.readFileSync(packageJsonPath, "utf8") + ); + dyadVersion = packageJson.version; + } catch (err) { + console.error("Failed to read package.json:", err); + } + + // Get telemetry info from settings + const settings = readSettings(); + const telemetryId = settings.telemetryUserId || "unknown"; + + // Get logs from electron-log + let logs = ""; + try { + const logPath = log.transports.file.getFile().path; + if (fs.existsSync(logPath)) { + const logContent = fs.readFileSync(logPath, "utf8"); + const logLines = logContent.split("\n"); + logs = logLines.slice(-100).join("\n"); + } + } catch (err) { + console.error("Failed to read log file:", err); + logs = `Error reading logs: ${err}`; + } + + return { + nodeVersion, + pnpmVersion, + nodePath, + telemetryId, + telemetryConsent: settings.telemetryConsent || "unknown", + telemetryUrl: "https://us.i.posthog.com", // Hardcoded from renderer.tsx + dyadVersion, + platform: process.platform, + architecture: arch(), + logs, + }; + } + ); + + console.log("Registered debug IPC handlers"); +} diff --git a/src/ipc/handlers/github_handlers.ts b/src/ipc/handlers/github_handlers.ts index 301fc73..eef5a46 100644 --- a/src/ipc/handlers/github_handlers.ts +++ b/src/ipc/handlers/github_handlers.ts @@ -16,6 +16,9 @@ import { db } from "../../db"; import { apps } from "../../db/schema"; import { eq } from "drizzle-orm"; import { GithubUser } from "../../lib/schemas"; +import log from "electron-log"; + +const logger = log.scope("github_handlers"); // --- GitHub Device Flow Constants --- // TODO: Fetch this securely, e.g., from environment variables or a config file @@ -65,7 +68,7 @@ export async function getGithubUser(): Promise { }); return { email }; } catch (err) { - console.error("[GitHub Handler] Failed to get GitHub username:", err); + logger.error("[GitHub Handler] Failed to get GitHub username:", err); return null; } } @@ -78,15 +81,13 @@ export async function getGithubUser(): Promise { async function pollForAccessToken(event: IpcMainInvokeEvent) { if (!currentFlowState || !currentFlowState.isPolling) { - console.log("[GitHub Handler] Polling stopped or no active flow."); + logger.debug("[GitHub Handler] Polling stopped or no active flow."); return; } const { deviceCode, interval } = currentFlowState; - console.log( - `[GitHub Handler] Polling for token with device code: ${deviceCode}` - ); + logger.debug("[GitHub Handler] Polling for token with device code"); event.sender.send("github:flow-update", { message: "Polling GitHub for authorization...", }); @@ -108,10 +109,7 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) { const data = await response.json(); if (response.ok && data.access_token) { - // --- SUCCESS --- - console.log( - "[GitHub Handler] Successfully obtained GitHub Access Token." - ); // TODO: Store this token securely! + logger.log("Successfully obtained GitHub Access Token."); event.sender.send("github:flow-success", { message: "Successfully connected!", }); @@ -120,13 +118,13 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) { value: data.access_token, }, }); - // TODO: Associate token with appId if provided + stopPolling(); return; } else if (data.error) { switch (data.error) { case "authorization_pending": - console.log("[GitHub Handler] Authorization pending..."); + logger.debug("Authorization pending..."); event.sender.send("github:flow-update", { message: "Waiting for user authorization...", }); @@ -138,9 +136,7 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) { break; case "slow_down": const newInterval = interval + 5; - console.log( - `[GitHub Handler] Slow down requested. New interval: ${newInterval}s` - ); + logger.debug(`Slow down requested. New interval: ${newInterval}s`); currentFlowState.interval = newInterval; // Update interval event.sender.send("github:flow-update", { message: `GitHub asked to slow down. Retrying in ${newInterval}s...`, @@ -151,24 +147,22 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) { ); break; case "expired_token": - console.error("[GitHub Handler] Device code expired."); + logger.error("Device code expired."); event.sender.send("github:flow-error", { error: "Verification code expired. Please try again.", }); stopPolling(); break; case "access_denied": - console.error("[GitHub Handler] Access denied by user."); + logger.error("Access denied by user."); event.sender.send("github:flow-error", { error: "Authorization denied by user.", }); stopPolling(); break; default: - console.error( - `[GitHub Handler] Unknown GitHub error: ${ - data.error_description || data.error - }` + logger.error( + `Unknown GitHub error: ${data.error_description || data.error}` ); event.sender.send("github:flow-error", { error: `GitHub authorization error: ${ @@ -182,10 +176,7 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) { throw new Error(`Unknown response structure: ${JSON.stringify(data)}`); } } catch (error) { - console.error( - "[GitHub Handler] Error polling for GitHub access token:", - error - ); + logger.error("Error polling for GitHub access token:", error); event.sender.send("github:flow-error", { error: `Network or unexpected error during polling: ${ error instanceof Error ? error.message : String(error) @@ -202,12 +193,9 @@ function stopPolling() { } currentFlowState.isPolling = false; currentFlowState.timeoutId = null; - // Maybe keep window reference for a bit if needed, or clear it - // currentFlowState.window = null; - console.log("[GitHub Handler] Polling stopped."); + + logger.debug("[GitHub Handler] Polling stopped."); } - // Setting to null signifies no active flow - // currentFlowState = null; // Decide if you want to clear immediately or allow potential restart } // --- IPC Handlers --- @@ -216,15 +204,11 @@ function handleStartGithubFlow( event: IpcMainInvokeEvent, args: { appId: number | null } ) { - console.log( - `[GitHub Handler] Received github:start-flow for appId: ${args.appId}` - ); + logger.debug(`Received github:start-flow for appId: ${args.appId}`); // If a flow is already in progress, maybe cancel it or send an error if (currentFlowState && currentFlowState.isPolling) { - console.warn( - "[GitHub Handler] Another GitHub flow is already in progress." - ); + logger.warn("Another GitHub flow is already in progress."); event.sender.send("github:flow-error", { error: "Another connection process is already active.", }); @@ -234,7 +218,7 @@ function handleStartGithubFlow( // Store the window that initiated the request const window = BrowserWindow.fromWebContents(event.sender); if (!window) { - console.error("[GitHub Handler] Could not get BrowserWindow instance."); + logger.error("Could not get BrowserWindow instance."); return; } @@ -272,7 +256,7 @@ function handleStartGithubFlow( return res.json(); }) .then((data) => { - console.log("[GitHub Handler] Received device code response:", data); + logger.info("Received device code response"); if (!currentFlowState) return; // Flow might have been cancelled currentFlowState.deviceCode = data.device_code; @@ -293,10 +277,7 @@ function handleStartGithubFlow( ); }) .catch((error) => { - console.error( - "[GitHub Handler] Error initiating GitHub device flow:", - error - ); + logger.error("Error initiating GitHub device flow:", error); event.sender.send("github:flow-error", { error: `Failed to start GitHub connection: ${error.message}`, }); @@ -305,15 +286,6 @@ function handleStartGithubFlow( }); } -// Optional: Handle cancellation from renderer -// function handleCancelGithubFlow(event: IpcMainEvent) { -// console.log('[GitHub Handler] Received github:cancel-flow'); -// stopPolling(); -// currentFlowState = null; // Clear state on cancel -// // Optionally send confirmation back -// event.sender.send('github:flow-cancelled', { message: 'GitHub flow cancelled.' }); -// } - // --- GitHub Repo Availability Handler --- async function handleIsRepoAvailable( event: IpcMainInvokeEvent, @@ -453,7 +425,6 @@ async function handlePushToGithub( // --- Registration --- export function registerGithubHandlers() { ipcMain.handle("github:start-flow", handleStartGithubFlow); - // ipcMain.on('github:cancel-flow', handleCancelGithubFlow); // Uncomment if you add cancellation ipcMain.handle("github:is-repo-available", handleIsRepoAvailable); ipcMain.handle("github:create-repo", handleCreateRepo); ipcMain.handle("github:push", handlePushToGithub); diff --git a/src/ipc/handlers/node_handlers.ts b/src/ipc/handlers/node_handlers.ts index cab50a8..706ddbf 100644 --- a/src/ipc/handlers/node_handlers.ts +++ b/src/ipc/handlers/node_handlers.ts @@ -1,51 +1,28 @@ import { ipcMain, app } from "electron"; -import { exec, execSync, spawn } from "child_process"; +import { exec, execSync } from "child_process"; import { platform, arch } from "os"; import { NodeSystemInfo } from "../ipc_types"; import fixPath from "fix-path"; +import { runShellCommand } from "../utils/runShellCommand"; +import log from "electron-log"; -function checkCommandExists(command: string): Promise { - return new Promise((resolve) => { - let output = ""; - const process = spawn(command, { - 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}": ${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}" failed with code ${code}`); - resolve(null); // Command failed - } - }); - }); -} +const logger = log.scope("node_handlers"); export function registerNodeHandlers() { ipcMain.handle("nodejs-status", async (): Promise => { + logger.log( + "handling ipc: nodejs-status for platform:", + platform(), + "and arch:", + arch() + ); // Run checks in parallel const [nodeVersion, pnpmVersion] = await Promise.all([ - checkCommandExists("node --version"), + runShellCommand("node --version"), // First, check if pnpm is installed. // If not, try to install it using corepack. // If both fail, then pnpm is not available. - checkCommandExists( + runShellCommand( "pnpm --version || (corepack enable pnpm && pnpm --version) || (npm install -g pnpm@latest-10 && pnpm --version)" ), ]); @@ -66,7 +43,7 @@ export function registerNodeHandlers() { }); ipcMain.handle("reload-env-path", async (): Promise => { - console.debug("Reloading env path, previously:", process.env.PATH); + logger.debug("Reloading env path, previously:", process.env.PATH); if (platform() === "win32") { const newPath = execSync("cmd /c echo %PATH%", { encoding: "utf8", @@ -75,6 +52,6 @@ export function registerNodeHandlers() { } else { fixPath(); } - console.debug("Reloaded env path, now:", process.env.PATH); + logger.debug("Reloaded env path, now:", process.env.PATH); }); } diff --git a/src/ipc/handlers/proposal_handlers.ts b/src/ipc/handlers/proposal_handlers.ts index 08bac4a..e3bcc20 100644 --- a/src/ipc/handlers/proposal_handlers.ts +++ b/src/ipc/handlers/proposal_handlers.ts @@ -10,6 +10,9 @@ import { getDyadWriteTags, processFullResponseActions, } from "../processors/response_processor"; +import log from "electron-log"; + +const logger = log.scope("proposal_handlers"); // Placeholder Proposal data (can be removed or kept for reference) // const placeholderProposal: Proposal = { ... }; @@ -33,7 +36,7 @@ const getProposalHandler = async ( _event: IpcMainInvokeEvent, { chatId }: { chatId: number } ): Promise => { - console.log(`IPC: get-proposal called for chatId: ${chatId}`); + logger.log(`IPC: get-proposal called for chatId: ${chatId}`); try { // Find the latest ASSISTANT message for the chat @@ -56,7 +59,7 @@ const getProposalHandler = async ( if (latestAssistantMessage?.content && latestAssistantMessage.id) { const messageId = latestAssistantMessage.id; // Get the message ID - console.log( + logger.log( `Found latest assistant message (ID: ${messageId}), parsing content...` ); const messageContent = latestAssistantMessage.content; @@ -78,20 +81,25 @@ const getProposalHandler = async ( summary: tag.description ?? "(no change summary found)", // Generic summary })), }; - console.log("Generated proposal on the fly:", proposal); + logger.log( + "Generated code proposal. title=", + proposal.title, + "files=", + proposal.filesChanged.length + ); return { proposal, chatId, messageId }; // Return proposal and messageId } else { - console.log( + logger.log( "No relevant tags found in the latest assistant message content." ); return null; // No proposal could be generated } } else { - console.log(`No assistant message found for chatId: ${chatId}`); + logger.log(`No assistant message found for chatId: ${chatId}`); return null; // No message found } } catch (error) { - console.error(`Error processing proposal for chatId ${chatId}:`, error); + logger.error(`Error processing proposal for chatId ${chatId}:`, error); return null; // Indicate DB or processing error } }; @@ -101,7 +109,7 @@ const approveProposalHandler = async ( _event: IpcMainInvokeEvent, { chatId, messageId }: { chatId: number; messageId: number } ): Promise<{ success: boolean; error?: string }> => { - console.log( + logger.log( `IPC: approve-proposal called for chatId: ${chatId}, messageId: ${messageId}` ); @@ -119,7 +127,7 @@ const approveProposalHandler = async ( }); if (!messageToApprove?.content) { - console.error( + logger.error( `Assistant message not found for chatId: ${chatId}, messageId: ${messageId}` ); return { success: false, error: "Assistant message not found." }; @@ -137,7 +145,7 @@ const approveProposalHandler = async ( ); if (processResult.error) { - console.error( + logger.error( `Error processing actions for message ${messageId}:`, processResult.error ); @@ -151,10 +159,7 @@ const approveProposalHandler = async ( return { success: true }; } catch (error) { - console.error( - `Error approving proposal for messageId ${messageId}:`, - error - ); + logger.error(`Error approving proposal for messageId ${messageId}:`, error); return { success: false, error: (error as Error)?.message || "Unknown error", @@ -167,7 +172,7 @@ const rejectProposalHandler = async ( _event: IpcMainInvokeEvent, { chatId, messageId }: { chatId: number; messageId: number } ): Promise<{ success: boolean; error?: string }> => { - console.log( + logger.log( `IPC: reject-proposal called for chatId: ${chatId}, messageId: ${messageId}` ); @@ -183,7 +188,7 @@ const rejectProposalHandler = async ( }); if (!messageToReject) { - console.error( + logger.error( `Assistant message not found for chatId: ${chatId}, messageId: ${messageId}` ); return { success: false, error: "Assistant message not found." }; @@ -195,13 +200,10 @@ const rejectProposalHandler = async ( .set({ approvalState: "rejected" }) .where(eq(messages.id, messageId)); - console.log(`Message ${messageId} marked as rejected.`); + logger.log(`Message ${messageId} marked as rejected.`); return { success: true }; } catch (error) { - console.error( - `Error rejecting proposal for messageId ${messageId}:`, - error - ); + logger.error(`Error rejecting proposal for messageId ${messageId}:`, error); return { success: false, error: (error as Error)?.message || "Unknown error", @@ -214,5 +216,4 @@ export function registerProposalHandlers() { ipcMain.handle("get-proposal", getProposalHandler); ipcMain.handle("approve-proposal", approveProposalHandler); ipcMain.handle("reject-proposal", rejectProposalHandler); - console.log("Registered proposal IPC handlers (get, approve, reject)"); } diff --git a/src/ipc/handlers/shell_handler.ts b/src/ipc/handlers/shell_handler.ts index b392ec5..22ed507 100644 --- a/src/ipc/handlers/shell_handler.ts +++ b/src/ipc/handlers/shell_handler.ts @@ -1,4 +1,7 @@ import { ipcMain, shell } from "electron"; +import log from "electron-log"; + +const logger = log.scope("shell_handlers"); export function registerShellHandlers() { ipcMain.handle("open-external-url", async (_event, url: string) => { @@ -6,15 +9,16 @@ export function registerShellHandlers() { // Basic validation to ensure it's a http/https url if (url && (url.startsWith("http://") || url.startsWith("https://"))) { await shell.openExternal(url); + logger.debug("Opened external URL:", url); return { success: true }; } - console.error("Attempted to open invalid or non-http URL:", url); - return { - success: false, - error: "Invalid URL provided. Only http/https URLs are allowed.", - }; + logger.error("Attempted to open invalid or non-http URL:", url); + return { + success: false, + error: "Invalid URL provided. Only http/https URLs are allowed.", + }; } catch (error) { - console.error(`Failed to open external URL ${url}:`, error); + logger.error(`Failed to open external URL ${url}:`, error); return { success: false, error: (error as Error).message }; } }); diff --git a/src/ipc/handlers/shell_handlers.ts b/src/ipc/handlers/shell_handlers.ts deleted file mode 100644 index 4859797..0000000 --- a/src/ipc/handlers/shell_handlers.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { IpcMainInvokeEvent } from "electron"; -import { shell } from "electron"; - -export async function handleShellOpenExternal( - _event: IpcMainInvokeEvent, - url: string -): Promise { - // Basic validation to ensure it's likely a URL - if (url && (url.startsWith("http://") || url.startsWith("https://"))) { - await shell.openExternal(url); - } else { - console.error(`Invalid URL attempt blocked: ${url}`); - // Optionally, you could throw an error back to the renderer - // throw new Error("Invalid or insecure URL provided."); - } -} diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index 00cc47c..d010c4b 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -16,6 +16,7 @@ import type { NodeSystemInfo, Message, Version, + SystemDebugInfo, } from "./ipc_types"; import type { CodeProposal, ProposalResult } from "@/lib/schemas"; import { showError } from "@/lib/toast"; @@ -670,4 +671,15 @@ export class IpcClient { } } // --- End Proposal Management --- + + // Get system debug information + public async getSystemDebugInfo(): Promise { + try { + const result = await this.ipcRenderer.invoke("get-system-debug-info"); + return result; + } catch (error) { + showError(error); + throw error; + } + } } diff --git a/src/ipc/ipc_host.ts b/src/ipc/ipc_host.ts index e7d99cd..2151f0a 100644 --- a/src/ipc/ipc_host.ts +++ b/src/ipc/ipc_host.ts @@ -7,6 +7,7 @@ import { registerDependencyHandlers } from "./handlers/dependency_handlers"; import { registerGithubHandlers } from "./handlers/github_handlers"; import { registerNodeHandlers } from "./handlers/node_handlers"; import { registerProposalHandlers } from "./handlers/proposal_handlers"; +import { registerDebugHandlers } from "./handlers/debug_handlers"; export function registerIpcHandlers() { // Register all IPC handlers by category @@ -19,4 +20,5 @@ export function registerIpcHandlers() { registerGithubHandlers(); registerNodeHandlers(); registerProposalHandlers(); + registerDebugHandlers(); } diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts index 1daea80..43b59e5 100644 --- a/src/ipc/ipc_types.ts +++ b/src/ipc/ipc_types.ts @@ -76,3 +76,16 @@ export interface NodeSystemInfo { pnpmVersion: string | null; nodeDownloadUrl: string; } + +export interface SystemDebugInfo { + nodeVersion: string | null; + pnpmVersion: string | null; + nodePath: string | null; + telemetryId: string; + telemetryConsent: string; + telemetryUrl: string; + dyadVersion: string; + platform: string; + architecture: string; + logs: string; +} diff --git a/src/ipc/utils/runShellCommand.ts b/src/ipc/utils/runShellCommand.ts new file mode 100644 index 0000000..ed15d7d --- /dev/null +++ b/src/ipc/utils/runShellCommand.ts @@ -0,0 +1,41 @@ +import { spawn } from "child_process"; +import log from "electron-log"; + +const logger = log.scope("runShellCommand"); + +export function runShellCommand(command: string): Promise { + logger.log(`Running command: ${command}`); + return new Promise((resolve) => { + let output = ""; + const process = spawn(command, { + 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 + logger.warn(`Stderr from "${command}": ${data.toString().trim()}`); + }); + + process.on("error", (error) => { + logger.error(`Error executing command "${command}":`, error.message); + resolve(null); // Command execution failed + }); + + process.on("close", (code) => { + if (code === 0) { + logger.debug( + `Command "${command}" succeeded with code ${code}: ${output.trim()}` + ); + resolve(output.trim()); // Command succeeded, return trimmed output + } else { + logger.error(`Command "${command}" failed with code ${code}`); + resolve(null); // Command failed + } + }); + }); +} diff --git a/src/main.ts b/src/main.ts index ec52072..d09aad7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,11 +7,9 @@ import started from "electron-squirrel-startup"; import { updateElectronApp } from "update-electron-app"; import log from "electron-log"; -console.log = log.log; -console.error = log.error; -console.warn = log.warn; -console.info = log.info; -console.debug = log.debug; +log.errorHandler.startCatching(); +log.eventLogger.startLogging(); +log.log("HELLO WORLD"); updateElectronApp(); // additional configuration options available diff --git a/src/preload.ts b/src/preload.ts index 09dde42..1e243c9 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -41,6 +41,7 @@ const validInvokeChannels = [ "get-proposal", "approve-proposal", "reject-proposal", + "get-system-debug-info", ] as const; // Add valid receive channels