diff --git a/backend/src/routes/versionRoutes.js b/backend/src/routes/versionRoutes.js new file mode 100644 index 0000000..298cdd7 --- /dev/null +++ b/backend/src/routes/versionRoutes.js @@ -0,0 +1,160 @@ +const express = require('express'); +const { authenticateToken } = require('../middleware/auth'); +const { requireManageSettings } = require('../middleware/permissions'); +const { PrismaClient } = require('@prisma/client'); +const { exec } = require('child_process'); +const { promisify } = require('util'); + +const prisma = new PrismaClient(); +const execAsync = promisify(exec); + +const router = express.Router(); + +// Get current version info +router.get('/current', authenticateToken, async (req, res) => { + try { + // For now, return hardcoded version - this should match your agent version + const currentVersion = '1.2.3'; + + res.json({ + version: currentVersion, + buildDate: new Date().toISOString(), + environment: process.env.NODE_ENV || 'development' + }); + } catch (error) { + console.error('Error getting current version:', error); + res.status(500).json({ error: 'Failed to get current version' }); + } +}); + +// Check for updates from GitHub +router.get('/check-updates', authenticateToken, requireManageSettings, async (req, res) => { + try { + // Get GitHub repo URL from settings + const settings = await prisma.settings.findFirst(); + if (!settings || !settings.githubRepoUrl) { + return res.status(400).json({ error: 'GitHub repository URL not configured' }); + } + + // Extract owner and repo from GitHub URL + // Support both SSH and HTTPS formats: + // git@github.com:owner/repo.git + // https://github.com/owner/repo.git + const repoUrl = settings.githubRepoUrl; + let owner, repo; + + if (repoUrl.includes('git@github.com:')) { + const match = repoUrl.match(/git@github\.com:([^\/]+)\/([^\/]+)\.git/); + if (match) { + [, owner, repo] = match; + } + } else if (repoUrl.includes('github.com/')) { + const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/); + if (match) { + [, owner, repo] = match; + } + } + + if (!owner || !repo) { + return res.status(400).json({ error: 'Invalid GitHub repository URL format' }); + } + + // Use SSH to fetch latest tag from private repository + const sshRepoUrl = `git@github.com:${owner}/${repo}.git`; + + try { + // Fetch the latest tag using SSH + const { stdout: latestTag } = await execAsync( + `git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^\\{\\}//'`, + { timeout: 10000 } + ); + + const latestVersion = latestTag.trim().replace('v', ''); // Remove 'v' prefix + const currentVersion = '1.2.3'; + + // Simple version comparison (assumes semantic versioning) + const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0; + + // Get additional tag information + let tagInfo = {}; + try { + const { stdout: tagDetails } = await execAsync( + `git ls-remote --tags ${sshRepoUrl} | grep "${latestTag.trim()}" | head -n 1`, + { timeout: 5000 } + ); + + // Extract commit hash and other details if needed + const parts = tagDetails.trim().split('\t'); + if (parts.length >= 2) { + tagInfo.commitHash = parts[0]; + } + } catch (tagDetailError) { + console.warn('Could not fetch tag details:', tagDetailError.message); + } + + res.json({ + currentVersion, + latestVersion, + isUpdateAvailable, + latestRelease: { + tagName: latestTag.trim(), + version: latestVersion, + commitHash: tagInfo.commitHash, + repository: `${owner}/${repo}`, + sshUrl: sshRepoUrl + } + }); + + } catch (sshError) { + console.error('SSH Git error:', sshError.message); + + // Check if it's a permission/access issue + if (sshError.message.includes('Permission denied') || sshError.message.includes('Host key verification failed')) { + return res.status(403).json({ + error: 'SSH access denied to repository', + suggestion: 'Ensure your SSH key is properly configured and has access to the repository. Check your ~/.ssh/config and known_hosts.' + }); + } + + if (sshError.message.includes('not found') || sshError.message.includes('does not exist')) { + return res.status(404).json({ + error: 'Repository not found', + suggestion: 'Check that the repository URL is correct and accessible.' + }); + } + + return res.status(500).json({ + error: 'Failed to fetch repository information', + details: sshError.message, + suggestion: 'Check SSH key configuration and repository access permissions.' + }); + } + + } catch (error) { + console.error('Error checking for updates:', error); + res.status(500).json({ + error: 'Failed to check for updates', + details: error.message + }); + } +}); + +// Simple version comparison function +function compareVersions(version1, version2) { + const v1Parts = version1.split('.').map(Number); + const v2Parts = version2.split('.').map(Number); + + const maxLength = Math.max(v1Parts.length, v2Parts.length); + + for (let i = 0; i < maxLength; i++) { + const v1Part = v1Parts[i] || 0; + const v2Part = v2Parts[i] || 0; + + if (v1Part > v2Part) return 1; + if (v1Part < v2Part) return -1; + } + + return 0; +} + +module.exports = router; diff --git a/backend/src/server.js b/backend/src/server.js index 5c4d95a..3d2df33 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -16,6 +16,7 @@ const permissionsRoutes = require('./routes/permissionsRoutes'); const settingsRoutes = require('./routes/settingsRoutes'); const dashboardPreferencesRoutes = require('./routes/dashboardPreferencesRoutes'); const repositoryRoutes = require('./routes/repositoryRoutes'); +const versionRoutes = require('./routes/versionRoutes'); // Initialize Prisma client const prisma = new PrismaClient(); @@ -134,6 +135,7 @@ app.use(`/api/${apiVersion}/permissions`, permissionsRoutes); app.use(`/api/${apiVersion}/settings`, settingsRoutes); app.use(`/api/${apiVersion}/dashboard-preferences`, dashboardPreferencesRoutes); app.use(`/api/${apiVersion}/repositories`, repositoryRoutes); +app.use(`/api/${apiVersion}/version`, versionRoutes); // Error handling middleware app.use((err, req, res, next) => { diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index bde3af6..b4780dc 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Save, Server, Globe, Shield, AlertCircle, CheckCircle, Code, Plus, Trash2, Star, Download, X, Settings as SettingsIcon } from 'lucide-react'; -import { settingsAPI, agentVersionAPI } from '../utils/api'; +import { settingsAPI, agentVersionAPI, versionAPI } from '../utils/api'; const Settings = () => { const [formData, setFormData] = useState({ @@ -37,6 +37,15 @@ const Settings = () => { isDefault: false }); + // Version checking state + const [versionInfo, setVersionInfo] = useState({ + currentVersion: '1.2.3', + latestVersion: null, + isUpdateAvailable: false, + checking: false, + error: null + }); + const queryClient = useQueryClient(); // Fetch current settings @@ -152,6 +161,31 @@ const Settings = () => { } }); + // Version checking functions + const checkForUpdates = async () => { + setVersionInfo(prev => ({ ...prev, checking: true, error: null })); + + try { + const response = await versionAPI.checkUpdates(); + const data = response.data; + + setVersionInfo({ + currentVersion: data.currentVersion, + latestVersion: data.latestVersion, + isUpdateAvailable: data.isUpdateAvailable, + checking: false, + error: null + }); + } catch (error) { + console.error('Version check error:', error); + setVersionInfo(prev => ({ + ...prev, + checking: false, + error: error.response?.data?.error || 'Failed to check for updates' + })); + } + }; + const handleInputChange = (field, value) => { console.log(`handleInputChange: ${field} = ${value}`); setFormData(prev => { @@ -694,7 +728,7 @@ const Settings = () => { Current Version - 1.2.3 + {versionInfo.currentVersion}
@@ -702,20 +736,29 @@ const Settings = () => { Latest Version
- Checking... + + {versionInfo.checking ? ( + Checking... + ) : versionInfo.latestVersion ? ( + + {versionInfo.latestVersion} + {versionInfo.isUpdateAvailable && ' (Update Available!)'} + + ) : ( + Not checked + )} +
+ + {versionInfo.error && ( +
+
+ +
+

Version Check Failed

+

+ {versionInfo.error} +

+ {versionInfo.error.includes('private') && ( +

+ For private repositories, you may need to configure GitHub authentication or make the repository public. +

+ )} +
+
+
+ )} -
-
- -
-

Setup Instructions

-
-

To enable version checking, you need to:

-
    -
  1. Create a version tag (e.g., v1.2.3) in your GitHub repository
  2. -
  3. Ensure the repository is publicly accessible or configure access tokens
  4. -
  5. Click "Check for Updates" to verify the connection
  6. -
-
-
-
-
)} diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 3453068..5281cdd 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -178,6 +178,12 @@ export const formatDate = (date) => { return new Date(date).toLocaleString() } +// Version API +export const versionAPI = { + getCurrent: () => api.get('/version/current'), + checkUpdates: () => api.get('/version/check-updates'), +} + export const formatRelativeTime = (date) => { const now = new Date() const diff = now - new Date(date)