mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 09:48:27 +00:00
Fix: cli working and mcp proxy
This commit is contained in:
parent
cd139f715a
commit
64a3cc888c
@ -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 (
|
||||
<svg
|
||||
width={width}
|
||||
|
||||
@ -6,13 +6,21 @@
|
||||
|
||||
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 { isbot } from "isbot";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
import { initializeStartupServices } from "./utils/startup";
|
||||
|
||||
const ABORT_DELAY = 5_000;
|
||||
|
||||
// Initialize startup services once per server process
|
||||
await initializeStartupServices();
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
@ -21,20 +29,20 @@ export default function handleRequest(
|
||||
// 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!
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
loadContext: AppLoadContext
|
||||
loadContext: AppLoadContext,
|
||||
) {
|
||||
return isbot(request.headers.get("user-agent") || "")
|
||||
? handleBotRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
remixContext,
|
||||
)
|
||||
: handleBrowserRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
remixContext,
|
||||
);
|
||||
}
|
||||
|
||||
@ -42,7 +50,7 @@ function handleBotRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
remixContext: EntryContext,
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
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);
|
||||
|
||||
@ -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<typeof loader>();
|
||||
return (
|
||||
<ThemeProvider specifiedTheme={Theme.LIGHT} themeAction="/action/set-theme">
|
||||
<App />
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -10,6 +10,9 @@ export const getIntegrationAccount = async (
|
||||
integratedById: userId,
|
||||
isActive: true,
|
||||
},
|
||||
include: {
|
||||
integrationDefinition: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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<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 {
|
||||
private Client: 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;
|
||||
}
|
||||
};
|
||||
|
||||
34
apps/webapp/app/utils/startup.ts
Normal file
34
apps/webapp/app/utils/startup.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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}`),
|
||||
|
||||
@ -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"],
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 <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);
|
||||
}
|
||||
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!");
|
||||
|
||||
63
packages/core-cli/src/utils/docker-login.ts
Normal file
63
packages/core-cli/src/utils/docker-login.ts
Normal 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."
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,6 @@ export async function getDockerCompatibleEnvVars(rootDir: string): Promise<Recor
|
||||
const envVarsExpand =
|
||||
dotenvExpand.expand(dotenv.config({ path: envPath, processEnv: {} })).parsed || {};
|
||||
|
||||
console.log(JSON.stringify(envVarsExpand));
|
||||
const getEnvValue = (key: string): string => {
|
||||
return envVarsExpand[key] || "";
|
||||
};
|
||||
|
||||
66
packages/core-cli/src/utils/trigger-deploy.ts
Normal file
66
packages/core-cli/src/utils/trigger-deploy.ts
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "https://turborepo.com/schema.json",
|
||||
"ui": "tui",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": [ "^build" ],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user