From f2b157b8bc84bfc3ee09d96b5d488c0a0b344f8c Mon Sep 17 00:00:00 2001 From: Will Chen Date: Thu, 17 Apr 2025 23:05:44 -0700 Subject: [PATCH] Simplified setup flow: user installs node.js; pnpm is configured --- src/components/SetupBanner.tsx | 301 +++++++++++++++--------------- src/ipc/handlers/node_handlers.ts | 137 +++----------- src/ipc/ipc_client.ts | 21 +-- src/ipc/ipc_types.ts | 15 +- src/pages/home.tsx | 4 - src/preload.ts | 1 + 6 files changed, 190 insertions(+), 289 deletions(-) diff --git a/src/components/SetupBanner.tsx b/src/components/SetupBanner.tsx index 2d994cd..7185891 100644 --- a/src/components/SetupBanner.tsx +++ b/src/components/SetupBanner.tsx @@ -20,29 +20,26 @@ import { } from "@/components/ui/accordion"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import { showError } from "@/lib/toast"; - +import { NodeSystemInfo } from "@/ipc/ipc_types"; export function SetupBanner() { const navigate = useNavigate(); - const { isAnyProviderSetup } = useSettings(); - const [nodeVersion, setNodeVersion] = useState(null); - const [pnpmVersion, setPnpmVersion] = useState(null); + const { isAnyProviderSetup, loading } = useSettings(); + const [nodeSystemInfo, setNodeSystemInfo] = useState( + null + ); const [nodeCheckError, setNodeCheckError] = useState(false); - const [nodeInstallError, setNodeInstallError] = useState(null); const [nodeInstallLoading, setNodeInstallLoading] = useState(false); const checkNode = useCallback(async () => { try { setNodeCheckError(false); const status = await IpcClient.getInstance().getNodejsStatus(); - setNodeVersion(status.nodeVersion); - setPnpmVersion(status.pnpmVersion); + setNodeSystemInfo(status); } catch (error) { console.error("Failed to check Node.js status:", error); - setNodeVersion(null); - setPnpmVersion(null); + setNodeSystemInfo(null); setNodeCheckError(true); } - }, [setNodeVersion, setNodeCheckError]); + }, [setNodeSystemInfo, setNodeCheckError]); useEffect(() => { checkNode(); @@ -57,39 +54,35 @@ export function SetupBanner() { const handleNodeInstallClick = async () => { setNodeInstallLoading(true); - try { - const result = await IpcClient.getInstance().installNode(); - if (!result.success) { - showError(result.errorMessage); - setNodeInstallError(result.errorMessage || "Unknown error"); - } else { - setNodeVersion(result.nodeVersion); - setPnpmVersion(result.pnpmVersion); - } - } catch (error) { - showError("Failed to install Node.js. " + (error as Error).message); - setNodeInstallError( - "Failed to install Node.js. " + (error as Error).message - ); - } finally { - setNodeInstallLoading(false); - } + IpcClient.getInstance().openExternalUrl(nodeSystemInfo!.nodeDownloadUrl); }; - const isNodeSetupComplete = !!nodeVersion && !!pnpmVersion; - const isAiProviderSetup = isAnyProviderSetup(); + const finishNodeInstallAndRestart = () => { + IpcClient.getInstance().reloadDyad(); + }; + + const isNodeSetupComplete = Boolean( + nodeSystemInfo?.nodeVersion && nodeSystemInfo?.pnpmVersion + ); const itemsNeedAction: string[] = []; - if (!isNodeSetupComplete) itemsNeedAction.push("node-setup"); - if (isNodeSetupComplete && !isAiProviderSetup) + if (!isNodeSetupComplete && nodeSystemInfo) { + itemsNeedAction.push("node-setup"); + } + if (!isAnyProviderSetup() && !loading) { itemsNeedAction.push("ai-setup"); + } if (itemsNeedAction.length === 0) { - return null; + return ( +

+ Build your dream app +

+ ); } const bannerClasses = cn( - "w-full mb-8 border rounded-xl shadow-sm overflow-hidden", + "w-full mb-6 border rounded-xl shadow-sm overflow-hidden", "border-zinc-200 dark:border-zinc-700" ); @@ -105,131 +98,131 @@ export function SetupBanner() { }; return ( -
- - +

+ Follow these steps and you'll be ready to start building with Dyad... +

+
+ - -
-
- {getStatusIcon(isNodeSetupComplete, nodeCheckError)} - - 1. Check Node.js Runtime - -
-
-
- - {nodeInstallError && ( -

- {nodeInstallError} -

- )} - {nodeCheckError ? ( -

- Error checking Node.js status. Please ensure node.js are - installed correctly and accessible in your system's PATH. -

- ) : isNodeSetupComplete ? ( -

Node.js ({nodeVersion}) installed.

- ) : ( -
-

- Node.js is required to run apps locally. We also use pnpm as - our package manager as it's faster and more efficient than - npm. -

- -
- )} -
- - - - !isNodeSetupComplete && e.preventDefault()} > -
-
- {getStatusIcon(isAiProviderSetup)} - - 2. Setup AI Model Access - -
-
-
- -

- Connect your preferred AI provider to start generating code. -

-
- isNodeSetupComplete && e.key === "Enter" && handleAiSetupClick() - } - > -
-
-
- -
-
-

- Setup Google Gemini API Key -

-

- - Use Google Gemini for free -

-
+ +
+
+ {getStatusIcon(isNodeSetupComplete, nodeCheckError)} + + 1. Install Node.js +
-
-
- - - -
+ + + {nodeCheckError && ( +

+ Error checking Node.js status. Try installing Node.js. +

+ )} + {isNodeSetupComplete ? ( +

+ Node.js ({nodeSystemInfo!.nodeVersion}) installed.{" "} + {nodeSystemInfo!.pnpmVersion && ( + + pnpm ({nodeSystemInfo!.pnpmVersion}) installed. + + )} +

+ ) : ( +
+

Node.js is required to run apps locally.

+

+ After you have installed node.js, click "Finish setup" to + restart Dyad. +

+ {nodeInstallLoading ? ( + + ) : ( + + )} +
+ )} +
+ + + + +
+
+ {getStatusIcon(isAnyProviderSetup())} + + 2. Setup AI Model Access + +
+
+
+ +

+ Connect your preferred AI provider to start generating code. +

+
+ isNodeSetupComplete && + e.key === "Enter" && + handleAiSetupClick() + } + > +
+
+
+ +
+
+

+ Setup Google Gemini API Key +

+

+ + Use Google Gemini for free +

+
+
+ +
+
+
+
+ +
+ ); } diff --git a/src/ipc/handlers/node_handlers.ts b/src/ipc/handlers/node_handlers.ts index 6131ac9..0732e00 100644 --- a/src/ipc/handlers/node_handlers.ts +++ b/src/ipc/handlers/node_handlers.ts @@ -1,58 +1,12 @@ -import { ipcMain } from "electron"; +import { ipcMain, app } from "electron"; import { spawn } from "child_process"; -import { platform } from "os"; -import { InstallNodeResult } from "../ipc_types"; -type ShellResult = - | { - success: true; - output: string; - } - | { - success: false; - errorMessage: string; - }; - -function runShell(command: string): Promise { - return new Promise((resolve) => { - let stdout = ""; - let stderr = ""; - const process = spawn(command, { - shell: true, - stdio: ["ignore", "pipe", "pipe"], // ignore stdin, pipe stdout/stderr - }); - - process.stdout?.on("data", (data) => { - stdout += data.toString(); - }); - - process.stderr?.on("data", (data) => { - stderr += data.toString(); - }); - - process.on("error", (error) => { - console.error(`Error executing command "${command}":`, error.message); - resolve({ success: false, errorMessage: error.message }); - }); - - process.on("close", (code) => { - if (code === 0) { - resolve({ success: true, output: stdout.trim() }); - } else { - const errorMessage = - stderr.trim() || `Command failed with code ${code}`; - console.error( - `Command "${command}" failed with code ${code}: ${stderr.trim()}` - ); - resolve({ success: false, errorMessage }); - } - }); - }); -} +import { platform, arch } from "os"; +import { NodeSystemInfo } from "../ipc_types"; function checkCommandExists(command: string): Promise { return new Promise((resolve) => { let output = ""; - const process = spawn(command, ["--version"], { + const process = spawn(command, { shell: true, stdio: ["ignore", "pipe", "pipe"], // ignore stdin, pipe stdout/stderr }); @@ -87,64 +41,35 @@ function checkCommandExists(command: string): Promise { } export function registerNodeHandlers() { - ipcMain.handle( - "nodejs-status", - async (): Promise<{ - nodeVersion: string | null; - pnpmVersion: string | null; - }> => { - // Run checks in parallel - const [nodeVersion, pnpmVersion] = await Promise.all([ - checkCommandExists("node"), - checkCommandExists("pnpm"), - ]); - return { nodeVersion, pnpmVersion }; - } - ); - - ipcMain.handle("install-node", async (): Promise => { - console.log("Installing Node.js..."); - if (platform() === "win32") { - let result = await runShell("winget install Volta.Volta"); - if (!result.success) { - return { success: false, errorMessage: result.errorMessage }; - } - } else { - let result = await runShell("curl https://get.volta.sh | bash"); - if (!result.success) { - return { success: false, errorMessage: result.errorMessage }; + ipcMain.handle("nodejs-status", async (): Promise => { + // Run checks in parallel + const [nodeVersion, pnpmVersion] = await Promise.all([ + checkCommandExists("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( + "pnpm --version || corepack enable pnpm && pnpm --version" + ), + ]); + // Default to mac download url. + let nodeDownloadUrl = "https://nodejs.org/dist/v22.14.0/node-v22.14.0.pkg"; + if (platform() == "win32") { + if (arch() === "arm64" || arch() === "arm") { + nodeDownloadUrl = + "https://nodejs.org/dist/v22.14.0/node-v22.14.0-arm64.msi"; + } else { + // x64 is the most common architecture for Windows so it's the + // default download url. + nodeDownloadUrl = + "https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi"; } } - console.log("Installed Volta"); + return { nodeVersion, pnpmVersion, nodeDownloadUrl }; + }); - process.env.PATH = ["~/.volta/bin", process.env.PATH].join(":"); - console.log("Updated PATH"); - let result = await runShell("volta install node"); - if (!result.success) { - return { success: false, errorMessage: result.errorMessage }; - } - console.log("Installed Node.js (via Volta)"); - - result = await runShell("node --version"); - if (!result.success) { - return { success: false, errorMessage: result.errorMessage }; - } - const nodeVersion = result.output.trim(); - console.log("Node.js is setup with version", nodeVersion); - - result = await runShell("corepack enable pnpm"); - if (!result.success) { - return { success: false, errorMessage: result.errorMessage }; - } - console.log("Enabled pnpm"); - - result = await runShell("pnpm --version"); - if (!result.success) { - return { success: false, errorMessage: result.errorMessage }; - } - const pnpmVersion = result.output.trim(); - console.log("pnpm is setup with version", pnpmVersion); - - return { success: true, nodeVersion, pnpmVersion }; + ipcMain.handle("reload-dyad", async (): Promise => { + app.relaunch(); + app.exit(0); }); } diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index ca795d3..0ccb543 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -15,6 +15,7 @@ import type { CreateAppResult, InstallNodeResult, ListAppsResponse, + NodeSystemInfo, SandboxConfig, Version, } from "./ipc_types"; @@ -132,6 +133,10 @@ export class IpcClient { return IpcClient.instance; } + public async reloadDyad(): Promise { + await this.ipcRenderer.invoke("reload-dyad"); + } + // Create a new app with an initial chat public async createApp(params: CreateAppParams): Promise { try { @@ -493,10 +498,7 @@ export class IpcClient { } // Check Node.js and npm status - public async getNodejsStatus(): Promise<{ - nodeVersion: string | null; - pnpmVersion: string | null; - }> { + public async getNodejsStatus(): Promise { try { const result = await this.ipcRenderer.invoke("nodejs-status"); return result; @@ -506,17 +508,6 @@ export class IpcClient { } } - // Install Node.js and npm - public async installNode(): Promise { - try { - const result = await this.ipcRenderer.invoke("install-node"); - return result; - } catch (error) { - showError(error); - throw error; - } - } - // --- GitHub Device Flow --- public startGithubDeviceFlow(appId: number | null): void { this.ipcRenderer.invoke("github:start-flow", { appId }); diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts index 4e85d50..f8670c9 100644 --- a/src/ipc/ipc_types.ts +++ b/src/ipc/ipc_types.ts @@ -66,13 +66,8 @@ export interface SandboxConfig { entry: string; } -export type InstallNodeResult = - | { - success: true; - nodeVersion: string; - pnpmVersion: string; - } - | { - success: false; - errorMessage: string; - }; +export interface NodeSystemInfo { + nodeVersion: string | null; + pnpmVersion: string | null; + nodeDownloadUrl: string; +} diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 0e8a6fc..ad7f4cc 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -84,10 +84,6 @@ export default function HomePage() { // Main Home Page Content return (
-

- Build your dream app -

-
diff --git a/src/preload.ts b/src/preload.ts index 692c6e5..249a37c 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -37,6 +37,7 @@ const validInvokeChannels = [ "github:create-repo", "github:push", "get-app-version", + "reload-dyad", ] as const; // Add valid receive channels