diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7a7fc5a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +npm-debug.log +Dockerfile +Dockerfile.jenkins +docker-compose*.yml +.git +.gitignore +.env +.vscode +dist \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 9b0f50a..8beec82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,17 @@ -# Jenkins LTS (with JDK 17). The current LTS line (>= 2.492.3) meets the MCP plugin minimum requirement. -FROM jenkins/jenkins:lts-jdk17 - -USER root - -# Install base tools (git, curl, certificates). -# If you use dedicated Jenkins agents, also install git inside your agent images. -RUN apt-get update && apt-get install -y --no-install-recommends \ - git curl ca-certificates && \ - rm -rf /var/lib/apt/lists/* - -# Switch back to jenkins user -USER jenkins - -# Preinstall plugins: -# MCP Server, Git, Git Client, GitHub integration, Pipeline, and Credentials -# Note: jenkins-plugin-cli is included in the official Jenkins image. -RUN jenkins-plugin-cli --plugins \ - mcp-server \ - git \ - git-client \ - github \ - github-branch-source \ - workflow-aggregator \ - credentials \ - ssh-credentials \ - configuration-as-code - -# Expose ports -EXPOSE 8080 50000 - -# (Optional) Jenkins startup parameters -# Disable the setup wizard on first startup: -# ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false" - -# (Optional) Mount JCasC configuration file -# ENV CASC_JENKINS_CONFIG=/var/jenkins_home/casc.yaml \ No newline at end of file +FROM node:20-alpine + +WORKDIR /app + +ENV NODE_ENV=production + +ENV PORT=3000 + +COPY package*.json ./ + +RUN npm ci --omit=dev + +COPY . . + +EXPOSE 3000 + +CMD ["node", "server/server.js"] \ No newline at end of file diff --git a/Dockerfile.jenkins b/Dockerfile.jenkins new file mode 100644 index 0000000..9b0f50a --- /dev/null +++ b/Dockerfile.jenkins @@ -0,0 +1,37 @@ +# Jenkins LTS (with JDK 17). The current LTS line (>= 2.492.3) meets the MCP plugin minimum requirement. +FROM jenkins/jenkins:lts-jdk17 + +USER root + +# Install base tools (git, curl, certificates). +# If you use dedicated Jenkins agents, also install git inside your agent images. +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# Switch back to jenkins user +USER jenkins + +# Preinstall plugins: +# MCP Server, Git, Git Client, GitHub integration, Pipeline, and Credentials +# Note: jenkins-plugin-cli is included in the official Jenkins image. +RUN jenkins-plugin-cli --plugins \ + mcp-server \ + git \ + git-client \ + github \ + github-branch-source \ + workflow-aggregator \ + credentials \ + ssh-credentials \ + configuration-as-code + +# Expose ports +EXPOSE 8080 50000 + +# (Optional) Jenkins startup parameters +# Disable the setup wizard on first startup: +# ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false" + +# (Optional) Mount JCasC configuration file +# ENV CASC_JENKINS_CONFIG=/var/jenkins_home/casc.yaml \ No newline at end of file diff --git a/server/agent/wizardAgent.js b/server/agent/wizardAgent.js index 6f141ec..1aa84b5 100644 --- a/server/agent/wizardAgent.js +++ b/server/agent/wizardAgent.js @@ -1,15 +1,29 @@ -import OpenAI from "openai"; -import dotenv from "dotenv"; -import fetch from "node-fetch"; +import OpenAI from 'openai'; +import dotenv from 'dotenv'; +import fetch from 'node-fetch'; dotenv.config(); -const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); +//const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + +// Construct the OpenAI client lazily so that the server does not shut down completely when we build the container +let client = null; + +function getClient() { + if (!client) { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('Missing OPENAI_API_KEY - cannot run Wizard Agent'); + } + client = new OpenAI({ apiKey }); + } + return client; +} // Helper: call MCP routes dynamically, with error handling async function callMCPTool(tool, input, cookie) { try { const response = await fetch(`http://localhost:3000/mcp/v1/${tool}`, { - method: "POST", + method: 'POST', headers: { "Content-Type": "application/json", "Cookie": cookie || (process.env.MCP_SESSION_TOKEN ? `mcp_session=${process.env.MCP_SESSION_TOKEN}` : ""), @@ -18,8 +32,8 @@ async function callMCPTool(tool, input, cookie) { }); return await response.json(); } catch (err) { - console.warn("⚠️ MCP call failed:", err.message || err); - return { error: "MCP server unreachable" }; + console.warn('⚠️ MCP call failed:', err.message || err); + return { error: 'MCP server unreachable' }; } } @@ -59,8 +73,10 @@ export async function runWizardAgent(userPrompt) { Never invent new template names. If unsure, default to "node_app". `; + const client = getClient(); + const completion = await client.chat.completions.create({ - model: "gpt-4o-mini", + model: 'gpt-4o-mini', messages: [ { role: "system", content: systemPrompt }, { role: "user", content: typeof userPrompt === "string" ? userPrompt : userPrompt.prompt }, @@ -68,7 +84,7 @@ export async function runWizardAgent(userPrompt) { }); const decision = completion.choices[0].message.content; - console.log("\n🤖 Agent decided:", decision); + console.log('\n🤖 Agent decided:', decision); let agentMeta = { agent_decision: decision, @@ -117,7 +133,7 @@ export async function runWizardAgent(userPrompt) { if (usernameMatch) payload.username = usernameMatch[1]; if (userIdMatch) payload.user_id = userIdMatch[1]; if (repoMatch) { - const [username, repo] = repoMatch[1].split("/"); + const [username, repo] = repoMatch[1].split('/'); payload.username = username; payload.repo = `${username}/${repo}`; } @@ -132,12 +148,13 @@ export async function runWizardAgent(userPrompt) { }; } - if (toolName === "pipeline_generator") { + if (toolName === 'pipeline_generator') { if (!repo) { - console.warn("⚠️ Missing repo context for pipeline generation."); - return { - success: false, - error: "I couldn’t determine which repository you meant. Please specify it, e.g., 'generate pipeline for user/repo'." + console.warn('⚠️ Missing repo context for pipeline generation.'); + return { + success: false, + error: + "I couldn’t determine which repository you meant. Please specify it, e.g., 'generate pipeline for user/repo'.", }; } @@ -154,29 +171,42 @@ export async function runWizardAgent(userPrompt) { console.log(`📦 Retrieved repo info from GitHub:`, repoInfo); } } catch (err) { - console.warn("⚠️ Failed to fetch GitHub info before pipeline generation:", err.message); + console.warn( + '⚠️ Failed to fetch GitHub info before pipeline generation:', + err.message + ); } // Merge language or visibility into payload if available - if (repoInfo?.language && !payload.language) payload.language = repoInfo.language.toLowerCase(); - if (repoInfo?.visibility && !payload.visibility) payload.visibility = repoInfo.visibility; + if (repoInfo?.language && !payload.language) + payload.language = repoInfo.language.toLowerCase(); + if (repoInfo?.visibility && !payload.visibility) + payload.visibility = repoInfo.visibility; // Infer template if still missing if (!payload.template) { - if (repoInfo?.language?.toLowerCase().includes("javascript") || repoInfo?.language?.toLowerCase().includes("typescript") || /js|ts|node|javascript/i.test(repo)) { - payload.template = "node_app"; - } else if (repoInfo?.language?.toLowerCase().includes("python") || /py|flask|django/i.test(repo)) { - payload.template = "python_app"; + if ( + repoInfo?.language?.toLowerCase().includes('javascript') || + repoInfo?.language?.toLowerCase().includes('typescript') || + /js|ts|node|javascript/i.test(repo) + ) { + payload.template = 'node_app'; + } else if ( + repoInfo?.language?.toLowerCase().includes('python') || + /py|flask|django/i.test(repo) + ) { + payload.template = 'python_app'; } else { - payload.template = "container_service"; + payload.template = 'container_service'; } console.log(`🪄 Inferred template: ${payload.template}`); } // --- Auto-correct short template names --- - if (payload.template === "node") payload.template = "node_app"; - if (payload.template === "python") payload.template = "python_app"; - if (payload.template === "container") payload.template = "container_service"; + if (payload.template === 'node') payload.template = 'node_app'; + if (payload.template === 'python') payload.template = 'python_app'; + if (payload.template === 'container') + payload.template = 'container_service'; // --- Validate template against allowed values --- const allowedTemplates = ["node_app", "python_app", "container_service"]; @@ -193,9 +223,13 @@ export async function runWizardAgent(userPrompt) { } // ✅ Ensure provider is valid before sending payload - if (!payload.provider || !["aws", "jenkins"].includes(payload.provider)) { + if ( + !payload.provider || + !['aws', 'jenkins'].includes(payload.provider) + ) { // Infer from repo visibility or fallback to AWS - payload.provider = repoInfo?.visibility === "private" ? "jenkins" : "aws"; + payload.provider = + repoInfo?.visibility === 'private' ? 'jenkins' : 'aws'; console.log(`🧭 Inferred provider: ${payload.provider}`); } @@ -298,7 +332,7 @@ export async function runWizardAgent(userPrompt) { }; } - if (toolName === "oidc_adapter") { + if (toolName === 'oidc_adapter') { const payload = provider ? { provider } : {}; agentMeta.tool_called = "oidc_adapter"; const output = await callMCPTool("oidc_adapter", payload, cookie); @@ -310,7 +344,7 @@ export async function runWizardAgent(userPrompt) { }; } - if (toolName === "github_adapter") { + if (toolName === 'github_adapter') { if (repo) { agentMeta.tool_called = "github_adapter"; const output = await callMCPTool("github/info", { repo }, cookie); @@ -321,10 +355,11 @@ export async function runWizardAgent(userPrompt) { tool_output: output }; } else { - console.warn("⚠️ Missing repo for GitHub info retrieval."); - return { - success: false, - error: "Couldn’t determine which repository to fetch. Please include it in your request (e.g., 'tell me about user/repo')." + console.warn('⚠️ Missing repo for GitHub info retrieval.'); + return { + success: false, + error: + "Couldn’t determine which repository to fetch. Please include it in your request (e.g., 'tell me about user/repo').", }; } } @@ -341,10 +376,10 @@ export async function runWizardAgent(userPrompt) { // Example local test (can comment out for production) if (process.argv[2]) { - const input = process.argv.slice(2).join(" "); + const input = process.argv.slice(2).join(' '); runWizardAgent(input) .then((res) => { - console.log("\n📦 Tool Output:\n", JSON.stringify(res, null, 2)); + console.log('\n📦 Tool Output:\n', JSON.stringify(res, null, 2)); }) .catch(console.error); -} \ No newline at end of file +} diff --git a/server/routes/auth.github.js b/server/routes/auth.github.js index efd9ce7..ca76fa4 100644 --- a/server/routes/auth.github.js +++ b/server/routes/auth.github.js @@ -19,6 +19,10 @@ const { JWT_SECRET, } = process.env; +// URL to redirect user to the apropiate endpoint after GitHub authentication success +const FRONTEND_URL = + process.env.FRONTEND_URL || 'http://localhost:5173/connect'; + if (!GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET || !GITHUB_OAUTH_REDIRECT_URI) { console.warn('[WARN] Missing GitHub OAuth env vars'); } @@ -158,7 +162,7 @@ router.get('/callback', async (req, res) => { // secure: true, // enable on HTTPS }); - return res.redirect('/'); + return res.redirect(FRONTEND_URL); } catch (e) { console.error('[OAuth callback] error:', e); return res.status(500).send(`OAuth failed: ${e.message}`); diff --git a/server/server.js b/server/server.js index 25a20f5..f0fb61c 100644 --- a/server/server.js +++ b/server/server.js @@ -75,7 +75,7 @@ const UserBody = z.object({ // Create or upsert user by email app.post('/users', async (req, res) => { - const parse = UserBody.safeParse(req.body); // love that you are doing this. great. + const parse = UserBody.safeParse(req.body); if (!parse.success) return res.status(400).json({ error: parse.error.message }); const { email, github_username } = parse.data;