From 64a3cc888c0c58028d63658d5cf5e1a8787182ac Mon Sep 17 00:00:00 2001 From: Harshith Mullapudi Date: Tue, 22 Jul 2025 10:17:19 +0530 Subject: [PATCH] Fix: cli working and mcp proxy --- apps/webapp/app/components/logo/logo.tsx | 5 - apps/webapp/app/entry.server.tsx | 28 ++- apps/webapp/app/root.tsx | 5 - apps/webapp/app/routes/api.v1.mcp.$slug.tsx | 40 +++- .../app/services/integrationAccount.server.ts | 3 + apps/webapp/app/services/oauth2.server.ts | 1 - apps/webapp/app/trigger/utils/mcp.ts | 192 ++++++++++++++++++ apps/webapp/app/utils/startup.ts | 34 ++++ apps/webapp/server.mjs | 1 + apps/webapp/tsconfig.json | 3 +- integrations/slack/spec.json | 6 +- packages/core-cli/package.json | 2 +- packages/core-cli/src/commands/init.ts | 87 +------- packages/core-cli/src/utils/docker-login.ts | 63 ++++++ packages/core-cli/src/utils/env-docker.ts | 1 - packages/core-cli/src/utils/trigger-deploy.ts | 66 ++++++ turbo.json | 1 + 17 files changed, 420 insertions(+), 118 deletions(-) create mode 100644 apps/webapp/app/utils/startup.ts create mode 100644 packages/core-cli/src/utils/docker-login.ts create mode 100644 packages/core-cli/src/utils/trigger-deploy.ts diff --git a/apps/webapp/app/components/logo/logo.tsx b/apps/webapp/app/components/logo/logo.tsx index ecf4dd6..955489e 100644 --- a/apps/webapp/app/components/logo/logo.tsx +++ b/apps/webapp/app/components/logo/logo.tsx @@ -1,14 +1,9 @@ -import React from "react"; -import { Theme, useTheme } from "remix-themes"; - export interface LogoProps { width: number; height: number; } export default function StaticLogo({ width, height }: LogoProps) { - const [theme] = useTheme(); - return ( { let shellRendered = false; @@ -64,7 +72,7 @@ function handleBotRequest( new Response(stream, { headers: responseHeaders, status: responseStatusCode, - }) + }), ); pipe(body); @@ -81,7 +89,7 @@ function handleBotRequest( console.error(error); } }, - } + }, ); setTimeout(abort, ABORT_DELAY); @@ -92,7 +100,7 @@ function handleBrowserRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, - remixContext: EntryContext + remixContext: EntryContext, ) { return new Promise((resolve, reject) => { let shellRendered = false; @@ -114,7 +122,7 @@ function handleBrowserRequest( new Response(stream, { headers: responseHeaders, status: responseStatusCode, - }) + }), ); pipe(body); @@ -131,7 +139,7 @@ function handleBrowserRequest( console.error(error); } }, - } + }, ); setTimeout(abort, ABORT_DELAY); diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx index 8028f39..d86b699 100644 --- a/apps/webapp/app/root.tsx +++ b/apps/webapp/app/root.tsx @@ -4,7 +4,6 @@ import { Outlet, Scripts, ScrollRestoration, - useLoaderData, } from "@remix-run/react"; import type { LinksFunction, @@ -41,7 +40,6 @@ import { useTheme, } from "remix-themes"; import clsx from "clsx"; -import { initNeo4jSchemaOnce } from "./lib/neo4j.server"; export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }]; @@ -50,8 +48,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const toastMessage = session.get("toastMessage") as ToastMessage; const { getTheme } = await themeSessionResolver(request); - await initNeo4jSchemaOnce(); - const posthogProjectKey = env.POSTHOG_PROJECT_KEY; return typedjson( @@ -138,7 +134,6 @@ function App() { // `specifiedTheme` is the stored theme in the session storage. // `themeAction` is the action name that's used to change the theme in the session storage. export default function AppWithProviders() { - const data = useLoaderData(); return ( diff --git a/apps/webapp/app/routes/api.v1.mcp.$slug.tsx b/apps/webapp/app/routes/api.v1.mcp.$slug.tsx index 4b87dc9..43831d1 100644 --- a/apps/webapp/app/routes/api.v1.mcp.$slug.tsx +++ b/apps/webapp/app/routes/api.v1.mcp.$slug.tsx @@ -6,6 +6,7 @@ import { z } from "zod"; import { getIntegrationAccount } from "~/services/integrationAccount.server"; import { createMCPStdioProxy } from "@core/mcp-proxy"; import { randomUUID } from "node:crypto"; +import { configureStdioMCPEnvironment } from "~/trigger/utils/mcp"; export const integrationSlugSchema = z.object({ slug: z.string(), @@ -64,13 +65,13 @@ const { action, loader } = createActionApiRoute( const { url, type } = spec.mcp; - if (type === "http") { - // Find the integration account for this user and integration - const integrationAccount = await getIntegrationAccount( - integrationDefinition.id, - authentication.userId, - ); + // Find the integration account for this user and integration + const integrationAccount = await getIntegrationAccount( + integrationDefinition.id, + authentication.userId, + ); + if (type === "http") { const integrationConfig = integrationAccount?.integrationConfiguration as any; @@ -97,7 +98,23 @@ const { action, loader } = createActionApiRoute( integrationConfig.mcp.tokens.access_token, ); } else { - const { command } = spec.mcp; + if (!integrationAccount) { + return new Response( + JSON.stringify({ + error: "No integration account found", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + // Configure environment variables using the utility function + const { env, args } = configureStdioMCPEnvironment( + spec, + integrationAccount, + ); // Get session_id from headers (case-insensitive), or generate a new uuid if not present const sessionId = @@ -105,10 +122,11 @@ const { action, loader } = createActionApiRoute( request.headers.get("Mcp-Session-Id") || randomUUID(); - return createMCPStdioProxy(request, "npx", ["-y", "hevy-mcp"], { - env: { - HEVY_API_KEY: "e1fa3a63-c7c2-4335-9753-042bd9028330", - }, + // Use the saved local file instead of command + const executablePath = `./integrations/${slug}/main`; + + return createMCPStdioProxy(request, executablePath, args, { + env, sessionId, }); } diff --git a/apps/webapp/app/services/integrationAccount.server.ts b/apps/webapp/app/services/integrationAccount.server.ts index d07e1b8..fb1d3ec 100644 --- a/apps/webapp/app/services/integrationAccount.server.ts +++ b/apps/webapp/app/services/integrationAccount.server.ts @@ -10,6 +10,9 @@ export const getIntegrationAccount = async ( integratedById: userId, isActive: true, }, + include: { + integrationDefinition: true, + }, }); }; diff --git a/apps/webapp/app/services/oauth2.server.ts b/apps/webapp/app/services/oauth2.server.ts index b271ee0..08f345e 100644 --- a/apps/webapp/app/services/oauth2.server.ts +++ b/apps/webapp/app/services/oauth2.server.ts @@ -83,7 +83,6 @@ export class OAuth2Service { // Validate redirect URI validateRedirectUri(client: any, redirectUri: string): boolean { - console.log(redirectUri); const allowedUris = client.redirectUris .split(",") .map((uri: string) => uri.trim()); diff --git a/apps/webapp/app/trigger/utils/mcp.ts b/apps/webapp/app/trigger/utils/mcp.ts index de0441c..a51db18 100644 --- a/apps/webapp/app/trigger/utils/mcp.ts +++ b/apps/webapp/app/trigger/utils/mcp.ts @@ -1,9 +1,69 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { logger } from "@trigger.dev/sdk/v3"; import { jsonSchema, tool, type ToolSet } from "ai"; +import * as fs from "fs"; +import * as path from "path"; import { type MCPTool } from "./types"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { prisma } from "~/db.server"; + +export const configureStdioMCPEnvironment = ( + spec: any, + account: any, +): { env: Record; args: any[] } => { + if (!spec.mcp) { + return { env: {}, args: [] }; + } + + const mcpSpec = spec.mcp; + const configuredMCP = { ...mcpSpec }; + + // Replace config placeholders in environment variables + if (configuredMCP.env) { + for (const [key, value] of Object.entries(configuredMCP.env)) { + if (typeof value === "string" && value.includes("${config:")) { + // Extract the config key from the placeholder + const configKey = value.match(/\$\{config:(.*?)\}/)?.[1]; + if ( + configKey && + account.integrationConfiguration && + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (account.integrationConfiguration as any)[configKey] + ) { + configuredMCP.env[key] = value.replace( + `\${config:${configKey}}`, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (account.integrationConfiguration as any)[configKey], + ); + } + } + + if (typeof value === "string" && value.includes("${integrationConfig:")) { + // Extract the config key from the placeholder + const configKey = value.match(/\$\{integrationConfig:(.*?)\}/)?.[1]; + if ( + configKey && + account.integrationDefinition.config && + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (account.integrationDefinition.config as any)[configKey] + ) { + configuredMCP.env[key] = value.replace( + `\${integrationConfig:${configKey}}`, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (account.integrationDefinition.config as any)[configKey], + ); + } + } + } + } + + return { + env: configuredMCP.env || {}, + args: Array.isArray(configuredMCP.args) ? configuredMCP.args : [], + }; +}; + export class MCP { private Client: any; private clients: Record = {}; @@ -133,3 +193,135 @@ export class MCP { } } } + +export const getIntegrationStdioFile = async ( + integrationDefinitionSlug: string, +) => { + // If the file is in public/integrations/[slug]/main, it is served at /integrations/[slug]/main + return `/integrations/${integrationDefinitionSlug}/main`; +}; + +export const fetchAndSaveStdioIntegrations = async () => { + try { + logger.info("Starting stdio integrations fetch and save process"); + + // Get all integration definitions + const integrationDefinitions = + await prisma.integrationDefinitionV2.findMany({ + where: { + deleted: null, // Only active integrations + }, + }); + + logger.info( + `Found ${integrationDefinitions.length} integration definitions`, + ); + + for (const integration of integrationDefinitions) { + try { + const spec = integration.spec as any; + + // Check if this integration has MCP config and is stdio type + if (spec?.mcp?.type === "stdio" && spec?.mcp?.url) { + logger.info(`Processing stdio integration: ${integration.slug}`); + + const integrationDir = path.join( + process.cwd(), + "integrations", + integration.slug, + ); + const targetFile = path.join(integrationDir, "main"); + + // Create directory if it doesn't exist + if (!fs.existsSync(integrationDir)) { + fs.mkdirSync(integrationDir, { recursive: true }); + logger.info(`Created directory: ${integrationDir}`); + } + + // Skip if file already exists + if (fs.existsSync(targetFile)) { + logger.info( + `Integration ${integration.slug} already exists, skipping`, + ); + continue; + } + + const urlOrPath = spec.mcp.url; + + // If urlOrPath looks like a URL, use fetch, otherwise treat as local path + let isUrl = false; + try { + // Try to parse as URL + const parsed = new URL(urlOrPath); + isUrl = ["http:", "https:"].includes(parsed.protocol); + } catch { + isUrl = false; + } + + if (isUrl) { + // Fetch the URL content + logger.info(`Fetching content from URL: ${urlOrPath}`); + const response = await fetch(urlOrPath); + + if (!response.ok) { + logger.error( + `Failed to fetch ${urlOrPath}: ${response.status} ${response.statusText}`, + ); + continue; + } + + const content = await response.text(); + + // Save the content to the target file + fs.writeFileSync(targetFile, content); + + // Make the file executable if it's a script + if (process.platform !== "win32") { + fs.chmodSync(targetFile, "755"); + } + + logger.info( + `Successfully saved stdio integration: ${integration.slug} to ${targetFile}`, + ); + } else { + // Treat as local file path + const sourcePath = path.isAbsolute(urlOrPath) + ? urlOrPath + : path.join(process.cwd(), urlOrPath); + + logger.info(`Copying content from local path: ${sourcePath}`); + + if (!fs.existsSync(sourcePath)) { + logger.error(`Source file does not exist: ${sourcePath}`); + continue; + } + + fs.copyFileSync(sourcePath, targetFile); + + // Make the file executable if it's a script + if (process.platform !== "win32") { + fs.chmodSync(targetFile, "755"); + } + + logger.info( + `Successfully copied stdio integration: ${integration.slug} to ${targetFile}`, + ); + } + } else { + logger.debug( + `Skipping integration ${integration.slug}: not a stdio type or missing URL`, + ); + } + } catch (error) { + logger.error(`Error processing integration ${integration.slug}:`, { + error, + }); + } + } + + logger.info("Completed stdio integrations fetch and save process"); + } catch (error) { + logger.error("Failed to fetch and save stdio integrations:", { error }); + throw error; + } +}; diff --git a/apps/webapp/app/utils/startup.ts b/apps/webapp/app/utils/startup.ts new file mode 100644 index 0000000..8940363 --- /dev/null +++ b/apps/webapp/app/utils/startup.ts @@ -0,0 +1,34 @@ +import { logger } from "~/services/logger.service"; +import { fetchAndSaveStdioIntegrations } from "~/trigger/utils/mcp"; +import { initNeo4jSchemaOnce } from "~/lib/neo4j.server"; + +// Global flag to ensure startup only runs once per server process +let startupInitialized = false; + +/** + * Initialize all startup services once per server process + * Safe to call multiple times - will only run initialization once + */ +export async function initializeStartupServices() { + if (startupInitialized) { + return; + } + + try { + logger.info("Starting application initialization..."); + + // Initialize Neo4j schema + await initNeo4jSchemaOnce(); + logger.info("Neo4j schema initialization completed"); + + await fetchAndSaveStdioIntegrations(); + logger.info("Stdio integrations initialization completed"); + + startupInitialized = true; + logger.info("Application initialization completed successfully"); + } catch (error) { + logger.error("Failed to initialize startup services:", { error }); + // Don't mark as initialized if there was an error, allow retry + throw error; + } +} diff --git a/apps/webapp/server.mjs b/apps/webapp/server.mjs index 5ec7342..2a62d17 100644 --- a/apps/webapp/server.mjs +++ b/apps/webapp/server.mjs @@ -47,6 +47,7 @@ async function init() { // handle SSR requests app.all("*", remixHandler); + const port = process.env.REMIX_APP_PORT || 3000; app.listen(port, () => console.log(`Express server listening at http://localhost:${port}`), diff --git a/apps/webapp/tsconfig.json b/apps/webapp/tsconfig.json index 8adba20..630a90c 100644 --- a/apps/webapp/tsconfig.json +++ b/apps/webapp/tsconfig.json @@ -7,7 +7,8 @@ "**/*.tsx", "tailwind.config.js", "tailwind.config.js", - "trigger.config.ts" + "trigger.config.ts", + "server.mjs" ], "compilerOptions": { "types": ["@remix-run/node", "vite/client"], diff --git a/integrations/slack/spec.json b/integrations/slack/spec.json index 9a39bd5..ccf64b5 100644 --- a/integrations/slack/spec.json +++ b/integrations/slack/spec.json @@ -5,10 +5,10 @@ "icon": "slack", "mcp": { "type": "stdio", - "command": "npx", - "args": [ "-y", "@modelcontextprotocol/server-slack" ], + "url": "", + "args": [ ], "env": { - "SLACK_BOT_TOKEN": "${config:access_token}" + "SLACK_MCP_XOXP_TOKEN": "${config:access_token}" } }, "auth": { diff --git a/packages/core-cli/package.json b/packages/core-cli/package.json index 72c35dc..5c9628c 100644 --- a/packages/core-cli/package.json +++ b/packages/core-cli/package.json @@ -1,6 +1,6 @@ { "name": "@redplanethq/core", - "version": "0.1.2", + "version": "0.1.3", "description": "A Command-Line Interface for Core", "type": "module", "license": "MIT", diff --git a/packages/core-cli/src/commands/init.ts b/packages/core-cli/src/commands/init.ts index ac45ab0..abf2125 100644 --- a/packages/core-cli/src/commands/init.ts +++ b/packages/core-cli/src/commands/init.ts @@ -6,6 +6,8 @@ import { printCoreBrainLogo } from "../utils/ascii.js"; import { setupEnvFile } from "../utils/env.js"; import { hasTriggerConfig } from "../utils/env-checker.js"; import { getDockerCompatibleEnvVars } from "../utils/env-docker.js"; +import { handleDockerLogin } from "../utils/docker-login.js"; +import { deployTriggerTasks } from "../utils/trigger-deploy.js"; import path from "path"; export async function initCommand() { @@ -36,6 +38,8 @@ export async function initCommand() { const rootDir = process.cwd(); const triggerDir = path.join(rootDir, "trigger"); const webappDir = path.join(rootDir, "apps", "webapp"); + const databaseDir = path.join(rootDir, "packages", "database"); + const typesDir = path.join(rootDir, "packages", "types"); try { // Step 2: Setup .env file in root @@ -225,89 +229,12 @@ export async function initCommand() { } } - // Step 13: Show docker login instructions + // Step 13: Handle Docker login note("Run the following command to login to Docker registry:", "🐳 Docker Registry Login"); - - try { - // Read env file to get docker registry details - const envContent = await import("fs").then((fs) => - fs.promises.readFile(triggerEnvPath, "utf8") - ); - const envLines = envContent.split("\n"); - - const getEnvValue = (key: string) => { - const line = envLines.find((l) => l.startsWith(`${key}=`)); - return line ? line.split("=")[1] : ""; - }; - - const dockerRegistryUrl = getEnvValue("DOCKER_REGISTRY_URL"); - const dockerRegistryUsername = getEnvValue("DOCKER_REGISTRY_USERNAME"); - const dockerRegistryPassword = getEnvValue("DOCKER_REGISTRY_PASSWORD"); - - log.info( - `docker login -u ${dockerRegistryUsername} -p ${dockerRegistryPassword} ${dockerRegistryUrl} ` - ); - } catch (error) { - log.info("docker login -u -p "); - } - - const dockerLoginConfirmed = await confirm({ - message: "Have you completed the Docker login successfully?", - }); - - if (!dockerLoginConfirmed) { - outro("❌ Setup cancelled. Please complete Docker login first and run the command again."); - process.exit(1); - } + await handleDockerLogin(triggerEnvPath); // Step 14: Deploy Trigger.dev tasks - note( - "We'll now deploy the trigger tasks to your Trigger.dev instance.", - "🚀 Deploying Trigger.dev tasks" - ); - - try { - // Login to trigger.dev CLI - await executeCommandInteractive( - "npx -y trigger.dev@4.0.0-v4-beta.22 login -a http://localhost:8030", - { - cwd: rootDir, - message: "Logging in to Trigger.dev CLI...", - showOutput: true, - } - ); - - await executeCommandInteractive("pnpm install", { - cwd: rootDir, - message: "Running package installation", - showOutput: true, - }); - - await executeCommandInteractive("pnpm build --filter=@core/types --filter=@core/database", { - cwd: rootDir, - message: "Building @core/types and @core/database with turbo...", - showOutput: true, - }); - - // Deploy trigger tasks - const envVars = await getDockerCompatibleEnvVars(rootDir); - - console.log(envVars); - await executeCommandInteractive("pnpm run trigger:deploy", { - cwd: webappDir, - message: "Deploying Trigger.dev tasks...", - showOutput: true, - env: envVars, - }); - - log.success("Trigger.dev tasks deployed successfully!"); - } catch (error: any) { - log.warning("Failed to deploy Trigger.dev tasks:"); - note( - `${error.message}\n\nYou can deploy them manually later with:\n1. npx trigger.dev@v4-beta login -a http://localhost:8030\n2. pnpm trigger:deploy`, - "Manual Deployment" - ); - } + await deployTriggerTasks(rootDir); // Step 15: Final instructions outro("🎉 Setup Complete!"); diff --git a/packages/core-cli/src/utils/docker-login.ts b/packages/core-cli/src/utils/docker-login.ts new file mode 100644 index 0000000..84af43c --- /dev/null +++ b/packages/core-cli/src/utils/docker-login.ts @@ -0,0 +1,63 @@ +import { confirm, log } from "@clack/prompts"; +import path from "path"; +import os from "os"; +import fs from "fs"; + +export async function handleDockerLogin(triggerEnvPath: string): Promise { + // Check if Docker is already logged in to localhost:5000 + let dockerLoginNeeded = true; + try { + const dockerConfigPath = process.env.DOCKER_CONFIG + ? path.join(process.env.DOCKER_CONFIG, "config.json") + : path.join(os.homedir(), ".docker", "config.json"); + + if (fs.existsSync(dockerConfigPath)) { + const configContent = await fs.promises.readFile(dockerConfigPath, "utf8"); + const config = JSON.parse(configContent); + if ( + config && + config.auths && + Object.prototype.hasOwnProperty.call(config.auths, "localhost:5000") + ) { + dockerLoginNeeded = false; + } + } + } catch (error) { + // Ignore errors, will prompt for login below + } + + if (dockerLoginNeeded) { + try { + // Read env file to get docker registry details + const envContent = await fs.promises.readFile(triggerEnvPath, "utf8"); + const envLines = envContent.split("\n"); + + const getEnvValue = (key: string) => { + const line = envLines.find((l) => l.startsWith(`${key}=`)); + return line ? line.split("=")[1] : ""; + }; + + const dockerRegistryUrl = getEnvValue("DOCKER_REGISTRY_URL"); + const dockerRegistryUsername = getEnvValue("DOCKER_REGISTRY_USERNAME"); + const dockerRegistryPassword = getEnvValue("DOCKER_REGISTRY_PASSWORD"); + + log.info( + `docker login -u ${dockerRegistryUsername} -p ${dockerRegistryPassword} ${dockerRegistryUrl} ` + ); + } catch (error) { + log.info("docker login -u -p "); + } + } else { + log.info("✅ Docker is already logged in to localhost:5000, skipping login prompt."); + } + + const dockerLoginConfirmed = await confirm({ + message: "Have you completed the Docker login successfully?", + }); + + if (!dockerLoginConfirmed) { + throw new Error( + "Docker login required. Please complete Docker login first and run the command again." + ); + } +} diff --git a/packages/core-cli/src/utils/env-docker.ts b/packages/core-cli/src/utils/env-docker.ts index 0156f1b..06f767d 100644 --- a/packages/core-cli/src/utils/env-docker.ts +++ b/packages/core-cli/src/utils/env-docker.ts @@ -16,7 +16,6 @@ export async function getDockerCompatibleEnvVars(rootDir: string): Promise { return envVarsExpand[key] || ""; }; diff --git a/packages/core-cli/src/utils/trigger-deploy.ts b/packages/core-cli/src/utils/trigger-deploy.ts new file mode 100644 index 0000000..ed9dc2a --- /dev/null +++ b/packages/core-cli/src/utils/trigger-deploy.ts @@ -0,0 +1,66 @@ +import { note, log } from "@clack/prompts"; +import { executeCommandInteractive } from "./docker-interactive.js"; +import { getDockerCompatibleEnvVars } from "./env-docker.js"; +import path from "path"; + +export async function deployTriggerTasks(rootDir: string): Promise { + const webappDir = path.join(rootDir, "apps", "webapp"); + const databaseDir = path.join(rootDir, "packages", "database"); + const typesDir = path.join(rootDir, "packages", "types"); + + note( + "We'll now deploy the trigger tasks to your Trigger.dev instance.", + "🚀 Deploying Trigger.dev tasks" + ); + + try { + // Login to trigger.dev CLI + await executeCommandInteractive( + "npx -y trigger.dev@4.0.0-v4-beta.22 login -a http://localhost:8030", + { + cwd: rootDir, + message: "Logging in to Trigger.dev CLI...", + showOutput: true, + } + ); + + await executeCommandInteractive("pnpm install", { + cwd: rootDir, + message: "Running package installation", + showOutput: true, + }); + + const envVars = await getDockerCompatibleEnvVars(rootDir); + + await executeCommandInteractive("pnpm build", { + cwd: databaseDir, + message: "Building @core/database...", + showOutput: true, + env: { + DATABASE_URL: envVars.DATABASE_URL as string, + }, + }); + + await executeCommandInteractive("pnpm build", { + cwd: typesDir, + message: "Building @core/types...", + showOutput: true, + }); + + // Deploy trigger tasks + await executeCommandInteractive("pnpm run trigger:deploy", { + cwd: webappDir, + message: "Deploying Trigger.dev tasks...", + showOutput: true, + env: envVars, + }); + + log.success("Trigger.dev tasks deployed successfully!"); + } catch (error: any) { + log.warning("Failed to deploy Trigger.dev tasks:"); + note( + `${error.message}\n\nYou can deploy them manually later with:\n1. npx trigger.dev@v4-beta login -a http://localhost:8030\n2. pnpm trigger:deploy`, + "Manual Deployment" + ); + } +} \ No newline at end of file diff --git a/turbo.json b/turbo.json index 74d3a93..9787ad1 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,6 @@ { "$schema": "https://turborepo.com/schema.json", + "ui": "tui", "tasks": { "build": { "dependsOn": [ "^build" ],