mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 18:28:29 +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 {
|
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}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,9 @@ export const getIntegrationAccount = async (
|
|||||||
integratedById: userId,
|
integratedById: userId,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
integrationDefinition: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
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
|
// 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}`),
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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!");
|
||||||
|
|||||||
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 =
|
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] || "";
|
||||||
};
|
};
|
||||||
|
|||||||
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",
|
"$schema": "https://turborepo.com/schema.json",
|
||||||
|
"ui": "tui",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": [ "^build" ],
|
"dependsOn": [ "^build" ],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user