Fix: cli working and mcp proxy

This commit is contained in:
Harshith Mullapudi 2025-07-22 10:17:19 +05:30
parent cd139f715a
commit 64a3cc888c
17 changed files with 420 additions and 118 deletions

View File

@ -1,14 +1,9 @@
import React from "react";
import { Theme, useTheme } from "remix-themes";
export interface LogoProps { export interface LogoProps {
width: number; width: number;
height: number; height: number;
} }
export default function StaticLogo({ width, height }: LogoProps) { export default function StaticLogo({ width, height }: LogoProps) {
const [theme] = useTheme();
return ( return (
<svg <svg
width={width} width={width}

View File

@ -6,13 +6,21 @@
import { PassThrough } from "node:stream"; import { PassThrough } from "node:stream";
import { type AppLoadContext, type EntryContext , createReadableStreamFromReadable } from "@remix-run/node"; import {
type AppLoadContext,
type EntryContext,
createReadableStreamFromReadable,
} from "@remix-run/node";
import { RemixServer } from "@remix-run/react"; import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot"; import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server"; import { renderToPipeableStream } from "react-dom/server";
import { initializeStartupServices } from "./utils/startup";
const ABORT_DELAY = 5_000; const ABORT_DELAY = 5_000;
// Initialize startup services once per server process
await initializeStartupServices();
export default function handleRequest( export default function handleRequest(
request: Request, request: Request,
responseStatusCode: number, responseStatusCode: number,
@ -21,20 +29,20 @@ export default function handleRequest(
// This is ignored so we can keep it in the template for visibility. Feel // This is ignored so we can keep it in the template for visibility. Feel
// free to delete this parameter in your app if you're not using it! // free to delete this parameter in your app if you're not using it!
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
loadContext: AppLoadContext loadContext: AppLoadContext,
) { ) {
return isbot(request.headers.get("user-agent") || "") return isbot(request.headers.get("user-agent") || "")
? handleBotRequest( ? handleBotRequest(
request, request,
responseStatusCode, responseStatusCode,
responseHeaders, responseHeaders,
remixContext remixContext,
) )
: handleBrowserRequest( : handleBrowserRequest(
request, request,
responseStatusCode, responseStatusCode,
responseHeaders, responseHeaders,
remixContext remixContext,
); );
} }
@ -42,7 +50,7 @@ function handleBotRequest(
request: Request, request: Request,
responseStatusCode: number, responseStatusCode: number,
responseHeaders: Headers, responseHeaders: Headers,
remixContext: EntryContext remixContext: EntryContext,
) { ) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let shellRendered = false; let shellRendered = false;
@ -64,7 +72,7 @@ function handleBotRequest(
new Response(stream, { new Response(stream, {
headers: responseHeaders, headers: responseHeaders,
status: responseStatusCode, status: responseStatusCode,
}) }),
); );
pipe(body); pipe(body);
@ -81,7 +89,7 @@ function handleBotRequest(
console.error(error); console.error(error);
} }
}, },
} },
); );
setTimeout(abort, ABORT_DELAY); setTimeout(abort, ABORT_DELAY);
@ -92,7 +100,7 @@ function handleBrowserRequest(
request: Request, request: Request,
responseStatusCode: number, responseStatusCode: number,
responseHeaders: Headers, responseHeaders: Headers,
remixContext: EntryContext remixContext: EntryContext,
) { ) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let shellRendered = false; let shellRendered = false;
@ -114,7 +122,7 @@ function handleBrowserRequest(
new Response(stream, { new Response(stream, {
headers: responseHeaders, headers: responseHeaders,
status: responseStatusCode, status: responseStatusCode,
}) }),
); );
pipe(body); pipe(body);
@ -131,7 +139,7 @@ function handleBrowserRequest(
console.error(error); console.error(error);
} }
}, },
} },
); );
setTimeout(abort, ABORT_DELAY); setTimeout(abort, ABORT_DELAY);

View File

