diff --git a/package-lock.json b/package-lock.json index 9fc5868..9a527b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@siteboon/claude-code-ui", - "version": "1.10.3", + "version": "1.10.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@siteboon/claude-code-ui", - "version": "1.10.3", + "version": "1.10.4", "license": "MIT", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.29", diff --git a/server/index.js b/server/index.js index 8ab26d7..9a39a5d 100755 --- a/server/index.js +++ b/server/index.js @@ -237,6 +237,71 @@ app.get('/api/config', authenticateToken, (req, res) => { }); }); +// System update endpoint +app.post('/api/system/update', authenticateToken, async (req, res) => { + try { + // Get the project root directory (parent of server directory) + const projectRoot = path.join(__dirname, '..'); + + console.log('Starting system update from directory:', projectRoot); + + // Run the update command + const updateCommand = 'git checkout main && git pull && npm install'; + + const child = spawn('sh', ['-c', updateCommand], { + cwd: projectRoot, + env: process.env + }); + + let output = ''; + let errorOutput = ''; + + child.stdout.on('data', (data) => { + const text = data.toString(); + output += text; + console.log('Update output:', text); + }); + + child.stderr.on('data', (data) => { + const text = data.toString(); + errorOutput += text; + console.error('Update error:', text); + }); + + child.on('close', (code) => { + if (code === 0) { + res.json({ + success: true, + output: output || 'Update completed successfully', + message: 'Update completed. Please restart the server to apply changes.' + }); + } else { + res.status(500).json({ + success: false, + error: 'Update command failed', + output: output, + errorOutput: errorOutput + }); + } + }); + + child.on('error', (error) => { + console.error('Update process error:', error); + res.status(500).json({ + success: false, + error: error.message + }); + }); + + } catch (error) { + console.error('System update error:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + app.get('/api/projects', authenticateToken, async (req, res) => { try { const projects = await getProjects(); diff --git a/src/App.jsx b/src/App.jsx index d6aac09..c018dc3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -42,7 +42,7 @@ function AppContent() { const navigate = useNavigate(); const { sessionId } = useParams(); - const { updateAvailable, latestVersion, currentVersion } = useVersionCheck('siteboon', 'claudecodeui'); + const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui'); const [showVersionModal, setShowVersionModal] = useState(false); const [projects, setProjects] = useState([]); @@ -536,8 +536,60 @@ function AppContent() { // Version Upgrade Modal Component const VersionUpgradeModal = () => { + const [isUpdating, setIsUpdating] = useState(false); + const [updateOutput, setUpdateOutput] = useState(''); + const [updateError, setUpdateError] = useState(''); + if (!showVersionModal) return null; + // Clean up changelog by removing GitHub-specific metadata + const cleanChangelog = (body) => { + if (!body) return ''; + + return body + // Remove full commit hashes (40 character hex strings) + .replace(/\b[0-9a-f]{40}\b/gi, '') + // Remove short commit hashes (7-10 character hex strings at start of line or after dash/space) + .replace(/(?:^|\s|-)([0-9a-f]{7,10})\b/gi, '') + // Remove "Full Changelog" links + .replace(/\*\*Full Changelog\*\*:.*$/gim, '') + // Remove compare links (e.g., https://github.com/.../compare/v1.0.0...v1.0.1) + .replace(/https?:\/\/github\.com\/[^\/]+\/[^\/]+\/compare\/[^\s)]+/gi, '') + // Clean up multiple consecutive empty lines + .replace(/\n\s*\n\s*\n/g, '\n\n') + // Trim whitespace + .trim(); + }; + + const handleUpdateNow = async () => { + setIsUpdating(true); + setUpdateOutput('Starting update...\n'); + setUpdateError(''); + + try { + // Call the backend API to run the update command + const response = await authenticatedFetch('/api/system/update', { + method: 'POST', + }); + + const data = await response.json(); + + if (response.ok) { + setUpdateOutput(prev => prev + data.output + '\n'); + setUpdateOutput(prev => prev + '\n✅ Update completed successfully!\n'); + setUpdateOutput(prev => prev + 'Please restart the server to apply changes.\n'); + } else { + setUpdateError(data.error || 'Update failed'); + setUpdateOutput(prev => prev + '\n❌ Update failed: ' + (data.error || 'Unknown error') + '\n'); + } + } catch (error) { + setUpdateError(error.message); + setUpdateOutput(prev => prev + '\n❌ Update failed: ' + error.message + '\n'); + } finally { + setIsUpdating(false); + } + }; + return (
{/* Backdrop */} @@ -546,9 +598,9 @@ function AppContent() { onClick={() => setShowVersionModal(false)} aria-label="Close version upgrade modal" /> - + {/* Modal */} -
+
{/* Header */}
@@ -559,7 +611,9 @@ function AppContent() {

Update Available

-

A new version is ready

+

+ {releaseInfo?.title || 'A new version is ready'} +

- {/* Upgrade Instructions */} -
-

How to upgrade:

-
- - git checkout main && git pull && npm install - + {/* Changelog */} + {releaseInfo?.body && ( +
+
+

What's New:

+ {releaseInfo?.htmlUrl && ( + + View full release + + + + + )} +
+
+
+ {cleanChangelog(releaseInfo.body)} +
+
-

- Run this command in your Claude Code UI directory to update to the latest version. -

-
+ )} + + {/* Update Output */} + {updateOutput && ( +
+

Update Progress:

+
+
{updateOutput}
+
+
+ )} + + {/* Upgrade Instructions */} + {!isUpdating && !updateOutput && ( +
+

Manual upgrade:

+
+ + git checkout main && git pull && npm install + +
+

+ Or click "Update Now" to run the update automatically. +

+
+ )} {/* Actions */}
@@ -603,18 +696,34 @@ function AppContent() { onClick={() => setShowVersionModal(false)} className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors" > - Later - - + {!updateOutput && ( + <> + + + + )}
@@ -642,6 +751,7 @@ function AppContent() { updateAvailable={updateAvailable} latestVersion={latestVersion} currentVersion={currentVersion} + releaseInfo={releaseInfo} onShowVersionModal={() => setShowVersionModal(true)} isPWA={isPWA} isMobile={isMobile} @@ -691,6 +801,7 @@ function AppContent() { updateAvailable={updateAvailable} latestVersion={latestVersion} currentVersion={currentVersion} + releaseInfo={releaseInfo} onShowVersionModal={() => setShowVersionModal(true)} isPWA={isPWA} isMobile={isMobile} diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index dc026f4..19970ea 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -39,12 +39,12 @@ const formatTimeAgo = (dateString, currentTime) => { return date.toLocaleDateString(); }; -function Sidebar({ - projects, - selectedProject, - selectedSession, - onProjectSelect, - onSessionSelect, +function Sidebar({ + projects, + selectedProject, + selectedSession, + onProjectSelect, + onSessionSelect, onNewSession, onSessionDelete, onProjectDelete, @@ -54,6 +54,7 @@ function Sidebar({ updateAvailable, latestVersion, currentVersion, + releaseInfo, onShowVersionModal, isPWA, isMobile @@ -1611,8 +1612,10 @@ function Sidebar({
-
Update Available
-
Version {latestVersion} is ready
+
+ {releaseInfo?.title || `Version ${latestVersion}`} +
+
Update available
@@ -1630,8 +1633,10 @@ function Sidebar({
-
Update Available
-
Version {latestVersion} is ready
+
+ {releaseInfo?.title || `Version ${latestVersion}`} +
+
Update available
diff --git a/src/hooks/useVersionCheck.js b/src/hooks/useVersionCheck.js index 78dc926..5951537 100644 --- a/src/hooks/useVersionCheck.js +++ b/src/hooks/useVersionCheck.js @@ -5,28 +5,39 @@ import { version } from '../../package.json'; export const useVersionCheck = (owner, repo) => { const [updateAvailable, setUpdateAvailable] = useState(false); const [latestVersion, setLatestVersion] = useState(null); + const [releaseInfo, setReleaseInfo] = useState(null); useEffect(() => { const checkVersion = async () => { try { const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`); const data = await response.json(); - + // Handle the case where there might not be any releases if (data.tag_name) { const latest = data.tag_name.replace(/^v/, ''); setLatestVersion(latest); setUpdateAvailable(version !== latest); + + // Store release information + setReleaseInfo({ + title: data.name || data.tag_name, + body: data.body || '', + htmlUrl: data.html_url || `https://github.com/${owner}/${repo}/releases/latest`, + publishedAt: data.published_at + }); } else { // No releases found, don't show update notification setUpdateAvailable(false); setLatestVersion(null); + setReleaseInfo(null); } } catch (error) { console.error('Version check failed:', error); // On error, don't show update notification setUpdateAvailable(false); setLatestVersion(null); + setReleaseInfo(null); } }; @@ -35,5 +46,5 @@ export const useVersionCheck = (owner, repo) => { return () => clearInterval(interval); }, [owner, repo]); - return { updateAvailable, latestVersion, currentVersion: version }; + return { updateAvailable, latestVersion, currentVersion: version, releaseInfo }; }; \ No newline at end of file