diff --git a/apps/studio/package.json b/apps/studio/package.json index 3f4d456e5..651e68e34 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -56,9 +56,6 @@ "@orpc/client": "catalog:", "@orpc/server": "catalog:", "@orpc/tanstack-query": "catalog:", - "@pnpm/package-bins": "^1000.0.3", - "@pnpm/read-package-json": "^1000.0.4", - "@pnpm/types": "^1000.2.0", "@quests/ai-gateway": "workspace:*", "@quests/components": "workspace:*", "@quests/shared": "workspace:*", @@ -83,6 +80,7 @@ "@tanstack/react-router-devtools": "^1.131.28", "@types/ws": "^8.18.1", "@vscode/ripgrep": "^1.15.13", + "@zkochan/cmd-shim": "^7.0.0", "arctic": "^3.6.0", "better-auth": "catalog:", "class-variance-authority": "^0.7.1", @@ -96,14 +94,11 @@ "electron-log": "^5.4.1", "electron-store": "^10.1.0", "electron-updater": "^6.6.8", - "execa": "^9.6.0", "framer-motion": "11.13.5", "hono": "catalog:", "jotai": "^2.14.0", "lucide-react": "catalog:", "ms": "^2.1.3", - "neverthrow": "^8.1.1", - "normalize-path": "^3.0.0", "pnpm": "^10.13.1", "posthog-js": "^1.258.6", "posthog-node": "^5.6.0", @@ -132,7 +127,6 @@ "@types/color-hash": "^2.0.0", "@types/ms": "^2.1.0", "@types/node": "22.16.0", - "@types/normalize-path": "^3.0.2", "@types/react": "catalog:", "@types/react-dom": "catalog:", "@types/semver": "^7.7.0", diff --git a/apps/studio/src/client/vite-env.d.ts b/apps/studio/src/client/vite-env.d.ts index 0e9ccb487..1843ac7d4 100644 --- a/apps/studio/src/client/vite-env.d.ts +++ b/apps/studio/src/client/vite-env.d.ts @@ -33,6 +33,7 @@ declare namespace NodeJS { ELECTRON_USE_NEW_USER_FOLDER: string | undefined; FORCE_DEV_AUTO_UPDATE: string | undefined; NODE_ENV: string | undefined; + PATH: string | undefined; SIGNTOOL_PATH: string | undefined; WIN_CERT_PATH: string | undefined; WIN_GCP_KMS_KEY_VERSION: string | undefined; diff --git a/apps/studio/src/electron-main/index.ts b/apps/studio/src/electron-main/index.ts index 7f4e88255..11fd61fe4 100644 --- a/apps/studio/src/electron-main/index.ts +++ b/apps/studio/src/electron-main/index.ts @@ -20,6 +20,7 @@ import path from "node:path"; import { createWorkspaceActor } from "./lib/create-workspace-actor"; import { registerTelemetry } from "./lib/register-telemetry"; +import { setupBinDirectory } from "./lib/setup-bin-directory"; import { watchThemePreferenceAndApply } from "./lib/theme-utils"; import { initializeRPC } from "./rpc/initialize"; @@ -114,6 +115,8 @@ void app.whenReady().then(async () => { updateTitleBarOverlay(); }); + await setupBinDirectory(); + appUpdater = new StudioAppUpdater(); appUpdater.pollForUpdates(); diff --git a/apps/studio/src/electron-main/lib/create-workspace-actor.ts b/apps/studio/src/electron-main/lib/create-workspace-actor.ts index 4bf1eea1c..a24b87b2a 100644 --- a/apps/studio/src/electron-main/lib/create-workspace-actor.ts +++ b/apps/studio/src/electron-main/lib/create-workspace-actor.ts @@ -11,28 +11,19 @@ import { workspacePublisher, } from "@quests/workspace/electron"; import { app, shell } from "electron"; -import { execa, parseCommandString } from "execa"; import ms from "ms"; -import { err, ok } from "neverthrow"; import path from "node:path"; import { createActor } from "xstate"; import { getProvidersStore } from "../stores/providers"; import { captureServerEvent } from "./capture-server-event"; import { captureServerException } from "./capture-server-exception"; -import { getFramework } from "./frameworks"; -import { getAllPackageBinaryPaths } from "./link-bins"; -import { getPnpmPath } from "./pnpm"; +import { getPNPMBinPath } from "./setup-bin-directory"; const scopedLogger = logger.scope("workspace-actor"); export function createWorkspaceActor() { const rootDir = path.join(app.getPath("userData"), WORKSPACE_FOLDER); - const execaEnv = { - // Required for normal node processes to work - // See https://www.electronjs.org/docs/latest/api/environment-variables - ELECTRON_RUN_AS_NODE: "1", - }; const actor = createActor(workspaceMachine, { input: { aiGatewayApp, @@ -54,79 +45,17 @@ export function createWorkspaceActor() { return providers; }, + nodeExecEnv: { + // Required to allow Electron to operate as a node process + // See https://www.electronjs.org/docs/latest/api/environment-variables + ELECTRON_RUN_AS_NODE: "1", + }, + pnpmBinPath: getPNPMBinPath(), previewCacheTimeMs: ms("24 hours"), registryDir: app.isPackaged ? path.join(process.resourcesPath, REGISTRY_DIR_NAME) : path.resolve(import.meta.dirname, REGISTRY_DEV_DIR_PATH), rootDir, - runPackageJsonScript: async ({ cwd, script, scriptOptions, signal }) => { - const { framework, frameworkModulePath } = await getFramework({ - rootDir: cwd, - script, - }); - if (!framework) { - return err(new Error(`Unsupported framework: ${script}`)); - } - - try { - return ok( - execa({ - cancelSignal: signal, - cwd, - env: { - ...execaEnv, - ...scriptOptions.env, - }, - node: true, - })`${frameworkModulePath} ${framework.args("dev", scriptOptions.port)}`, - ); - } catch (error) { - return err(error instanceof Error ? error : new Error(String(error))); - } - }, - runShellCommand: async (command, { cwd, signal }) => { - const [commandName, ...rest] = parseCommandString(command); - const pnpmPath = getPnpmPath(); - - if (commandName === "pnpm") { - return ok( - execa({ - cancelSignal: signal, - cwd, - env: execaEnv, - node: true, - })`${pnpmPath} ${rest}`, - ); - } - - if (commandName === "tsc") { - const binaryPaths = await getAllPackageBinaryPaths(cwd); - const binPaths = binaryPaths.get("typescript"); - - if (!binPaths) { - return err(new Error(`tsc not found in ${cwd}`)); - } - - const modulePath = binPaths.find( - (binPath) => path.basename(binPath) === "tsc", - ); - - if (!modulePath) { - return err(new Error(`tsc not found in ${cwd}`)); - } - - return ok( - execa({ - cancelSignal: signal, - cwd, - env: execaEnv, - node: true, - })`${modulePath} ${rest}`, - ); - } - - return err(new Error(`Not implemented: ${command}`)); - }, shimClientDir: app.isPackaged ? path.resolve(process.resourcesPath, "shim-client") : // Uncomment to test built shim diff --git a/apps/studio/src/electron-main/lib/frameworks.ts b/apps/studio/src/electron-main/lib/frameworks.ts deleted file mode 100644 index a1583fe4c..000000000 --- a/apps/studio/src/electron-main/lib/frameworks.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { getAllPackageBinaryPaths } from "@/electron-main/lib/link-bins"; - -const FrameworkErrorCode = { - missingDevScript: "missing_dev_script", - missingPackageJson: "missing_package_json", - missingRootPath: "missing_root_path", - unsupportedFramework: "unsupported_framework", -} as const; - -const Frameworks = { - next: { - args: (subCommand: string, serverPort: number) => [ - subCommand, - "--port", - String(serverPort), - ], - bin: "next", - }, - vite: { - args: (subCommand: string, serverPort: number) => [ - subCommand, - "--port", - String(serverPort), - "--strictPort", - "--clearScreen", - "false", - // Avoids logging confusing localhost and port info - "--logLevel", - "warn", - ], - bin: "vite", - }, -}; - -export const getFramework = async ({ - rootDir, - script, -}: { - rootDir: string; - script: string; -}) => { - const binaryPaths = await getAllPackageBinaryPaths(rootDir); - - const frameworkType = script - ? Object.keys(Frameworks).find((type) => script.includes(type)) - : null; - - const framework = frameworkType - ? Frameworks[frameworkType as keyof typeof Frameworks] - : null; - const [frameworkModulePath] = framework?.bin - ? (binaryPaths.get(framework.bin) ?? []) - : []; - - return framework && frameworkModulePath - ? { - framework, - frameworkModulePath, - } - : { - error: FrameworkErrorCode.unsupportedFramework, - }; -}; diff --git a/apps/studio/src/electron-main/lib/link-bins.ts b/apps/studio/src/electron-main/lib/link-bins.ts deleted file mode 100644 index 4a727d841..000000000 --- a/apps/studio/src/electron-main/lib/link-bins.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { logger } from "@/electron-main/lib/electron-logger"; -import { getBinsFromPackageManifest } from "@pnpm/package-bins"; -import { readPackageJsonFromDir } from "@pnpm/read-package-json"; -import { type DependencyManifest } from "@pnpm/types"; -import { promises as fs } from "node:fs"; -import path from "node:path"; -import normalizePath from "normalize-path"; -import { isEmpty } from "radashi"; - -const scopedLogger = logger.scope("pnpm:bins"); - -/** - * Gets the binary paths for all packages in node_modules that have binaries - * @param projectPath Path to the project root (containing package.json) - * @returns Map of package names to their binary paths - */ -export async function getAllPackageBinaryPaths( - projectPath: string, -): Promise> { - const manifest = await safeReadPkgJson(projectPath); - if (!manifest) { - return new Map(); - } - - const binPaths = new Map(); - const allDeps = { - ...manifest.dependencies, - ...manifest.devDependencies, - }; - - for (const [pkgName] of Object.entries(allDeps)) { - const paths = await getPackageBinaryPaths(projectPath, pkgName); - if (paths.length > 0) { - binPaths.set(pkgName, paths); - } - } - - return binPaths; -} - -/** - * Gets the binary paths for a specific package in node_modules - * @param projectPath Path to the project root (containing package.json) - * @param packageName Name of the package to get binary paths for - * @returns Array of paths to the package's binaries - */ -async function getPackageBinaryPaths( - projectPath: string, - packageName: string, -): Promise { - const pkgPath = path.join(projectPath, "node_modules", packageName); - const bins = await getPackageBins(pkgPath); - return bins.map((bin) => bin.path); -} - -// Original source, which isn't exported: https://github.com/pnpm/pnpm/blob/cd8caece258e154274c2feb4d6dab3293cb8bc21/pkg-manager/link-bins/src/index.ts -// We need essentially the same functionality. -async function getPackageBins(target: string) { - const manifest = await safeReadPkgJson(target); - - if (manifest == null) { - // There's a directory in node_modules without package.json: ${target}. - // This used to be a warning but it didn't really cause any issues. - return []; - } - - if (isEmpty(manifest.bin) && !(await isFromModules(target))) { - scopedLogger.warn( - `Package in ${target} must have a non-empty bin field to get bin linked.`, - "EMPTY_BIN", - ); - return []; - } - - if (typeof manifest.bin === "string" && !manifest.name) { - scopedLogger.warn( - "INVALID_PACKAGE_NAME", - `Package in ${target} must have a name to get bin linked.`, - ); - return []; - } - - return getBinsFromPackageManifest(manifest, target); -} - -async function isFromModules(filename: string): Promise { - const real = await fs.realpath(filename); - return normalizePath(real).includes("/node_modules/"); -} - -async function safeReadPkgJson( - pkgDir: string, -): Promise { - try { - return (await readPackageJsonFromDir(pkgDir)) as DependencyManifest; - } catch (error: unknown) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { - return null; - } - throw error; - } -} diff --git a/apps/studio/src/electron-main/lib/pnpm.ts b/apps/studio/src/electron-main/lib/pnpm.ts index 6d4bc8ea4..89f99f30a 100644 --- a/apps/studio/src/electron-main/lib/pnpm.ts +++ b/apps/studio/src/electron-main/lib/pnpm.ts @@ -2,16 +2,16 @@ import { forkExecCommand } from "@/electron-main/lib/exec-command"; import { createRequire } from "node:module"; import path from "node:path"; -export function getPnpmPath() { +export async function pnpmVersion() { + const pnpmPath = getPnpmPath(); + const result = await forkExecCommand(pnpmPath, ["-v"]); + return result.stdout; +} + +function getPnpmPath() { const require = createRequire(import.meta.url); const packageJsonPath = require.resolve("pnpm"); const unpackedPath = packageJsonPath.replace("app.asar", "app.asar.unpacked"); const pnpmPath = path.dirname(unpackedPath); return path.join(pnpmPath, "bin", "pnpm.cjs"); } - -export async function pnpmVersion() { - const pnpmPath = getPnpmPath(); - const result = await forkExecCommand(pnpmPath, ["-v"]); - return result.stdout; -} diff --git a/apps/studio/src/electron-main/lib/setup-bin-directory.ts b/apps/studio/src/electron-main/lib/setup-bin-directory.ts new file mode 100644 index 000000000..8ec9bc52d --- /dev/null +++ b/apps/studio/src/electron-main/lib/setup-bin-directory.ts @@ -0,0 +1,199 @@ +import cmdShim from "@zkochan/cmd-shim"; +import { app } from "electron"; +import fs from "node:fs/promises"; +import path from "node:path"; + +import { captureServerException } from "./capture-server-exception"; + +const BIN_DIR_NAME = "bin"; + +interface BinaryConfig { + getTargetPath: () => string; + name: string; +} + +export function getPNPMBinPath(): string { + return getNodeModulePath("pnpm", "bin", "pnpm.cjs"); +} + +// Added to PATH so that child processes (the users's apps) can access the binaries +// as if they were installed globally. We don't use these ourselves due to issues +// with orphaned processes on Windows. +export async function setupBinDirectory(): Promise { + const binDir = getBinDirectoryPath(); + + await ensureDirectoryExists(binDir); + await cleanBinDirectory(binDir); + + await setupNodeLink(binDir); + + const binaries = getBinaryConfigs(); + + for (const binary of binaries) { + try { + const targetPath = binary.getTargetPath(); + + try { + await fs.access(targetPath); + } catch { + continue; + } + + await linkDirect(binDir, binary.name, targetPath); + } catch (error) { + captureServerException(error, { + scopes: ["studio"], + }); + } + } + + prependBinDirectoryToPath(binDir); + + return binDir; +} + +async function cleanBinDirectory(binDir: string): Promise { + try { + const entries = await fs.readdir(binDir); + + for (const entry of entries) { + const entryPath = path.join(binDir, entry); + try { + await fs.rm(entryPath, { force: true, recursive: true }); + } catch (error) { + captureServerException(error, { + scopes: ["studio"], + }); + } + } + } catch (error) { + captureServerException(error, { scopes: ["studio"] }); + } +} + +async function createNodeShim( + binDir: string, + nodeExePath: string, +): Promise { + const shimCmdPath = path.join(binDir, "node.cmd"); + + const shimContent = `@ECHO OFF +SETLOCAL +"${nodeExePath}" %* +`; + + await fs.writeFile(shimCmdPath, shimContent, "utf8"); +} + +async function ensureDirectoryExists(dirPath: string): Promise { + try { + await fs.mkdir(dirPath, { recursive: true }); + } catch (error) { + captureServerException(error, { + scopes: ["studio"], + }); + throw error; + } +} + +function getBinaryConfigs(): BinaryConfig[] { + const isWindows = process.platform === "win32"; + + return [ + { + getTargetPath: () => getNodeModulePath("pnpm", "bin", "pnpm.cjs"), + name: "pnpm", + }, + { + getTargetPath: () => { + const basePath = isWindows + ? getNodeModulePath("dugite", "git", "cmd") + : getNodeModulePath("dugite", "git", "bin"); + return isWindows + ? path.join(basePath, "git.exe") + : path.join(basePath, "git"); + }, + name: "git", + }, + { + getTargetPath: () => { + const basePath = getNodeModulePath("@vscode/ripgrep", "bin"); + return isWindows + ? path.join(basePath, "rg.exe") + : path.join(basePath, "rg"); + }, + name: "rg", + }, + ]; +} + +function getBinDirectoryPath(): string { + return path.join(app.getPath("userData"), BIN_DIR_NAME); +} + +function getNodeModulePath(...parts: string[]): string { + const appPath = app.getAppPath(); + const modulePath = path.join(appPath, "node_modules", ...parts); + + if (app.isPackaged && appPath.endsWith(".asar")) { + const unpackedPath = modulePath.replace( + /app\.asar([/\\])/, + "app.asar.unpacked$1", + ); + return unpackedPath; + } + + return modulePath; +} + +async function linkDirect( + binDir: string, + name: string, + targetPath: string, +): Promise { + const isWindows = process.platform === "win32"; + + if (isWindows) { + const outputPath = path.join(binDir, name); + await cmdShim(targetPath, outputPath, { + createCmdFile: true, + createPwshFile: false, + }); + } else { + const linkPath = path.join(binDir, name); + await fs.symlink(targetPath, linkPath); + } +} + +function prependBinDirectoryToPath(binDir: string): void { + const currentPath = process.env.PATH || ""; + const pathSeparator = path.delimiter; + + const pathParts = currentPath.split(pathSeparator).filter(Boolean); + + const binDirIndex = pathParts.indexOf(binDir); + if (binDirIndex !== -1) { + pathParts.splice(binDirIndex, 1); + } + + const newPath = [binDir, ...pathParts].join(pathSeparator); + + process.env.PATH = newPath; +} + +async function setupNodeLink(binDir: string): Promise { + const isWindows = process.platform === "win32"; + const nodeExePath = process.execPath; + const linkPath = path.join(binDir, isWindows ? "node.exe" : "node"); + + try { + await (isWindows + ? createNodeShim(binDir, nodeExePath) + : fs.symlink(nodeExePath, linkPath)); + } catch (error) { + captureServerException(error, { + scopes: ["studio"], + }); + throw error; + } +} diff --git a/apps/studio/tsconfig.json b/apps/studio/tsconfig.json index 4cd8f66e9..6dccfcc35 100644 --- a/apps/studio/tsconfig.json +++ b/apps/studio/tsconfig.json @@ -5,13 +5,5 @@ "@/*": ["./src/*"] } }, - "include": [ - "src", - "*.ts", - "*.js", - "*.cjs", - "scripts", - "__mocks__/*.ts", - "../../packages/ai-gateway/src/lib/add-ref.ts" - ] + "include": ["src", "*.ts", "*.js", "*.cjs", "scripts", "__mocks__/*.ts"] } diff --git a/cspell.json b/cspell.json index dc17c9df9..018bb6c9f 100644 --- a/cspell.json +++ b/cspell.json @@ -25,6 +25,7 @@ "dryrun", "dugite", "dyld", + "errorlevel", "esbenp", "esbuild", "eslintcache", @@ -45,6 +46,8 @@ "kimi", "kmscng", "konsole", + "kwallet", + "libsecret", "listify", "logprobs", "lucide", @@ -57,26 +60,31 @@ "nsis", "nums", "numstat", + "nuxt", "ollama", "oneline", "opencode", "openrouter", "orpc", + "PATHEXT", "posthog", "pwsh", "qwen", + "qwik", "radashi", "recaptcha", "retryable", "rharkor", "rmrf", "serviceworker", + "SETLOCAL", "signtool", "softprops", "sonner", "staticity", "stringbool", "tanstack", + "taskkill", "textnodes", "tipc", "togglefullscreen", @@ -89,7 +97,6 @@ "workerd", "worktree", "XAPI", - "libsecret", - "kwallet" + "zkochan" ] } diff --git a/packages/shared/src/types/telemetry.ts b/packages/shared/src/types/telemetry.ts index 7e3e76cbc..9bc3673f6 100644 --- a/packages/shared/src/types/telemetry.ts +++ b/packages/shared/src/types/telemetry.ts @@ -15,6 +15,9 @@ export interface AnalyticsEvents { "app.sidebar_opened": never; "favorite.added": never; "favorite.removed": never; + "framework.not-supported": { + framework: string; + }; "llm.error": WithModelProperties; "llm.request_finished": WithModelProperties<{ cached_input_tokens?: number | undefined; diff --git a/packages/workspace/package.json b/packages/workspace/package.json index ab3707b4b..1eaf992ca 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@hono/node-server": "^1.18.1", + "@netlify/build-info": "^10.0.9", "@orpc/server": "catalog:", "@quests/ai-gateway": "workspace:*", "@quests/shared": "workspace:*", @@ -48,7 +49,6 @@ "ms": "^2.1.3", "neverthrow": "^8.1.1", "radashi": "^12.6.0", - "read-pkg": "^9.0.1", "superjson": "^2.2.2", "tiny-invariant": "^1.3.3", "typescript": "catalog:", diff --git a/packages/workspace/scripts/run-workspace.ts b/packages/workspace/scripts/run-workspace.ts index 87f8b76a3..136a99d0d 100644 --- a/packages/workspace/scripts/run-workspace.ts +++ b/packages/workspace/scripts/run-workspace.ts @@ -5,8 +5,7 @@ import { type AIGatewayProvider, fetchAISDKModel, } from "@quests/ai-gateway"; -import { execa, parseCommandString } from "execa"; -import { err, ok, type Result } from "neverthrow"; +import { execa } from "execa"; import path from "node:path"; import readline from "node:readline"; import { createActor } from "xstate"; @@ -17,28 +16,6 @@ import { project } from "../src/rpc/routes/project"; import { StoreId } from "../src/schemas/store-id"; import { env } from "./lib/env"; -function scriptToCommand({ - port, - script, -}: { - port: number; - script: string; -}): Result { - const [firstPart, ...rest] = parseCommandString(script); - if (firstPart === "vite") { - return ok([ - "pnpm", - "exec", - firstPart, - ...rest, - "--port", - port.toString(), - "--strictPort", - ]); - } - return err(new Error(`Unknown script: ${script}`)); -} - const registryDir = path.resolve("../../registry"); const actor = createActor(workspaceMachine, { input: { @@ -103,66 +80,13 @@ const actor = createActor(workspaceMachine, { return providers; }, + nodeExecEnv: {}, + pnpmBinPath: await execa({ reject: false })`which pnpm`.then( + (result) => result.stdout.trim() || "pnpm", + ), registryDir, // Sibling directory to monorepo to avoid using same pnpm and git rootDir: path.resolve("../../../workspace.local"), - runPackageJsonScript: ({ cwd, script, scriptOptions, signal }) => { - const command = scriptToCommand({ - port: scriptOptions.port, - script, - }); - if (command.isErr()) { - return Promise.resolve(err(command.error)); - } - try { - let finalCommand = command.value; - // Spawning directly to avoid orphaned processes - if ( - command.value[0] === "pnpm" && - command.value[1] === "exec" && - command.value[2] === "vite" - ) { - finalCommand = ["node_modules/.bin/vite", ...command.value.slice(3)]; - } - return Promise.resolve( - ok( - execa({ - cancelSignal: signal, - cwd, - env: scriptOptions.env, - })`${finalCommand}`, - ), - ); - } catch (error) { - return Promise.resolve( - err(error instanceof Error ? error : new Error(String(error))), - ); - } - }, - runShellCommand: (command, { cwd, signal }) => { - const [commandName, ...rest] = parseCommandString(command); - if (commandName === "pnpm") { - return Promise.resolve( - ok( - execa({ - cancelSignal: signal, - cwd, - })`${commandName} ${rest} --ignore-workspace`, - ), - ); - } - if (commandName === "tsc") { - return Promise.resolve( - ok( - execa({ - cancelSignal: signal, - cwd, - })`pnpm exec tsc ${rest.join(" ")}`, - ), - ); - } - return Promise.resolve(err(new Error(`Not implemented: ${command}`))); - }, // Uncomment to test built shim // shimClientDir: path.resolve("../shim-client/dist"), shimClientDir: "dev-server", diff --git a/packages/workspace/src/lib/execa-node-for-app.ts b/packages/workspace/src/lib/execa-node-for-app.ts new file mode 100644 index 000000000..933429aa8 --- /dev/null +++ b/packages/workspace/src/lib/execa-node-for-app.ts @@ -0,0 +1,19 @@ +import { execa, type Options } from "execa"; + +import { type AppConfig } from "./app-config/types"; + +export function execaNodeForApp( + appConfig: AppConfig, + file: string | URL, + arguments_?: readonly string[], + options?: OptionsType, +) { + return execa(file, arguments_, { + ...options, + env: { + ...options?.env, + ...appConfig.workspaceConfig.nodeExecEnv, + }, + node: true, + } as unknown as OptionsType & { env: Record; node: true }); +} diff --git a/packages/workspace/src/lib/get-framework.ts b/packages/workspace/src/lib/get-framework.ts new file mode 100644 index 000000000..caf786448 --- /dev/null +++ b/packages/workspace/src/lib/get-framework.ts @@ -0,0 +1,122 @@ +import { type Info } from "@netlify/build-info/node"; +import { parseCommandString } from "execa"; +import { err, ok, type Result } from "neverthrow"; +import invariant from "tiny-invariant"; + +import { type AppDir } from "../schemas/paths"; +import { absolutePathJoin } from "./absolute-path-join"; +import { type AppConfig } from "./app-config/types"; +import { TypedError } from "./errors"; +import { readPNPMShim } from "./read-pnpm-shim"; + +export async function getFramework({ + appConfig, + buildInfo: { frameworks }, + port, +}: { + appConfig: AppConfig; + buildInfo: Info; + port: number; +}): Promise< + Result< + { + arguments: string[]; + command: string; + errorMessage?: string; + name: string; + }, + TypedError.FileSystem | TypedError.NotFound | TypedError.Parse + > +> { + const [framework] = frameworks; // First framework is already sorted by accuracy + invariant(framework, "No framework found"); + const [devCommand, ...devCommandArgs] = parseCommandString( + framework.dev?.command ?? "", + ); + if (!devCommand) { + return err( + new TypedError.NotFound( + "No dev command found in framework configuration", + ), + ); + } + + const binPathResult = await readPNPMShim( + getBinShimPath(appConfig.appDir, devCommand), + ); + + if (binPathResult.isErr()) { + return err(binPathResult.error); + } + + const binPath = binPathResult.value; + + switch (devCommand) { + case "astro": { + return ok({ + arguments: [...devCommandArgs, "--port", port.toString()], + command: binPath, + name: framework.name, + }); + } + case "next": { + return ok({ + arguments: [...devCommandArgs, "-p", port.toString()], + command: binPath, + name: framework.name, + }); + } + case "nuxt": { + return ok({ + arguments: [...devCommandArgs, "--port", port.toString()], + command: binPath, + name: framework.name, + }); + } + case "qwik": { + return ok({ + arguments: [ + ...devCommandArgs, + "--mode", + "ssr", + "--port", + port.toString(), + ], + command: binPath, + name: framework.name, + }); + } + case "vite": { + return ok({ + arguments: [ + ...devCommandArgs, + "--port", + port.toString(), + "--strictPort", + "--clearScreen", + "false", + "--logLevel", + "warn", + ], + command: binPath, + name: framework.name, + }); + } + } + appConfig.workspaceConfig.captureEvent("framework.not-supported", { + framework: framework.name, + }); + + // Falling back to generic --port argument and hopefully it works + return ok({ + arguments: [...devCommandArgs, "--port", port.toString()], + command: binPath, + errorMessage: + "Unsupported framework, falling back to generic --port argument", + name: framework.name, + }); +} + +function getBinShimPath(appDir: AppDir, command: string) { + return absolutePathJoin(appDir, "node_modules", ".bin", command); +} diff --git a/packages/workspace/src/lib/get-package-manager.ts b/packages/workspace/src/lib/get-package-manager.ts new file mode 100644 index 000000000..6c69b7218 --- /dev/null +++ b/packages/workspace/src/lib/get-package-manager.ts @@ -0,0 +1,17 @@ +import { type AppConfig } from "./app-config/types"; +import { PackageManager } from "./package-manager"; + +export function getPackageManager({ appConfig }: { appConfig: AppConfig }) { + // For now, we only support PNPM + return { + arguments: + appConfig.type === "version" || appConfig.type === "sandbox" + ? // These app types are nested in the project directory, so we need + // to ignore the workspace config otherwise PNPM may not install the + // dependencies correctly + ["install", "--ignore-workspace"] + : ["install"], + command: appConfig.workspaceConfig.pnpmBinPath, + name: PackageManager.PNPM, + }; +} diff --git a/packages/workspace/src/lib/package-manager.ts b/packages/workspace/src/lib/package-manager.ts new file mode 100644 index 000000000..d92ce43c1 --- /dev/null +++ b/packages/workspace/src/lib/package-manager.ts @@ -0,0 +1,11 @@ +import { type Info } from "@netlify/build-info/node"; + +type PackageManagerName = NonNullable["name"]; + +// Not exported by @netlify/build-info so we recreate it here +export const PackageManager = { + BUN: "bun" as PackageManagerName, + NPM: "npm" as PackageManagerName, + PNPM: "pnpm" as PackageManagerName, + YARN: "yarn" as PackageManagerName, +}; diff --git a/packages/workspace/src/lib/read-pnpm-shim.test.ts b/packages/workspace/src/lib/read-pnpm-shim.test.ts new file mode 100644 index 000000000..caf38928e --- /dev/null +++ b/packages/workspace/src/lib/read-pnpm-shim.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; + +import { readWindowsShim, resolveShimTarget } from "./read-pnpm-shim"; + +const createWindowsShim = (targetPath: string) => { + return `@SETLOCAL +@IF NOT DEFINED NODE_PATH ( + @SET "NODE_PATH=C:\\Users\\tests\\code\\quests\\node_modules\\.pnpm\\pnpm@10.13.1\\node_modules\\pnpm\\bin\\node_modules;C:\\Users\\tests\\code\\quests\\node_modules\\.pnpm\\pnpm@10.13.1\\node_modules\\pnpm\\node_modules;C:\\Users\\tests\\code\\quests\\node_modules\\.pnpm\\pnpm@10.13.1\\node_modules;C:\\Users\\tests\\code\\quests\\node_modules\\.pnpm\\node_modules" +) ELSE ( + @SET "NODE_PATH=C:\\Users\\tests\\code\\quests\\node_modules\\.pnpm\\pnpm@10.13.1\\node_modules\\pnpm\\bin\\node_modules;C:\\Users\\tests\\code\\quests\\node_modules\\.pnpm\\pnpm@10.13.1\\node_modules\\pnpm\\node_modules;C:\\Users\\tests\\code\\quests\\node_modules\\.pnpm\\pnpm@10.13.1\\node_modules;C:\\Users\\tests\\code\\quests\\node_modules\\.pnpm\\node_modules;%NODE_PATH%" +) +@IF EXIST "%~dp0\\node.exe" ( + "%~dp0\\node.exe" "%~dp0\\${targetPath}" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + node "%~dp0\\${targetPath}" %* +)`; +}; + +describe("readWindowsShim", () => { + it("should extract the relative target path from a Windows pnpm shim with .cjs", () => { + const shimContent = createWindowsShim("..\\pnpm\\bin\\pnpm.cjs"); + const result = readWindowsShim(shimContent); + + expect(result).toBe("..\\pnpm\\bin\\pnpm.cjs"); + }); + + it("should extract path with .js extension", () => { + const shimContent = createWindowsShim("..\\pnpm\\bin\\pnpm.js"); + const result = readWindowsShim(shimContent); + + expect(result).toBe("..\\pnpm\\bin\\pnpm.js"); + }); + + it("should extract path with .mjs extension", () => { + const shimContent = createWindowsShim("..\\pnpm\\bin\\pnpm.mjs"); + const result = readWindowsShim(shimContent); + + expect(result).toBe("..\\pnpm\\bin\\pnpm.mjs"); + }); + + it("should extract path when using node.exe explicitly", () => { + const shimContent = createWindowsShim("..\\pnpm\\bin\\pnpm.cjs").replace( + 'node "%~dp0', + 'node.exe "%~dp0', + ); + const result = readWindowsShim(shimContent); + + expect(result).toBe("..\\pnpm\\bin\\pnpm.cjs"); + }); +}); + +describe("resolveShimTarget", () => { + it("should resolve a relative path from the shim file location", () => { + const shimFilePath = "/usr/local/bin/pnpm"; + const relativePath = "../lib/node_modules/pnpm/bin/pnpm.cjs"; + + const result = resolveShimTarget(shimFilePath, relativePath); + + expect(result).toBe("/usr/local/lib/node_modules/pnpm/bin/pnpm.cjs"); + }); +}); diff --git a/packages/workspace/src/lib/read-pnpm-shim.ts b/packages/workspace/src/lib/read-pnpm-shim.ts new file mode 100644 index 000000000..69d43cb4e --- /dev/null +++ b/packages/workspace/src/lib/read-pnpm-shim.ts @@ -0,0 +1,81 @@ +import { err, ok, type Result } from "neverthrow"; +import fs from "node:fs/promises"; +import path from "node:path"; + +import { type AbsolutePath } from "../schemas/paths"; +import { TypedError } from "./errors"; + +const SHEBANG = "#!/bin/sh"; + +export async function readPNPMShim( + shimPath: AbsolutePath, +): Promise> { + const isWindows = process.platform === "win32"; + const shimFilePath = isWindows ? `${shimPath}.cmd` : shimPath; + + try { + const content = await fs.readFile(shimFilePath, "utf8"); + + const relativePath = isWindows + ? readWindowsShim(content) + : readPosixShim(content); + + if (!relativePath) { + return err( + new TypedError.Parse( + `Failed to parse shim file at ${shimFilePath}: could not extract relative path`, + ), + ); + } + + return ok(resolveShimTarget(shimFilePath, relativePath)); + } catch (error) { + return err( + new TypedError.FileSystem(`Failed to read shim file at ${shimFilePath}`, { + cause: error, + }), + ); + } +} + +export function readWindowsShim(content: string): null | string { + const pattern = /node(?:\.exe)?\s+"([^"]+)"/; + + const match = pattern.exec(content); + + if (!match?.[1]) { + return null; + } + + let targetPath = match[1]; + targetPath = targetPath.replaceAll("%~dp0\\", "").replaceAll("%~dp0", ""); + + return targetPath; +} + +export function resolveShimTarget( + shimFilePath: string, + relativePath: string, +): string { + const shimDir = path.dirname(shimFilePath); + return path.resolve(shimDir, relativePath); +} + +function readPosixShim(content: string): null | string { + if (!content.startsWith(SHEBANG)) { + return null; + } + + const execPattern = + /exec\s+(?:node|"\$basedir\/node")\s+"?(\$basedir\/[^"\s]+|[^"\s]+)"?/; + const match = execPattern.exec(content); + + if (!match?.[1]) { + return null; + } + + let targetPath = match[1]; + targetPath = targetPath.replaceAll("$basedir/", ""); + + return targetPath; +} diff --git a/packages/workspace/src/lib/run-diagnostics.ts b/packages/workspace/src/lib/run-diagnostics.ts index 33feaeaf9..50b832a31 100644 --- a/packages/workspace/src/lib/run-diagnostics.ts +++ b/packages/workspace/src/lib/run-diagnostics.ts @@ -3,6 +3,7 @@ import { ExecaError } from "execa"; import type { AppConfig } from "./app-config/types"; import { cancelableTimeout } from "./cancelable-timeout"; +import { runNodeModuleBin } from "./run-pnpm-bin"; export async function runDiagnostics( appConfig: AppConfig, @@ -20,9 +21,11 @@ export async function runDiagnostics( // not just the specific file. This is a limitation of TypeScript CLI. // In the future, this will be replaced with LSP-based diagnostics that // can target specific files efficiently. - const diagnosticsResult = await appConfig.workspaceConfig.runShellCommand( - "tsc --noEmit", - { cwd: appConfig.appDir, signal: diagnosticsSignal }, + const diagnosticsResult = await runNodeModuleBin( + appConfig.appDir, + "tsc", + ["--noEmit"], + { cancelSignal: diagnosticsSignal }, ); timeout.cancel(); @@ -30,7 +33,9 @@ export async function runDiagnostics( if (diagnosticsResult.isOk()) { try { const result = await diagnosticsResult.value; - const output = result.stderr + result.stdout; + const stderr = result.stderr?.toString() ?? ""; + const stdout = result.stdout?.toString() ?? ""; + const output = stderr + stdout; if (output.trim() && result.exitCode !== 0) { return output.trim(); } diff --git a/packages/workspace/src/lib/run-pnpm-bin.ts b/packages/workspace/src/lib/run-pnpm-bin.ts new file mode 100644 index 000000000..351ab662d --- /dev/null +++ b/packages/workspace/src/lib/run-pnpm-bin.ts @@ -0,0 +1,33 @@ +import { execa, type Options } from "execa"; +import { err, ok } from "neverthrow"; + +import { type AppDir } from "../schemas/paths"; +import { absolutePathJoin } from "./absolute-path-join"; +import { TypedError } from "./errors"; +import { readPNPMShim } from "./read-pnpm-shim"; + +export async function runNodeModuleBin( + appDir: AppDir, + bin: string, + args: string[], + options?: Options, +) { + const shimPath = absolutePathJoin(appDir, "node_modules", ".bin", bin); + const binPathResult = await readPNPMShim(shimPath); + + if (binPathResult.isErr()) { + return err(binPathResult.error); + } + + const binPath = binPathResult.value; + + try { + return ok(execa(binPath, args, { cwd: appDir, node: true, ...options })); + } catch (error) { + return err( + new TypedError.NotFound( + `Failed to execute ${bin}: ${error instanceof Error ? error.message : String(error)}`, + ), + ); + } +} diff --git a/packages/workspace/src/logic/server/routes/all-proxy.ts b/packages/workspace/src/logic/server/routes/all-proxy.ts index 64df7ea9c..1feba41ca 100644 --- a/packages/workspace/src/logic/server/routes/all-proxy.ts +++ b/packages/workspace/src/logic/server/routes/all-proxy.ts @@ -62,13 +62,15 @@ app.all("/*", async (c, next) => { return c.html(fallbackPage); } - const url = `http://localhost:${port}${c.req.path}`; + const requestUrl = new URL(c.req.url); + const url = `http://localhost:${port}${requestUrl.pathname}${requestUrl.search}`; const headers = new Headers(c.req.raw.headers); headers.set( "X-Forwarded-For", c.req.header("x-forwarded-for") || c.req.header("x-real-ip") || "127.0.0.1", ); headers.set("X-Forwarded-Host", host); + headers.set("X-Forwarded-Proto", requestUrl.protocol.replace(":", "")); headers.delete("if-none-match"); headers.delete("if-modified-since"); diff --git a/packages/workspace/src/logic/spawn-runtime.ts b/packages/workspace/src/logic/spawn-runtime.ts index 297c8b39b..24ee5b103 100644 --- a/packages/workspace/src/logic/spawn-runtime.ts +++ b/packages/workspace/src/logic/spawn-runtime.ts @@ -1,7 +1,7 @@ +import { getBuildInfo } from "@netlify/build-info/node"; import { envForProviders } from "@quests/ai-gateway"; -import { ExecaError, parseCommandString, type ResultPromise } from "execa"; +import { ExecaError, type ResultPromise } from "execa"; import ms from "ms"; -import { type NormalizedPackageJson, readPackage } from "read-pkg"; import { type ActorRef, type ActorRefFrom, @@ -12,9 +12,11 @@ import { import { type AppConfig } from "../lib/app-config/types"; import { cancelableTimeout, TimeoutError } from "../lib/cancelable-timeout"; +import { execaNodeForApp } from "../lib/execa-node-for-app"; +import { getFramework } from "../lib/get-framework"; +import { getPackageManager } from "../lib/get-package-manager"; import { pathExists } from "../lib/path-exists"; import { PortManager } from "../lib/port-manager"; -import { type RunPackageJsonScript } from "../types"; import { getWorkspaceServerURL } from "./server/url"; const BASE_RUNTIME_TIMEOUT_MS = ms("1 minute"); @@ -65,13 +67,13 @@ async function isLocalServerRunning(port: number) { } function sendProcessLogs( - execaProcess: ResultPromise<{ cancelSignal: AbortSignal; cwd: string }>, + execaProcess: ResultPromise, parentRef: ActorRef, options: { errorOnly?: boolean } = {}, ) { const { stderr, stdout } = execaProcess; - stderr.on("data", (data: Buffer) => { + stderr?.on("data", (data: Buffer) => { const message = data.toString().trim(); if (message) { if ( @@ -90,7 +92,7 @@ function sendProcessLogs( }); if (!options.errorOnly) { - stdout.on("data", (data: Buffer) => { + stdout?.on("data", (data: Buffer) => { const message = data.toString().trim(); if (message) { parentRef.send({ @@ -107,9 +109,8 @@ export const spawnRuntimeLogic = fromCallback< appConfig: AppConfig; attempt: number; parentRef: ActorRef; - runPackageJsonScript: RunPackageJsonScript; } ->(({ input: { appConfig, attempt, parentRef, runPackageJsonScript } }) => { +>(({ input: { appConfig, attempt, parentRef } }) => { const abortController = new AbortController(); const timeout = cancelableTimeout( BASE_RUNTIME_TIMEOUT_MS + attempt * RUNTIME_TIMEOUT_MULTIPLIER_MS, @@ -118,6 +119,22 @@ export const spawnRuntimeLogic = fromCallback< let port: number | undefined; async function main() { + const buildInfo = await getBuildInfo({ projectDir: appConfig.appDir }); + + if (buildInfo.frameworks.length === 0) { + parentRef.send({ + isRetryable: false, + shouldLog: true, + type: "spawnRuntime.error.unknown", + value: { + error: new Error( + "No frameworks detected. Ensure a framework like Next.js, Nuxt.js, or Vite exists in the package.json.", + ), + }, + }); + return; + } + port = await portManager.reservePort(); if (!port) { @@ -130,151 +147,110 @@ export const spawnRuntimeLogic = fromCallback< return; } + if (!(await pathExists(appConfig.appDir))) { + parentRef.send({ + isRetryable: false, + shouldLog: true, + type: "spawnRuntime.error.app-dir-does-not-exist", + value: { + error: new Error(`App directory does not exist: ${appConfig.appDir}`), + }, + }); + return; + } + const installTimeout = cancelableTimeout(INSTALL_TIMEOUT_MS); const installSignal = AbortSignal.any([ abortController.signal, installTimeout.controller.signal, ]); - const installCommand = - appConfig.type === "version" || appConfig.type === "sandbox" - ? // These app types are nested in the project directory, so we need - // to ignore the workspace config otherwise PNPM may not install the - // dependencies correctly - "pnpm install --ignore-workspace" - : "pnpm install"; + const packageManager = getPackageManager({ appConfig }); parentRef.send({ type: "spawnRuntime.log", - value: { message: `$ ${installCommand}`, type: "normal" }, + value: { + message: `Installing dependencies with ${packageManager.name}`, + type: "normal", + }, }); - const installResult = await appConfig.workspaceConfig.runShellCommand( - installCommand, + const installProcess = execaNodeForApp( + appConfig, + packageManager.command, + packageManager.arguments, { + cancelSignal: installSignal, cwd: appConfig.appDir, - signal: installSignal, }, ); + + sendProcessLogs(installProcess, parentRef); + await installProcess; installTimeout.cancel(); - if (installResult.isErr()) { - parentRef.send({ - isRetryable: true, - shouldLog: true, - type: "spawnRuntime.error.install-failed", - value: { - error: new Error(installResult.error.message, { - cause: installResult.error, - }), - }, - }); - return; - } - const installProcessPromise = installResult.value; - sendProcessLogs(installProcessPromise, parentRef); - await installProcessPromise; + const providerEnv = envForProviders({ + providers: appConfig.workspaceConfig.getAIProviders(), + workspaceServerURL: getWorkspaceServerURL(), + }); - const scriptName = "dev"; + const signal = AbortSignal.any([ + abortController.signal, + timeout.controller.signal, + ]); - let pkg: NormalizedPackageJson; - try { - pkg = await readPackage({ cwd: appConfig.appDir }); - } catch (error) { - parentRef.send({ - isRetryable: false, - shouldLog: true, - type: "spawnRuntime.error.package-json", - value: { - error: new Error("Unknown error reading package.json", { - cause: error instanceof Error ? error : new Error(String(error)), - }), - }, - }); - return; - } + const frameworkResult = await getFramework({ + appConfig, + buildInfo, + port, + }); - const script = pkg.scripts?.[scriptName]; - if (!script) { - parentRef.send({ - isRetryable: false, - shouldLog: true, - type: "spawnRuntime.error.package-json", - value: { - error: new Error(`No script \`${scriptName}\` found in package.json`), - }, - }); - return; - } - const [commandName] = parseCommandString(script); - if (commandName !== "vite") { + if (frameworkResult.isErr()) { parentRef.send({ isRetryable: false, shouldLog: true, - type: "spawnRuntime.error.unsupported-script", - value: { - error: new Error( - `Unsupported command \`${commandName ?? "missing"}\` for script \`${scriptName}\` in package.json`, - ), - }, + type: "spawnRuntime.error.unknown", + value: { error: frameworkResult.error }, }); return; } - if (!(await pathExists(appConfig.appDir))) { + const framework = frameworkResult.value; + + if (framework.errorMessage) { parentRef.send({ - isRetryable: false, - shouldLog: true, - type: "spawnRuntime.error.app-dir-does-not-exist", + type: "spawnRuntime.log", value: { - error: new Error(`App directory does not exist: ${appConfig.appDir}`), + message: framework.errorMessage, + type: "error", }, }); - return; } - const providerEnv = envForProviders({ - providers: appConfig.workspaceConfig.getAIProviders(), - workspaceServerURL: getWorkspaceServerURL(), - }); - - const signal = AbortSignal.any([ - abortController.signal, - timeout.controller.signal, - ]); - parentRef.send({ type: "spawnRuntime.log", - value: { message: `$ pnpm run ${script}`, type: "normal" }, + value: { + message: `Starting ${framework.name} dev server`, + type: "normal", + }, }); timeout.start(); - const result = await runPackageJsonScript({ - cwd: appConfig.appDir, - script, - scriptOptions: { + const runtimeProcess = execaNodeForApp( + appConfig, + framework.command, + framework.arguments, + { + cancelSignal: signal, + cwd: appConfig.appDir, env: { ...providerEnv, - NO_COLOR: "1", // Disable color to avoid ANSI escape codes in logs - QUESTS_INSIDE_STUDIO: "true", // Used by apps to detect if they are running inside Studio + NO_COLOR: "1", + PORT: port.toString(), + QUESTS_INSIDE_STUDIO: "true", }, - port, }, - signal, - }); - if (result.isErr()) { - // Happens for immediate errors - parentRef.send({ - isRetryable: true, - shouldLog: false, - type: "spawnRuntime.error.unknown", - value: { - error: result.error, - }, - }); - return; - } - const processPromise = result.value; - sendProcessLogs(processPromise, parentRef); + ); + sendProcessLogs(runtimeProcess, parentRef); let shouldCheckServer = true; const checkServer = async () => { @@ -318,10 +294,10 @@ export const spawnRuntimeLogic = fromCallback< // Must abort check server if the process promise rejects because it // means the server is not running and we could accidentally check // another server and say it's running - processPromise.catch(() => (shouldCheckServer = false)); + runtimeProcess.catch(() => (shouldCheckServer = false)); // Ensures we catch errors from the process promise - await Promise.all([checkServer(), processPromise]); + await Promise.all([checkServer(), runtimeProcess]); } main() diff --git a/packages/workspace/src/machines/execute-tool-call.test.ts b/packages/workspace/src/machines/execute-tool-call.test.ts index d6ecf74c3..355f746c6 100644 --- a/packages/workspace/src/machines/execute-tool-call.test.ts +++ b/packages/workspace/src/machines/execute-tool-call.test.ts @@ -15,48 +15,50 @@ import { executeToolCallMachine } from "./execute-tool-call"; vi.mock(import("ulid")); vi.mock(import("../lib/session-store-storage")); vi.mock(import("../lib/get-current-date")); +vi.mock(import("../lib/execa-node-for-app"), () => ({ + execaNodeForApp: vi.fn(), +})); describe("executeToolCallMachine", () => { const projectAppConfig = createMockAppConfig( ProjectSubdomainSchema.parse("test"), - { - runShellCommand: vi.fn().mockImplementation((command: string) => { + ); + const sessionId = StoreId.newSessionId(); + const messageId = StoreId.newMessageId(); + const mockDate = new Date("2025-01-01T00:00:00.000Z"); + + beforeEach(async () => { + const { execaNodeForApp: execaElectronNode } = await import( + "../lib/execa-node-for-app" + ); + vi.mocked(execaElectronNode).mockImplementation( + async (_appConfig, file, args, _options) => { + const command = [file, ...(args ?? [])].join(" "); + if (command.includes("throw-error")) { throw new Error("Shell command failed"); } if (command.includes("hang-command")) { - return Promise.resolve({ - isErr: () => false, - value: new Promise((resolve) => { - setTimeout(() => { - resolve({ - exitCode: 0, - stderr: "mocked stderr", - stdout: "mocked stdout", - }); - }, 100); - }), + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + exitCode: 0, + stderr: "mocked stderr", + stdout: "mocked stdout", + }); + }, 100); }); } - // Default successful command - return Promise.resolve({ - isErr: () => false, - value: Promise.resolve({ - exitCode: 0, - stderr: "mocked stderr", - stdout: "mocked stdout", - }), - }); - }), - }, - ); - const sessionId = StoreId.newSessionId(); - const messageId = StoreId.newMessageId(); - const mockDate = new Date("2025-01-01T00:00:00.000Z"); + return { + exitCode: 0, + stderr: "mocked stderr", + stdout: "mocked stdout", + }; + }, + ); - beforeEach(async () => { mockFs({ [MOCK_WORKSPACE_DIRS.projects]: { [projectAppConfig.folderName]: { @@ -94,6 +96,7 @@ describe("executeToolCallMachine", () => { afterEach(() => { mockFs.restore(); + vi.clearAllMocks(); }); function createTestActor({ diff --git a/packages/workspace/src/machines/runtime.ts b/packages/workspace/src/machines/runtime.ts index 6369ff94b..d6e1756f5 100644 --- a/packages/workspace/src/machines/runtime.ts +++ b/packages/workspace/src/machines/runtime.ts @@ -19,7 +19,7 @@ import { type SpawnRuntimeRef, } from "../logic/spawn-runtime"; import { publisher } from "../rpc/publisher"; -import { type AppStatus, type RunPackageJsonScript } from "../types"; +import { type AppStatus } from "../types"; const MAX_RETRIES = 3; @@ -123,13 +123,11 @@ export const runtimeMachine = setup({ logs: RuntimeLogEntry[]; port?: number; retryCount: number; - runPackageJsonScript: RunPackageJsonScript; spawnRuntimeRef?: SpawnRuntimeRef; }, events: {} as RuntimeEvent, input: {} as { appConfig: AppConfig; - runPackageJsonScript: RunPackageJsonScript; }, output: {} as { error?: unknown }, tags: {} as Exclude, @@ -141,7 +139,6 @@ export const runtimeMachine = setup({ lastHeartbeat: new Date(), logs: [], retryCount: 0, - runPackageJsonScript: input.runPackageJsonScript, }; }, id: "runtime", @@ -324,7 +321,6 @@ export const runtimeMachine = setup({ appConfig: context.appConfig, attempt: context.retryCount, parentRef: self, - runPackageJsonScript: context.runPackageJsonScript, }, }), })), diff --git a/packages/workspace/src/machines/workspace/index.ts b/packages/workspace/src/machines/workspace/index.ts index ce9bc5d2e..d1fe27bbe 100644 --- a/packages/workspace/src/machines/workspace/index.ts +++ b/packages/workspace/src/machines/workspace/index.ts @@ -42,7 +42,6 @@ import { import { type SessionMessage } from "../../schemas/session/message"; import { type StoreId } from "../../schemas/store-id"; import { type AppSubdomain } from "../../schemas/subdomains"; -import { type RunShellCommand } from "../../tools/types"; import { type WorkspaceConfig } from "../../types"; import { type ToolCallUpdate } from "../agent"; import { runtimeMachine } from "../runtime"; @@ -183,11 +182,11 @@ export const workspaceMachine = setup({ captureEvent: CaptureEventFunction; captureException: CaptureExceptionFunction; getAIProviders: GetAIProviders; + nodeExecEnv: Record; + pnpmBinPath: string; previewCacheTimeMs?: number; registryDir: string; rootDir: string; - runPackageJsonScript: WorkspaceContext["runPackageJsonScript"]; - runShellCommand: RunShellCommand; shimClientDir: string; trashItem: (path: AbsolutePath) => Promise; }, @@ -199,6 +198,8 @@ export const workspaceMachine = setup({ captureEvent: input.captureEvent, captureException: input.captureException, getAIProviders: input.getAIProviders, + nodeExecEnv: input.nodeExecEnv, + pnpmBinPath: AbsolutePathSchema.parse(input.pnpmBinPath), previewCacheTimeMs: input.previewCacheTimeMs, previewsDir: AbsolutePathSchema.parse( path.join(input.rootDir, PREVIEWS_FOLDER), @@ -208,7 +209,6 @@ export const workspaceMachine = setup({ ), registryDir: AbsolutePathSchema.parse(input.registryDir), rootDir: WorkspaceDirSchema.parse(input.rootDir), - runShellCommand: input.runShellCommand, trashItem: input.trashItem, }; return { @@ -216,7 +216,6 @@ export const workspaceMachine = setup({ checkoutVersionRefs: new Map(), config: workspaceConfig, createPreviewRefs: new Map(), - runPackageJsonScript: input.runPackageJsonScript, runtimeRefs: new Map(), sessionRefsBySubdomain: new Map(), workspaceServerRef: spawn("workspaceServerLogic", { @@ -486,7 +485,6 @@ export const workspaceMachine = setup({ spawn("runtimeMachine", { input: { appConfig: event.value.appConfig, - runPackageJsonScript: context.runPackageJsonScript, }, }), ), diff --git a/packages/workspace/src/machines/workspace/types.ts b/packages/workspace/src/machines/workspace/types.ts index 1f5a3df0b..5eba2be1a 100644 --- a/packages/workspace/src/machines/workspace/types.ts +++ b/packages/workspace/src/machines/workspace/types.ts @@ -6,7 +6,7 @@ import { type PreviewSubdomain, type VersionSubdomain, } from "../../schemas/subdomains"; -import { type RunPackageJsonScript, type WorkspaceConfig } from "../../types"; +import { type WorkspaceConfig } from "../../types"; import { type RuntimeActorRef } from "../runtime"; import { type SessionActorRef } from "../session"; @@ -17,7 +17,6 @@ export interface WorkspaceContext { config: WorkspaceConfig; createPreviewRefs: Map; error?: unknown; - runPackageJsonScript: RunPackageJsonScript; runtimeRefs: Map; sessionRefsBySubdomain: Map; workspaceServerRef: WorkspaceServerActorRef; diff --git a/packages/workspace/src/test/helpers/mock-app-config.ts b/packages/workspace/src/test/helpers/mock-app-config.ts index f2078d6ab..692181912 100644 --- a/packages/workspace/src/test/helpers/mock-app-config.ts +++ b/packages/workspace/src/test/helpers/mock-app-config.ts @@ -1,10 +1,6 @@ -import { execa } from "execa"; -import { ok } from "neverthrow"; - import { createAppConfig } from "../../lib/app-config/create"; import { AbsolutePathSchema, WorkspaceDirSchema } from "../../schemas/paths"; import { type AppSubdomain } from "../../schemas/subdomains"; -import { type WorkspaceConfig } from "../../types"; const MOCK_WORKSPACE_DIR = "/tmp/workspace"; @@ -14,14 +10,7 @@ export const MOCK_WORKSPACE_DIRS = { registry: `${MOCK_WORKSPACE_DIR}/registry`, } as const; -export function createMockAppConfig( - subdomain: AppSubdomain, - { - runShellCommand, - }: { - runShellCommand?: WorkspaceConfig["runShellCommand"]; - } = {}, -) { +export function createMockAppConfig(subdomain: AppSubdomain) { return createAppConfig({ subdomain, workspaceConfig: { @@ -33,25 +22,12 @@ export function createMockAppConfig( console.error("captureException", args); }, getAIProviders: () => [], + nodeExecEnv: {}, + pnpmBinPath: AbsolutePathSchema.parse("/tmp/pnpm"), previewsDir: AbsolutePathSchema.parse(MOCK_WORKSPACE_DIRS.previews), projectsDir: AbsolutePathSchema.parse(MOCK_WORKSPACE_DIRS.projects), registryDir: AbsolutePathSchema.parse(MOCK_WORKSPACE_DIRS.registry), rootDir: WorkspaceDirSchema.parse(MOCK_WORKSPACE_DIR), - runShellCommand: - runShellCommand ?? - (( - command: string, - { cwd, signal }: { cwd: string; signal: AbortSignal }, - ) => { - return Promise.resolve( - ok( - execa({ - cancelSignal: signal, - cwd, - })`echo '${command} not mocked'`, - ), - ); - }), trashItem: () => Promise.resolve(), }, }); diff --git a/packages/workspace/src/tools/run-shell-command.ts b/packages/workspace/src/tools/run-shell-command.ts index 8f40c0f5c..bd60689dd 100644 --- a/packages/workspace/src/tools/run-shell-command.ts +++ b/packages/workspace/src/tools/run-shell-command.ts @@ -9,8 +9,10 @@ import { z } from "zod"; import type { AppConfig } from "../lib/app-config/types"; import { absolutePathJoin } from "../lib/absolute-path-join"; +import { execaNodeForApp } from "../lib/execa-node-for-app"; import { fixRelativePath } from "../lib/fix-relative-path"; import { pathExists } from "../lib/path-exists"; +import { readPNPMShim } from "../lib/read-pnpm-shim"; import { BaseInputSchema } from "./base"; import { createTool } from "./create-tool"; @@ -38,6 +40,11 @@ const AVAILABLE_COMMANDS = { example: "rm temp/cache.json or rm -r temp/", isFileOperation: true, }, + tsc: { + description: "TypeScript compiler", + example: "tsc", + isFileOperation: false, + }, } as const; const SHELL_COMMANDS = new Set( @@ -403,23 +410,47 @@ export const RunShellCommand = createTool({ }); } - const safeResult = await appConfig.workspaceConfig.runShellCommand( - input.command, - { cwd: appConfig.appDir, signal }, - ); - if (safeResult.isErr()) { - return err({ - message: safeResult.error.message, - type: "execute-error", + if (commandName === "pnpm") { + const process = await execaNodeForApp( + appConfig, + appConfig.workspaceConfig.pnpmBinPath, + args, + { + cancelSignal: signal, + cwd: appConfig.appDir, + }, + ); + return ok({ + command: input.command, + exitCode: process.exitCode ?? 0, + stderr: process.stderr, + stdout: process.stdout, + }); + } else if (commandName === "tsc") { + // TODO: Allow more bins + const binPath = await readPNPMShim( + absolutePathJoin(appConfig.appDir, "node_modules", ".bin", commandName), + ); + if (binPath.isErr()) { + return err({ + message: binPath.error.message, + type: "execute-error", + }); + } + const process = await execaNodeForApp(appConfig, binPath.value, args, { + cancelSignal: signal, + cwd: appConfig.appDir, + }); + return ok({ + command: input.command, + exitCode: process.exitCode ?? 0, + stderr: process.stderr, + stdout: process.stdout, }); } - const result = await safeResult.value; - - return ok({ - command: input.command, - exitCode: result.exitCode ?? 0, - stderr: result.stderr, - stdout: result.stdout, + return err({ + message: `Invalid command. The available commands are: ${Object.keys(AVAILABLE_COMMANDS).join(", ")}.`, + type: "execute-error", }); }, inputSchema: BaseInputSchema.extend({ diff --git a/packages/workspace/src/tools/types.ts b/packages/workspace/src/tools/types.ts index 1cdbb0a0b..b0687b89b 100644 --- a/packages/workspace/src/tools/types.ts +++ b/packages/workspace/src/tools/types.ts @@ -2,7 +2,6 @@ import type { Tool } from "ai"; import type { Result } from "neverthrow"; import { type LanguageModelV2ToolResultOutput } from "@ai-sdk/provider"; -import { type ResultPromise } from "execa"; import { type z } from "zod"; import type { ToolNameSchema } from "./name"; @@ -34,18 +33,6 @@ export interface AgentTool< // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AnyAgentTool = AgentTool; -export type RunShellCommand = ( - command: string, - options: { - cwd: string; - signal: AbortSignal; - }, -) => ShellResult; - -export type ShellResult = Promise< - Result, Error> ->; - export type ToolName = z.output; type ExecuteResult = Result; diff --git a/packages/workspace/src/types.ts b/packages/workspace/src/types.ts index 46c9d5950..0ff4cf232 100644 --- a/packages/workspace/src/types.ts +++ b/packages/workspace/src/types.ts @@ -6,29 +6,19 @@ import { import { type APP_STATUSES } from "./constants"; import { type AbsolutePath, type WorkspaceDir } from "./schemas/paths"; -import { type RunShellCommand, type ShellResult } from "./tools/types"; export type AppStatus = (typeof APP_STATUSES)[number]; -export type RunPackageJsonScript = (options: { - cwd: string; - script: string; - scriptOptions: { - env: Record; - port: number; - }; - signal: AbortSignal; -}) => Promise | ShellResult; - export interface WorkspaceConfig { captureEvent: CaptureEventFunction; captureException: CaptureExceptionFunction; getAIProviders: GetAIProviders; + nodeExecEnv: Record; + pnpmBinPath: AbsolutePath; previewCacheTimeMs?: number; previewsDir: AbsolutePath; projectsDir: AbsolutePath; registryDir: AbsolutePath; rootDir: WorkspaceDir; - runShellCommand: RunShellCommand; trashItem: (path: AbsolutePath) => Promise; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f7919309..3f4e9c8f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,15 +144,6 @@ importers: '@orpc/tanstack-query': specifier: 'catalog:' version: 1.8.8(@opentelemetry/api@1.9.0)(@orpc/client@1.8.8(@opentelemetry/api@1.9.0))(@tanstack/query-core@5.85.5) - '@pnpm/package-bins': - specifier: ^1000.0.3 - version: 1000.0.7 - '@pnpm/read-package-json': - specifier: ^1000.0.4 - version: 1000.0.8 - '@pnpm/types': - specifier: ^1000.2.0 - version: 1000.5.0 '@quests/ai-gateway': specifier: workspace:* version: link:../../packages/ai-gateway @@ -225,6 +216,9 @@ importers: '@vscode/ripgrep': specifier: ^1.15.13 version: 1.15.13 + '@zkochan/cmd-shim': + specifier: ^7.0.0 + version: 7.0.0 arctic: specifier: ^3.6.0 version: 3.6.0 @@ -264,9 +258,6 @@ importers: electron-updater: specifier: ^6.6.8 version: 6.6.8 - execa: - specifier: ^9.6.0 - version: 9.6.0 framer-motion: specifier: 11.13.5 version: 11.13.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -282,12 +273,6 @@ importers: ms: specifier: ^2.1.3 version: 2.1.3 - neverthrow: - specifier: ^8.1.1 - version: 8.2.0 - normalize-path: - specifier: ^3.0.0 - version: 3.0.0 pnpm: specifier: ^10.13.1 version: 10.13.1 @@ -367,9 +352,6 @@ importers: '@types/node': specifier: 22.16.0 version: 22.16.0 - '@types/normalize-path': - specifier: ^3.0.2 - version: 3.0.2 '@types/react': specifier: 'catalog:' version: 19.1.8 @@ -687,6 +669,9 @@ importers: '@hono/node-server': specifier: ^1.18.1 version: 1.18.1(hono@4.8.5) + '@netlify/build-info': + specifier: ^10.0.9 + version: 10.0.9 '@orpc/server': specifier: 'catalog:' version: 1.8.8(@opentelemetry/api@1.9.0)(crossws@0.3.4)(ws@8.18.3) @@ -738,9 +723,6 @@ importers: radashi: specifier: ^12.6.0 version: 12.6.0 - read-pkg: - specifier: ^9.0.1 - version: 9.0.1 superjson: specifier: ^2.2.2 version: 2.2.2 @@ -1034,6 +1016,24 @@ packages: '@better-fetch/fetch@1.1.18': resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} + '@bugsnag/browser@8.6.0': + resolution: {integrity: sha512-7UGqTGnQqXUQ09gOlWbDTFUSbeLIIrP+hML3kTOq8Zdc8nP/iuOEflXGLV2TxWBWW8xIUPc928caFPr9EcaDuw==} + + '@bugsnag/core@8.6.0': + resolution: {integrity: sha512-94Jo443JegaiKV8z8NXMFdyTGubiUnwppWhq3kG2ldlYKtEvrmIaO5+JA58B6oveySvoRu3cCe2W9ysY7G7mDw==} + + '@bugsnag/cuid@3.2.1': + resolution: {integrity: sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q==} + + '@bugsnag/js@8.6.0': + resolution: {integrity: sha512-U+ofNTTMA2Z6tCrOhK/QhHBhLoQHoalk8Y82WWc7FAcVSoJZYadND/QuXUriNRZpC4YgJ/s/AxPeQ2y+WvMxzw==} + + '@bugsnag/node@8.6.0': + resolution: {integrity: sha512-O91sELo6zBjflVeP3roRC9l68iYaafVs5lz2N0FDkrT08mP2UljtNWpjjoR/0h1so5Ny1OxHgnZ1IrsXhz5SMQ==} + + '@bugsnag/safe-json-stringify@6.1.0': + resolution: {integrity: sha512-ImA35rnM7bGr+J30R979FQ95BhRB4UO1KfJA0J2sVqc8nwnrS9hhE5mkTmQWMs8Vh1Da+hkLKs5jJB4JjNZp4A==} + '@bundled-es-modules/cookie@2.0.1': resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} @@ -1606,6 +1606,9 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@iarna/toml@2.2.5': + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + '@img/sharp-darwin-arm64@0.34.3': resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1878,6 +1881,11 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@netlify/build-info@10.0.9': + resolution: {integrity: sha512-lkcEejs4D0gwDIVtyRpIXXIv4SPZOii9cstGI5eOsMwoMTlZRL/jniZOSeMk2ZS147l9ncD6vtKxaZPnW1MJew==} + engines: {node: '>=18.14.0'} + hasBin: true + '@noble/ciphers@0.6.0': resolution: {integrity: sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==} @@ -2111,26 +2119,6 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@pnpm/constants@1001.1.0': - resolution: {integrity: sha512-xb9dfSGi1qfUKY3r4Zy9JdC9+ZeaDxwfE7HrrGIEsBVY1hvIn6ntbR7A97z3nk44yX7vwbINNf9sizTp0WEtEw==} - engines: {node: '>=18.12'} - - '@pnpm/error@1000.0.2': - resolution: {integrity: sha512-2SfE4FFL73rE1WVIoESbqlj4sLy5nWW4M/RVdHvCRJPjlQHa9MH7m7CVJM204lz6I+eHoB+E7rL3zmpJR5wYnQ==} - engines: {node: '>=18.12'} - - '@pnpm/package-bins@1000.0.7': - resolution: {integrity: sha512-STDyH1zejF7kBotEmqTzBRKygJxtwtS1fuXDTx03Wuz90wVKP94/QbsGG+teZJFlq//Qc1x74tO0x/GX0yY/5Q==} - engines: {node: '>=18.12'} - - '@pnpm/read-package-json@1000.0.8': - resolution: {integrity: sha512-ygpqkFbj6VyuAeBog5GMcD/XniRF/HDHLI1EYU8NIVIxVNCU017RZNABo7XhdW4dJ5FyrYl1UgYU9W4ArQpr7Q==} - engines: {node: '>=18.12'} - - '@pnpm/types@1000.5.0': - resolution: {integrity: sha512-R8yOeAfqTXW+REWa8UZ9KtDibpfybYhCKwvEtAp3HXcm8GHCg0JN4guZUybwh1yV9dYN354Kz9uAvd7VON2/WQ==} - engines: {node: '>=18.12'} - '@poppinss/cliui@6.4.4': resolution: {integrity: sha512-yJfm+3yglxdeH85C+YebxZ1zsTB4pBh+QwCuxJcxV/pVbxagn63uYyxqnQif2sKWi+nkNZxuyemON3WrtGMBCQ==} @@ -3137,9 +3125,6 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/normalize-path@3.0.2': - resolution: {integrity: sha512-DO++toKYPaFn0Z8hQ7Tx+3iT9t77IJo/nDiqTXilgEP+kPNIYdpS9kh3fXuc53ugqwp9pxC1PVjCpV1tQDyqMA==} - '@types/pegjs@0.10.6': resolution: {integrity: sha512-eLYXDbZWXh2uxf+w8sXS8d6KSoXTswfps6fvCUuVAGN8eRpfe7h9eSRydxiSJvo9Bf+GzifsDOr9TMQlmJdmkw==} @@ -3405,6 +3390,10 @@ packages: resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} engines: {node: '>=10.0.0'} + '@zkochan/cmd-shim@7.0.0': + resolution: {integrity: sha512-E5mgrRS8Kk80n19Xxmrx5qO9UG03FyZd8Me5gxYi++VPZsOv8+OsclA+0Fth4KTDCrQ/FkJryNFKJ6/642lo4g==} + engines: {node: '>=18.12'} + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -3652,10 +3641,6 @@ packages: better-call@1.0.15: resolution: {integrity: sha512-u4ZNRB1yBx5j3CltTEbY2ZoFPVcgsuvciAqTEmPvnZpZ483vlZf4LGJ5aVau1yMlrvlyHxOCica3OqXBLhmsUw==} - better-path-resolve@1.0.0: - resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} - engines: {node: '>=4'} - better-sqlite3@11.9.1: resolution: {integrity: sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==} @@ -3719,6 +3704,10 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + byline@5.0.0: + resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} + engines: {node: '>=0.10.0'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -3894,6 +3883,10 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + cmd-extension@1.0.2: + resolution: {integrity: sha512-iWDjmP8kvsMdBmLTHxFaqXikO8EdFRDfim7k6vUHglY/2xJ5jLrPsnQGijdfp4U+sr/BeecG0wKm02dSIAeQ1g==} + engines: {node: '>=10'} + cmdk@1.1.1: resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} peerDependencies: @@ -4453,6 +4446,9 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} @@ -4866,6 +4862,10 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -5151,10 +5151,6 @@ packages: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} - hosted-git-info@6.1.3: - resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - hosted-git-info@7.0.2: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} @@ -5431,10 +5427,6 @@ packages: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} - is-subdir@1.2.0: - resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} - engines: {node: '>=4'} - is-symbol@1.1.1: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} engines: {node: '>= 0.4'} @@ -5486,6 +5478,9 @@ packages: resolution: {integrity: sha512-V3W56Hnztt4Wdh3VUlAMbdNicX/tOM38eChW3a2ixP6KEBJAeehxzYzTD59JrU5NCTgBZwRt9lRWr8D7eMZVYQ==} engines: {node: '>=18'} + iserror@0.0.2: + resolution: {integrity: sha512-oKGGrFVaWwETimP3SiWwjDeY27ovZoyZPHtxblC4hCq9fXxed/jasx+ATWFFjCVSRZng8VTMsN1nDnGo6zMBSw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -5723,10 +5718,6 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - load-json-file@6.2.0: - resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} - engines: {node: '>=8'} - locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -5735,6 +5726,10 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lodash.castarray@4.4.0: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} @@ -6229,10 +6224,6 @@ packages: normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} - normalize-package-data@5.0.0: - resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - normalize-package-data@6.0.2: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} engines: {node: ^16.14.0 || >=18.0.0} @@ -6334,6 +6325,10 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -6342,6 +6337,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} @@ -6391,6 +6390,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -7122,9 +7125,15 @@ packages: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} + stack-generator@2.0.10: + resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} @@ -7198,10 +7207,6 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - strip-final-newline@4.0.0: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} @@ -8012,6 +8017,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + yoctocolors-cjs@2.1.3: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} @@ -8330,6 +8339,36 @@ snapshots: '@better-fetch/fetch@1.1.18': {} + '@bugsnag/browser@8.6.0': + dependencies: + '@bugsnag/core': 8.6.0 + + '@bugsnag/core@8.6.0': + dependencies: + '@bugsnag/cuid': 3.2.1 + '@bugsnag/safe-json-stringify': 6.1.0 + error-stack-parser: 2.1.4 + iserror: 0.0.2 + stack-generator: 2.0.10 + + '@bugsnag/cuid@3.2.1': {} + + '@bugsnag/js@8.6.0': + dependencies: + '@bugsnag/browser': 8.6.0 + '@bugsnag/node': 8.6.0 + + '@bugsnag/node@8.6.0': + dependencies: + '@bugsnag/core': 8.6.0 + byline: 5.0.0 + error-stack-parser: 2.1.4 + iserror: 0.0.2 + pump: 3.0.3 + stack-generator: 2.0.10 + + '@bugsnag/safe-json-stringify@6.1.0': {} + '@bundled-es-modules/cookie@2.0.1': dependencies: cookie: 0.7.2 @@ -8899,6 +8938,8 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@iarna/toml@2.2.5': {} + '@img/sharp-darwin-arm64@0.34.3': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.0 @@ -9200,6 +9241,18 @@ snapshots: '@neon-rs/load@0.0.4': optional: true + '@netlify/build-info@10.0.9': + dependencies: + '@bugsnag/js': 8.6.0 + '@iarna/toml': 2.2.5 + dot-prop: 9.0.0 + find-up: 7.0.0 + minimatch: 9.0.5 + read-pkg: 9.0.1 + semver: 7.7.2 + yaml: 2.8.0 + yargs: 17.7.2 + '@noble/ciphers@0.6.0': {} '@noble/hashes@1.8.0': {} @@ -9451,27 +9504,6 @@ snapshots: '@pkgr/core@0.2.9': {} - '@pnpm/constants@1001.1.0': {} - - '@pnpm/error@1000.0.2': - dependencies: - '@pnpm/constants': 1001.1.0 - - '@pnpm/package-bins@1000.0.7': - dependencies: - '@pnpm/types': 1000.5.0 - is-subdir: 1.2.0 - tinyglobby: 0.2.14 - - '@pnpm/read-package-json@1000.0.8': - dependencies: - '@pnpm/error': 1000.0.2 - '@pnpm/types': 1000.5.0 - load-json-file: 6.2.0 - normalize-package-data: 5.0.0 - - '@pnpm/types@1000.5.0': {} - '@poppinss/cliui@6.4.4': dependencies: '@poppinss/colors': 4.1.5 @@ -10474,8 +10506,6 @@ snapshots: '@types/normalize-package-data@2.4.4': {} - '@types/normalize-path@3.0.2': {} - '@types/pegjs@0.10.6': {} '@types/pg@8.6.1': @@ -10787,6 +10817,12 @@ snapshots: '@xmldom/xmldom@0.8.10': {} + '@zkochan/cmd-shim@7.0.0': + dependencies: + cmd-extension: 1.0.2 + graceful-fs: 4.2.11 + is-windows: 1.0.2 + abbrev@1.1.1: {} acorn-jsx@5.3.2(acorn@8.15.0): @@ -11141,10 +11177,6 @@ snapshots: set-cookie-parser: 2.7.1 uncrypto: 0.1.3 - better-path-resolve@1.0.0: - dependencies: - is-windows: 1.0.2 - better-sqlite3@11.9.1: dependencies: bindings: 1.5.0 @@ -11258,6 +11290,8 @@ snapshots: builtin-modules@3.3.0: {} + byline@5.0.0: {} + cac@6.7.14: {} cacache@16.1.3: @@ -11446,6 +11480,8 @@ snapshots: cluster-key-slot@1.1.2: optional: true + cmd-extension@1.0.2: {} + cmdk@1.1.1(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) @@ -11997,6 +12033,10 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 @@ -12610,6 +12650,12 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -12981,10 +13027,6 @@ snapshots: dependencies: lru-cache: 6.0.0 - hosted-git-info@6.1.3: - dependencies: - lru-cache: 7.18.3 - hosted-git-info@7.0.2: dependencies: lru-cache: 10.4.3 @@ -13257,10 +13299,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-subdir@1.2.0: - dependencies: - better-path-resolve: 1.0.0 - is-symbol@1.1.1: dependencies: call-bound: 1.0.4 @@ -13298,6 +13336,8 @@ snapshots: isbot@5.1.27: {} + iserror@0.0.2: {} + isexe@2.0.0: {} isexe@3.1.1: {} @@ -13512,13 +13552,6 @@ snapshots: dependencies: uc.micro: 2.1.0 - load-json-file@6.2.0: - dependencies: - graceful-fs: 4.2.11 - parse-json: 5.2.0 - strip-bom: 4.0.0 - type-fest: 0.6.0 - locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -13527,6 +13560,10 @@ snapshots: dependencies: p-locate: 5.0.0 + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + lodash.castarray@4.4.0: {} lodash.defaults@4.2.0: @@ -14292,13 +14329,6 @@ snapshots: semver: 5.7.2 validate-npm-package-license: 3.0.4 - normalize-package-data@5.0.0: - dependencies: - hosted-git-info: 6.1.3 - is-core-module: 2.16.1 - semver: 7.7.2 - validate-npm-package-license: 3.0.4 - normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 @@ -14447,6 +14477,10 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -14455,6 +14489,10 @@ snapshots: dependencies: p-limit: 3.1.0 + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + p-map@4.0.0: dependencies: aggregate-error: 3.1.0 @@ -14515,6 +14553,8 @@ snapshots: path-exists@4.0.0: {} + path-exists@5.0.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -15340,8 +15380,14 @@ snapshots: stable-hash-x@0.2.0: {} + stack-generator@2.0.10: + dependencies: + stackframe: 1.3.4 + stackback@0.0.2: {} + stackframe@1.3.4: {} + standard-as-callback@2.1.0: optional: true @@ -15448,8 +15494,6 @@ snapshots: strip-bom@3.0.0: {} - strip-bom@4.0.0: {} - strip-final-newline@4.0.0: {} strip-indent@3.0.0: @@ -16367,6 +16411,8 @@ snapshots: yocto-queue@0.1.0: {} + yocto-queue@1.2.1: {} + yoctocolors-cjs@2.1.3: optional: true diff --git a/turbo.json b/turbo.json index eaaa7b700..cd249536e 100644 --- a/turbo.json +++ b/turbo.json @@ -13,6 +13,7 @@ "ELECTRON_USE_NEW_USER_FOLDER", "FORCE_DEV_AUTO_UPDATE", "NODE_ENV", + "PATH", "SIGNTOOL_PATH", "WIN_CERT_PATH", "WIN_GCP_KMS_KEY_VERSION",