diff --git a/apps/studio/src/client/components/studio-sidebar.tsx b/apps/studio/src/client/components/studio-sidebar.tsx index 56010aff..b1861875 100644 --- a/apps/studio/src/client/components/studio-sidebar.tsx +++ b/apps/studio/src/client/components/studio-sidebar.tsx @@ -38,6 +38,17 @@ const data = { export function StudioSidebar({ ...props }: React.ComponentProps) { + React.useEffect(() => { + // Installs the basic template to ensure it's available for new projects + void vanillaRpcClient.workspace.registry.template + .installDependencies({ + templateName: "basic", + }) + .catch((error: unknown) => { + logger.error("Error installing template", { error }); + }); + }, []); + const [userResult] = useAtom(userAtom); const { data: favorites } = useQuery( diff --git a/packages/workspace/src/constants.ts b/packages/workspace/src/constants.ts index a3ae11c1..79ce914d 100644 --- a/packages/workspace/src/constants.ts +++ b/packages/workspace/src/constants.ts @@ -23,3 +23,4 @@ export const APP_STATUSES = [ export const GIT_AUTHOR = { email: "agent@quests.dev", name: "Quests Agent" }; export const WEBSITE_URL = "https://quests.dev"; export const APP_NAME = "Quests"; +export const INSTALL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes diff --git a/packages/workspace/src/logic/spawn-runtime.ts b/packages/workspace/src/logic/spawn-runtime.ts index d62fc97b..9384f333 100644 --- a/packages/workspace/src/logic/spawn-runtime.ts +++ b/packages/workspace/src/logic/spawn-runtime.ts @@ -9,6 +9,7 @@ import { fromCallback, } from "xstate"; +import { INSTALL_TIMEOUT_MS } from "../constants"; import { type AppConfig } from "../lib/app-config/types"; import { cancelableTimeout, TimeoutError } from "../lib/cancelable-timeout"; import { pathExists } from "../lib/path-exists"; @@ -18,7 +19,6 @@ import { getWorkspaceServerURL } from "./server/url"; const BASE_RUNTIME_TIMEOUT_MS = 60 * 1000; // 1 minute const RUNTIME_TIMEOUT_MULTIPLIER_MS = 30 * 1000; // 30 seconds -const INSTALL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes const portManager = new PortManager({ basePort: 9200, diff --git a/packages/workspace/src/rpc/routes/registry.ts b/packages/workspace/src/rpc/routes/registry.ts index 6b249e0c..15ed9b57 100644 --- a/packages/workspace/src/rpc/routes/registry.ts +++ b/packages/workspace/src/rpc/routes/registry.ts @@ -1,6 +1,14 @@ import fs from "node:fs/promises"; import { z } from "zod"; +import { + DEFAULT_TEMPLATE_NAME, + INSTALL_TIMEOUT_MS, + REGISTRY_TEMPLATES_FOLDER, +} from "../../constants"; +import { absolutePathJoin } from "../../lib/absolute-path-join"; +import { templateExists } from "../../lib/app-dir-utils"; +import { cancelableTimeout } from "../../lib/cancelable-timeout"; import { getRegistryAppDetails, RegistryAppDetailsSchema, @@ -56,10 +64,89 @@ const screenshot = base } }); +let HAS_INSTALLED_DEFAULT_TEMPLATE = false; + +const installDependencies = base + .input(z.object({ templateName: z.literal(DEFAULT_TEMPLATE_NAME) })) + .handler(async ({ context, errors, input, signal }) => { + const { templateName } = input; + + if (HAS_INSTALLED_DEFAULT_TEMPLATE) { + return { + message: `Dependencies already installed for template ${templateName}`, + success: true, + }; + } + + // Check if template exists + const exists = await templateExists({ + folderName: templateName, + workspaceConfig: context.workspaceConfig, + }); + + if (!exists) { + throw errors.NOT_FOUND({ + message: `Template ${templateName} not found`, + }); + } + + const templateDir = absolutePathJoin( + context.workspaceConfig.registryDir, + REGISTRY_TEMPLATES_FOLDER, + templateName, + ); + + try { + const installTimeout = cancelableTimeout(INSTALL_TIMEOUT_MS); + installTimeout.start(); + const combinedSignal = signal + ? AbortSignal.any([signal, installTimeout.controller.signal]) + : installTimeout.controller.signal; + + const result = await context.workspaceConfig.runShellCommand( + "pnpm install", + { + cwd: templateDir, + signal: combinedSignal, + }, + ); + + installTimeout.cancel(); + + if (result.isErr()) { + // Log error but don't expose to UI + context.workspaceConfig.captureException(result.error); + return; + } + + const processResult = await result.value; + + if (processResult.exitCode !== 0) { + const error = new Error( + `pnpm install failed with exit code ${processResult.exitCode ?? "unknown"}: ${processResult.stderr}`, + ); + context.workspaceConfig.captureException(error); + return; + } + + HAS_INSTALLED_DEFAULT_TEMPLATE = true; + return; + } catch (error) { + // Log error but don't expose to UI + context.workspaceConfig.captureException( + error instanceof Error ? error : new Error(String(error)), + ); + return; + } + }); + export const registry = { app: { byFolderName, list, screenshot, }, + template: { + installDependencies, + }, };