From 2a36c1399bb1d165324b98a218ca99d8ef5f9c38 Mon Sep 17 00:00:00 2001 From: Lorenc Dedaj Date: Thu, 13 Nov 2025 21:17:17 -0500 Subject: [PATCH 1/2] fix the CAN NOT GET bug --- server/routes/auth.github.js | 6 +++++- server/server.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) 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 e294564..0e7ebc4 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; From dc849304d07540f895a8c02a8065f472b2a391f3 Mon Sep 17 00:00:00 2001 From: Lorenc Dedaj Date: Sat, 15 Nov 2025 12:02:49 -0500 Subject: [PATCH 2/2] initial implementation of Dockerfile --- .dockerignore | 10 ++ Dockerfile | 54 ++++------ Dockerfile.jenkins | 37 +++++++ server/agent/wizardAgent.js | 194 +++++++++++++++++++++++------------- 4 files changed, 191 insertions(+), 104 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile.jenkins 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 64f0d2b..96eed21 100644 --- a/server/agent/wizardAgent.js +++ b/server/agent/wizardAgent.js @@ -1,25 +1,39 @@ -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) { try { const response = await fetch(`http://localhost:3000/mcp/v1/${tool}`, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${process.env.MCP_SESSION_TOKEN}`, + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.MCP_SESSION_TOKEN}`, }, body: JSON.stringify(input), }); 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' }; } } @@ -45,16 +59,18 @@ export async function runWizardAgent(userPrompt) { - “List repos”, “List repositories”, or “repositories” → use \`repo_reader\` with optional \`{ username: "...", user_id: "..." }\` `; + 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: userPrompt }, + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt }, ], }); const decision = completion.choices[0].message.content; - console.log("\n🤖 Agent decided:", decision); + console.log('\n🤖 Agent decided:', decision); // Tool mapping using regex patterns const toolMap = { @@ -70,47 +86,67 @@ export async function runWizardAgent(userPrompt) { // --- Extract context dynamically from userPrompt or decision --- // Prefer explicit labels like: "repo owner/name", "template node_app", "provider aws" - const labeledRepo = userPrompt.match(/\brepo\s+([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\b/i) - || decision.match(/\brepo\s+([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\b/i); - const genericRepo = (userPrompt + " " + decision).match(/\b(?!ci\/cd\b)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\b/); - const repo = (labeledRepo?.[1] || genericRepo?.[1] || null); - - const labeledProvider = userPrompt.match(/\bprovider\s+(aws|jenkins|gcp|azure)\b/i) - || decision.match(/\bprovider\s+(aws|jenkins|gcp|azure)\b/i); - const genericProvider = userPrompt.match(/\b(aws|jenkins|github actions|gcp|azure)\b/i) - || decision.match(/\b(aws|jenkins|github actions|gcp|azure)\b/i); - const provider = (labeledProvider?.[1] || genericProvider?.[1] || null)?.toLowerCase().replace(/\s+/g, ' '); - - const labeledTemplate = userPrompt.match(/\btemplate\s+([a-z_][a-z0-9_]+)\b/i) - || decision.match(/\btemplate\s+([a-z_][a-z0-9_]+)\b/i); - const genericTemplate = userPrompt.match(/\b(node_app|python_app|container_service|node|python|react|express|django|flask|java|go)\b/i) - || decision.match(/\b(node_app|python_app|container_service|node|python|react|express|django|flask|java|go)\b/i); - const template = (labeledTemplate?.[1] || genericTemplate?.[1] || null)?.toLowerCase(); - - if (toolName === "repo_reader") { + const labeledRepo = + userPrompt.match(/\brepo\s+([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\b/i) || + decision.match(/\brepo\s+([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\b/i); + const genericRepo = (userPrompt + ' ' + decision).match( + /\b(?!ci\/cd\b)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\b/ + ); + const repo = labeledRepo?.[1] || genericRepo?.[1] || null; + + const labeledProvider = + userPrompt.match(/\bprovider\s+(aws|jenkins|gcp|azure)\b/i) || + decision.match(/\bprovider\s+(aws|jenkins|gcp|azure)\b/i); + const genericProvider = + userPrompt.match(/\b(aws|jenkins|github actions|gcp|azure)\b/i) || + decision.match(/\b(aws|jenkins|github actions|gcp|azure)\b/i); + const provider = (labeledProvider?.[1] || genericProvider?.[1] || null) + ?.toLowerCase() + .replace(/\s+/g, ' '); + + const labeledTemplate = + userPrompt.match(/\btemplate\s+([a-z_][a-z0-9_]+)\b/i) || + decision.match(/\btemplate\s+([a-z_][a-z0-9_]+)\b/i); + const genericTemplate = + userPrompt.match( + /\b(node_app|python_app|container_service|node|python|react|express|django|flask|java|go)\b/i + ) || + decision.match( + /\b(node_app|python_app|container_service|node|python|react|express|django|flask|java|go)\b/i + ); + const template = ( + labeledTemplate?.[1] || + genericTemplate?.[1] || + null + )?.toLowerCase(); + + if (toolName === 'repo_reader') { // Extract optional username, user_id, and repo info const usernameMatch = userPrompt.match(/\busername[:=]?\s*([\w-]+)\b/i); - const userIdMatch = userPrompt.match(/\buser[_ ]?id[:=]?\s*([\w-]+)\b/i); + const userIdMatch = userPrompt.match( + /\buser[_ ]?id[:=]?\s*([\w-]+)\b/i + ); const repoMatch = userPrompt.match(/\b([\w-]+\/[\w-]+)\b/); const payload = {}; 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}`; } - return await callMCPTool("repo_reader", payload); + return await callMCPTool('repo_reader', payload); } - 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'.", }; } @@ -121,35 +157,51 @@ export async function runWizardAgent(userPrompt) { // Fetch GitHub repo details before pipeline generation let repoInfo = null; try { - const info = await callMCPTool("github_adapter", { action: "info", repo }); + const info = await callMCPTool('github_adapter', { + action: 'info', + repo, + }); if (info?.data?.success) { repoInfo = info.data; 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'; // --- Preserve repo context globally --- if (!payload.repo && globalThis.LAST_REPO_USED) { @@ -159,44 +211,52 @@ 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}`); } - console.log("🧩 Final payload to pipeline_generator:", payload); - return await callMCPTool("pipeline_generator", payload); + console.log('🧩 Final payload to pipeline_generator:', payload); + return await callMCPTool('pipeline_generator', payload); } - if (toolName === "oidc_adapter") { + if (toolName === 'oidc_adapter') { const payload = provider ? { provider } : {}; - return await callMCPTool("oidc_adapter", payload); + return await callMCPTool('oidc_adapter', payload); } - if (toolName === "github_adapter") { + if (toolName === 'github_adapter') { if (repo) { - return await callMCPTool("github/info", { repo }); + return await callMCPTool('github/info', { repo }); } 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').", }; } } } } - return { message: "No matching tool found. Try asking about a repo, pipeline, or AWS role." }; + return { + message: + 'No matching tool found. Try asking about a repo, pipeline, or AWS role.', + }; } // 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 +}