@ -4,7 +4,6 @@ import {
Outlet, Outlet,
Scripts, Scripts,
ScrollRestoration, ScrollRestoration,
useLoaderData,
} from "@remix-run/react"; } from "@remix-run/react";
import type { import type {
LinksFunction, LinksFunction,
@ -41,7 +40,6 @@ import {
useTheme, useTheme,
} from "remix-themes"; } from "remix-themes";
import clsx from "clsx"; import clsx from "clsx";
import { initNeo4jSchemaOnce } from "./lib/neo4j.server";
export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }]; 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 toastMessage = session.get("toastMessage") as ToastMessage;
const { getTheme } = await themeSessionResolver(request); const { getTheme } = await themeSessionResolver(request);
await initNeo4jSchemaOnce();
const posthogProjectKey = env.POSTHOG_PROJECT_KEY; const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
return typedjson( return typedjson(
@ -138,7 +134,6 @@ function App() {
// `specifiedTheme` is the stored theme in the session storage. // `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. // `themeAction` is the action name that's used to change the theme in the session storage.
export default function AppWithProviders() { export default function AppWithProviders() {
const data = useLoaderData<typeof loader>();
return ( return (
<ThemeProvider specifiedTheme={Theme.LIGHT} themeAction="/action/set-theme"> <ThemeProvider specifiedTheme={Theme.LIGHT} themeAction="/action/set-theme">
<App /> <App />

View File

@ -6,6 +6,7 @@ import { z } from "zod";
import { getIntegrationAccount } from "~/services/integrationAccount.server"; import { getIntegrationAccount } from "~/services/integrationAccount.server";
import { createMCPStdioProxy } from "@core/mcp-proxy"; import { createMCPStdioProxy } from "@core/mcp-proxy";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { configureStdioMCPEnvironment } from "~/trigger/utils/mcp";
export const integrationSlugSchema = z.object({ export const integrationSlugSchema = z.object({
slug: z.string(), slug: z.string(),
@ -64,13 +65,13 @@ const { action, loader } = createActionApiRoute(
const { url, type } = spec.mcp; const { url, type } = spec.mcp;
if (type === "http") { // Find the integration account for this user and integration
// Find the integration account for this user and integration const integrationAccount = await getIntegrationAccount(
const integrationAccount = await getIntegrationAccount( integrationDefinition.id,
integrationDefinition.id, authentication.userId,
authentication.userId, );
);
if (type === "http") {
const integrationConfig = const integrationConfig =
integrationAccount?.integrationConfiguration as any; integrationAccount?.integrationConfiguration as any;
@ -97,7 +98,23 @@ const { action, loader } = createActionApiRoute(
integrationConfig.mcp.tokens.access_token, integrationConfig.mcp.tokens.access_token,
); );
} else { } 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 // Get session_id from headers (case-insensitive), or generate a new uuid if not present
const sessionId = const sessionId =
@ -105,10 +122,11 @@ const { action, loader } = createActionApiRoute(
request.headers.get("Mcp-Session-Id") || request.headers.get("Mcp-Session-Id") ||
randomUUID(); randomUUID();
return createMCPStdioProxy(request, "npx", ["-y", "hevy-mcp"], { // Use the saved local file instead of command
env: { const executablePath = `./integrations/${slug}/main`;
HEVY_API_KEY: "e1fa3a63-c7c2-4335-9753-042bd9028330",
}, return createMCPStdioProxy(request, executablePath, args, {
env,
sessionId, sessionId,
}); });
} }

View File

@ -10,6 +10,9 @@ export const getIntegrationAccount = async (
integratedById: userId, integratedById: userId,
isActive: true, isActive: true,
}, },
include: {
integrationDefinition: true,
},
}); });
}; };

View File

@ -83,7 +83,6 @@ export class OAuth2Service {
// Validate redirect URI // Validate redirect URI
validateRedirectUri(client: any, redirectUri: string): boolean { validateRedirectUri(client: any, redirectUri: string): boolean {
console.log(redirectUri);
const allowedUris = client.redirectUris const allowedUris = client.redirectUris
.split(",") .split(",")
.map((uri: string) => uri.trim()); .map((uri: string) => uri.trim());

View File

@ -1,9 +1,69 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { logger } from "@trigger.dev/sdk/v3"; import { logger } from "@trigger.dev/sdk/v3";
import { jsonSchema, tool, type ToolSet } from "ai"; import { jsonSchema, tool, type ToolSet } from "ai";
import * as fs from "fs";
import * as path from "path";
import { type MCPTool } from "./types"; import { type MCPTool } from "./types";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { prisma } from "~/db.server";
export const configureStdioMCPEnvironment = (
spec: any,
account: any,
): { env: Record<string, string>; 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 { export class MCP {
private Client: any; private Client: any;
private clients: Record<string, any> = {}; private clients: Record<string, any> = {};
@ -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;
}
};

View File

@ -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;
}
}

View File

@ -47,6 +47,7 @@ async function init() {
// handle SSR requests // handle SSR requests
app.all("*", remixHandler); app.all("*", remixHandler);
const port = process.env.REMIX_APP_PORT || 3000; const port = process.env.REMIX_APP_PORT || 3000;
app.listen(port, () => app.listen(port, () =>
console.log(`Express server listening at http://localhost:${port}`), console.log(`Express server listening at http://localhost:${port}`),

View File

@ -7,7 +7,8 @@
"**/*.tsx", "**/*.tsx",
"tailwind.config.js", "tailwind.config.js",
"tailwind.config.js", "tailwind.config.js",
"trigger.config.ts" "trigger.config.ts",
"server.mjs"
], ],
"compilerOptions": { "compilerOptions": {
"types": ["@remix-run/node", "vite/client"], "types": ["@remix-run/node", "vite/client"],

View File

@ -5,10 +5,10 @@
"icon": "slack", "icon": "slack",
"mcp": { "mcp": {
"type": "stdio", "type": "stdio",
"command": "npx", "url": "",
"args": [ "-y", "@modelcontextprotocol/server-slack" ], "args": [ ],
"env": { "env": {
"SLACK_BOT_TOKEN": "${config:access_token}" "SLACK_MCP_XOXP_TOKEN": "${config:access_token}"
} }
}, },
"auth": { "auth": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@redplanethq/core", "name": "@redplanethq/core",
"version": "0.1.2", "version": "0.1.3",
"description": "A Command-Line Interface for Core", "description": "A Command-Line Interface for Core",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",

View File

@ -6,6 +6,8 @@ import { printCoreBrainLogo } from "../utils/ascii.js";
import { setupEnvFile } from "../utils/env.js"; import { setupEnvFile } from "../utils/env.js";
import { hasTriggerConfig } from "../utils/env-checker.js"; import { hasTriggerConfig } from "../utils/env-checker.js";
import { getDockerCompatibleEnvVars } from "../utils/env-docker.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"; import path from "path";
export async function initCommand() { export async function initCommand() {
@ -36,6 +38,8 @@ export async function initCommand() {
const rootDir = process.cwd(); const rootDir = process.cwd();
const triggerDir = path.join(rootDir, "trigger"); const triggerDir = path.join(rootDir, "trigger");
const webappDir = path.join(rootDir, "apps", "webapp"); const webappDir = path.join(rootDir, "apps", "webapp");
const databaseDir = path.join(rootDir, "packages", "database");
const typesDir = path.join(rootDir, "packages", "types");
try { try {
// Step 2: Setup .env file in root // 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"); note("Run the following command to login to Docker registry:", "🐳 Docker Registry Login");
await handleDockerLogin(triggerEnvPath);
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 <USERNAME> -p <PASSWORD> <REGISTRY_URL>");
}
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);
}
// Step 14: Deploy Trigger.dev tasks // Step 14: Deploy Trigger.dev tasks
note( await deployTriggerTasks(rootDir);
"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"
);
}
// Step 15: Final instructions // Step 15: Final instructions
outro("🎉 Setup Complete!"); outro("🎉 Setup Complete!");

View File

@ -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<void> {
// 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 <USERNAME> -p <PASSWORD> <REGISTRY_URL>");
}
} 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."
);
}
}

View File

@ -16,7 +16,6 @@ export async function getDockerCompatibleEnvVars(rootDir: string): Promise<Recor
const envVarsExpand = const envVarsExpand =
dotenvExpand.expand(dotenv.config({ path: envPath, processEnv: {} })).parsed || {}; dotenvExpand.expand(dotenv.config({ path: envPath, processEnv: {} })).parsed || {};
console.log(JSON.stringify(envVarsExpand));
const getEnvValue = (key: string): string => { const getEnvValue = (key: string): string => {
return envVarsExpand[key] || ""; return envVarsExpand[key] || "";
}; };

View File

@ -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<void> {
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"
);
}
}

View File

@ -1,5 +1,6 @@
{ {
"$schema": "https://turborepo.com/schema.json", "$schema": "https://turborepo.com/schema.json",
"ui": "tui",
"tasks": { "tasks": {
"build": { "build": {
"dependsOn": [ "^build" ], "dependsOn": [ "^build" ],