From ec559c1ed3be96d4a9e18db3d8ecda0b62db6394 Mon Sep 17 00:00:00 2001 From: AJ Date: Mon, 30 Mar 2026 08:17:42 -0700 Subject: [PATCH 1/7] feat: make application base path configurable via BASE_PATH env var Migrate all routes, asset references, and internal links from the hardcoded root path `/` to a configurable base path controlled by the BASE_PATH environment variable. Defaults to `/` when the variable is not set. What changed: - Introduced BASE_PATH env var to drive route prefix resolution - Updated all route definitions, static asset references, and internal links to resolve under the configured prefix - Set / as the default base path Why: - Enables the app to coexist behind a reverse proxy or within a larger platform without path collisions - Simplifies multi-tenant and namespace-isolated deployments - Eliminates the need for code changes when the hosting path shifts --- .env.example | 5 + index.html | 15 --- public/manifest.json | 22 ++-- public/sw.js | 18 +-- server/index.js | 108 ++++++++++-------- src/App.tsx | 3 +- .../llm-logo-provider/ClaudeLogo.tsx | 3 +- .../llm-logo-provider/CodexLogo.tsx | 3 +- .../llm-logo-provider/CursorLogo.tsx | 3 +- .../llm-logo-provider/GeminiLogo.tsx | 4 +- .../data/workspaceApi.ts | 4 +- src/components/shell/utils/socket.ts | 5 +- src/contexts/WebSocketContext.tsx | 5 +- src/hooks/useVersionCheck.ts | 3 +- src/main.jsx | 2 +- src/types/global.d.ts | 1 - src/utils/api.js | 21 +++- vite.config.js | 20 +++- 18 files changed, 138 insertions(+), 107 deletions(-) diff --git a/.env.example b/.env.example index 7e1d124c7..49ceda5eb 100755 --- a/.env.example +++ b/.env.example @@ -25,6 +25,11 @@ VITE_PORT=5173 # Use 127.0.0.1 to restrict to localhost only HOST=0.0.0.0 +# Base path prefix for all routes and assets (default: /) +# Useful when serving behind a reverse proxy at a sub-path. +# Requires a frontend rebuild (npm run build) after changing. +# BASE_PATH=/cloudcli + # Uncomment the following line if you have a custom claude cli path other than the default "claude" # CLAUDE_CLI_PATH=claude diff --git a/index.html b/index.html index c476f13e4..fe3046be9 100644 --- a/index.html +++ b/index.html @@ -29,20 +29,5 @@
- - - diff --git a/public/manifest.json b/public/manifest.json index fd4113f21..4f5e4617a 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -2,60 +2,60 @@ "name": "CloudCLI UI", "short_name": "CloudCLI UI", "description": "CloudCLI UI web application", - "start_url": "/", + "start_url": "./", "display": "standalone", "background_color": "#ffffff", "theme_color": "#ffffff", "orientation": "portrait-primary", - "scope": "/", + "scope": "./", "icons": [ { - "src": "/icons/icon-72x72.png", + "src": "icons/icon-72x72.png", "sizes": "72x72", "type": "image/png", "purpose": "maskable any" }, { - "src": "/icons/icon-96x96.png", + "src": "icons/icon-96x96.png", "sizes": "96x96", "type": "image/png", "purpose": "maskable any" }, { - "src": "/icons/icon-128x128.png", + "src": "icons/icon-128x128.png", "sizes": "128x128", "type": "image/png", "purpose": "maskable any" }, { - "src": "/icons/icon-144x144.png", + "src": "icons/icon-144x144.png", "sizes": "144x144", "type": "image/png", "purpose": "maskable any" }, { - "src": "/icons/icon-152x152.png", + "src": "icons/icon-152x152.png", "sizes": "152x152", "type": "image/png", "purpose": "maskable any" }, { - "src": "/icons/icon-192x192.png", + "src": "icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable any" }, { - "src": "/icons/icon-384x384.png", + "src": "icons/icon-384x384.png", "sizes": "384x384", "type": "image/png", "purpose": "maskable any" }, { - "src": "/icons/icon-512x512.png", + "src": "icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable any" } ] -} \ No newline at end of file +} diff --git a/public/sw.js b/public/sw.js index 9b4351bc7..561ab50e4 100755 --- a/public/sw.js +++ b/public/sw.js @@ -1,9 +1,13 @@ // Service Worker for Claude Code UI PWA // Cache only manifest (needed for PWA install). HTML and JS are never pre-cached // so a rebuild + refresh always picks up the latest assets. + +// Derive base path from service worker URL (e.g. /prefix/sw.js → /prefix) +const BASE_PATH = new URL('.', self.location).pathname.replace(/\/$/, ''); + const CACHE_NAME = 'claude-ui-v2'; const urlsToCache = [ - '/manifest.json' + `${BASE_PATH}/manifest.json` ]; // Install event @@ -20,14 +24,14 @@ self.addEventListener('fetch', event => { const url = event.request.url; // Never intercept API requests or WebSocket upgrades - if (url.includes('/api/') || url.includes('/ws')) { + if (url.includes(`${BASE_PATH}/api/`) || url.includes(`${BASE_PATH}/ws`)) { return; } // Navigation requests (HTML) — always go to network, no caching if (event.request.mode === 'navigate') { event.respondWith( - fetch(event.request).catch(() => caches.match('/manifest.json').then(() => + fetch(event.request).catch(() => caches.match(`${BASE_PATH}/manifest.json`).then(() => new Response('

Offline

Please check your connection.

', { headers: { 'Content-Type': 'text/html' } }) @@ -37,7 +41,7 @@ self.addEventListener('fetch', event => { } // Hashed assets (JS/CSS in /assets/) — cache-first since filenames change per build - if (url.includes('/assets/')) { + if (url.includes(`${BASE_PATH}/assets/`)) { event.respondWith( caches.match(event.request).then(cached => { if (cached) return cached; @@ -84,8 +88,8 @@ self.addEventListener('push', event => { const options = { body: payload.body || '', - icon: '/logo-256.png', - badge: '/logo-128.png', + icon: `${BASE_PATH}/logo-256.png`, + badge: `${BASE_PATH}/logo-128.png`, data: payload.data || {}, tag: payload.data?.tag || `${payload.data?.sessionId || 'global'}:${payload.data?.code || 'default'}`, renotify: true @@ -102,7 +106,7 @@ self.addEventListener('notificationclick', event => { const sessionId = event.notification.data?.sessionId; const provider = event.notification.data?.provider || null; - const urlPath = sessionId ? `/session/${sessionId}` : '/'; + const urlPath = sessionId ? `${BASE_PATH}/session/${sessionId}` : `${BASE_PATH}/`; event.waitUntil( self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(async clientList => { diff --git a/server/index.js b/server/index.js index 5318fb359..01951cbd3 100755 --- a/server/index.js +++ b/server/index.js @@ -344,8 +344,11 @@ app.use(express.json({ })); app.use(express.urlencoded({ limit: '50mb', extended: true })); +// Application base path — configurable via BASE_PATH env var, defaults to / +const BASE_PATH = (process.env.BASE_PATH || '/').replace(/\/+$/, ''); + // Public health check endpoint (no authentication required) -app.get('/health', (req, res) => { +app.get(`${BASE_PATH}/health`, (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), @@ -353,63 +356,68 @@ app.get('/health', (req, res) => { }); }); +// Redirect root to base path (only when a non-root base path is configured) +if (BASE_PATH) { + app.get('/', (req, res) => res.redirect(`${BASE_PATH}/`)); +} + // Optional API key validation (if configured) -app.use('/api', validateApiKey); +app.use(`${BASE_PATH}/api`, validateApiKey); // Authentication routes (public) -app.use('/api/auth', authRoutes); +app.use(`${BASE_PATH}/api/auth`, authRoutes); // Projects API Routes (protected) -app.use('/api/projects', authenticateToken, projectsRoutes); +app.use(`${BASE_PATH}/api/projects`, authenticateToken, projectsRoutes); // Git API Routes (protected) -app.use('/api/git', authenticateToken, gitRoutes); +app.use(`${BASE_PATH}/api/git`, authenticateToken, gitRoutes); // MCP API Routes (protected) -app.use('/api/mcp', authenticateToken, mcpRoutes); +app.use(`${BASE_PATH}/api/mcp`, authenticateToken, mcpRoutes); // Cursor API Routes (protected) -app.use('/api/cursor', authenticateToken, cursorRoutes); +app.use(`${BASE_PATH}/api/cursor`, authenticateToken, cursorRoutes); // TaskMaster API Routes (protected) -app.use('/api/taskmaster', authenticateToken, taskmasterRoutes); +app.use(`${BASE_PATH}/api/taskmaster`, authenticateToken, taskmasterRoutes); // MCP utilities -app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes); +app.use(`${BASE_PATH}/api/mcp-utils`, authenticateToken, mcpUtilsRoutes); // Commands API Routes (protected) -app.use('/api/commands', authenticateToken, commandsRoutes); +app.use(`${BASE_PATH}/api/commands`, authenticateToken, commandsRoutes); // Settings API Routes (protected) -app.use('/api/settings', authenticateToken, settingsRoutes); +app.use(`${BASE_PATH}/api/settings`, authenticateToken, settingsRoutes); // CLI Authentication API Routes (protected) -app.use('/api/cli', authenticateToken, cliAuthRoutes); +app.use(`${BASE_PATH}/api/cli`, authenticateToken, cliAuthRoutes); // User API Routes (protected) -app.use('/api/user', authenticateToken, userRoutes); +app.use(`${BASE_PATH}/api/user`, authenticateToken, userRoutes); // Codex API Routes (protected) -app.use('/api/codex', authenticateToken, codexRoutes); +app.use(`${BASE_PATH}/api/codex`, authenticateToken, codexRoutes); // Gemini API Routes (protected) -app.use('/api/gemini', authenticateToken, geminiRoutes); +app.use(`${BASE_PATH}/api/gemini`, authenticateToken, geminiRoutes); // Plugins API Routes (protected) -app.use('/api/plugins', authenticateToken, pluginsRoutes); +app.use(`${BASE_PATH}/api/plugins`, authenticateToken, pluginsRoutes); // Unified session messages route (protected) -app.use('/api/sessions', authenticateToken, messagesRoutes); +app.use(`${BASE_PATH}/api/sessions`, authenticateToken, messagesRoutes); // Agent API Routes (uses API key authentication) -app.use('/api/agent', agentRoutes); +app.use(`${BASE_PATH}/api/agent`, agentRoutes); // Serve public files (like api-docs.html) -app.use(express.static(path.join(__dirname, '../public'))); +app.use(`${BASE_PATH}`, express.static(path.join(__dirname, '../public'))); // Static files served after API routes // Add cache control: HTML files should not be cached, but assets can be cached -app.use(express.static(path.join(__dirname, '../dist'), { +app.use(`${BASE_PATH}`, express.static(path.join(__dirname, '../dist'), { setHeaders: (res, filePath) => { if (filePath.endsWith('.html')) { // Prevent HTML caching to avoid service worker issues after builds @@ -428,7 +436,7 @@ app.use(express.static(path.join(__dirname, '../dist'), { // Frontend now uses window.location for WebSocket URLs // System update endpoint -app.post('/api/system/update', authenticateToken, async (req, res) => { +app.post(`${BASE_PATH}/api/system/update`, authenticateToken, async (req, res) => { try { // Get the project root directory (parent of server directory) const projectRoot = path.join(__dirname, '..'); @@ -494,7 +502,7 @@ app.post('/api/system/update', authenticateToken, async (req, res) => { } }); -app.get('/api/projects', authenticateToken, async (req, res) => { +app.get(`${BASE_PATH}/api/projects`, authenticateToken, async (req, res) => { try { const projects = await getProjects(broadcastProgress); res.json(projects); @@ -503,7 +511,7 @@ app.get('/api/projects', authenticateToken, async (req, res) => { } }); -app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => { +app.get(`${BASE_PATH}/api/projects/:projectName/sessions`, authenticateToken, async (req, res) => { try { const { limit = 5, offset = 0 } = req.query; const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset)); @@ -515,7 +523,7 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re }); // Rename project endpoint -app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => { +app.put(`${BASE_PATH}/api/projects/:projectName/rename`, authenticateToken, async (req, res) => { try { const { displayName } = req.body; await renameProject(req.params.projectName, displayName); @@ -526,7 +534,7 @@ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) }); // Delete session endpoint -app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => { +app.delete(`${BASE_PATH}/api/projects/:projectName/sessions/:sessionId`, authenticateToken, async (req, res) => { try { const { projectName, sessionId } = req.params; console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`); @@ -541,7 +549,7 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, }); // Rename session endpoint -app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) => { +app.put(`${BASE_PATH}/api/sessions/:sessionId/rename`, authenticateToken, async (req, res) => { try { const { sessionId } = req.params; const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, ''); @@ -567,7 +575,7 @@ app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) = }); // Delete project endpoint (force=true to delete with sessions) -app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => { +app.delete(`${BASE_PATH}/api/projects/:projectName`, authenticateToken, async (req, res) => { try { const { projectName } = req.params; const force = req.query.force === 'true'; @@ -579,7 +587,7 @@ app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => }); // Create project endpoint -app.post('/api/projects/create', authenticateToken, async (req, res) => { +app.post(`${BASE_PATH}/api/projects/create`, authenticateToken, async (req, res) => { try { const { path: projectPath } = req.body; @@ -596,7 +604,7 @@ app.post('/api/projects/create', authenticateToken, async (req, res) => { }); // Search conversations content (SSE streaming) -app.get('/api/search/conversations', authenticateToken, async (req, res) => { +app.get(`${BASE_PATH}/api/search/conversations`, authenticateToken, async (req, res) => { const query = typeof req.query.q === 'string' ? req.query.q.trim() : ''; const parsedLimit = Number.parseInt(String(req.query.limit), 10); const limit = Number.isNaN(parsedLimit) ? 50 : Math.max(1, Math.min(parsedLimit, 100)); @@ -652,7 +660,7 @@ const expandWorkspacePath = (inputPath) => { }; // Browse filesystem endpoint for project suggestions - uses existing getFileTree -app.get('/api/browse-filesystem', authenticateToken, async (req, res) => { +app.get(`${BASE_PATH}/api/browse-filesystem`, authenticateToken, async (req, res) => { try { const { path: dirPath } = req.query; @@ -732,7 +740,7 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => { } }); -app.post('/api/create-folder', authenticateToken, async (req, res) => { +app.post(`${BASE_PATH}/api/create-folder`, authenticateToken, async (req, res) => { try { const { path: folderPath } = req.body; if (!folderPath) { @@ -773,7 +781,7 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => { }); // Read file content endpoint -app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => { +app.get(`${BASE_PATH}/api/projects/:projectName/file`, authenticateToken, async (req, res) => { try { const { projectName } = req.params; const { filePath } = req.query; @@ -813,7 +821,7 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) = }); // Serve binary file content endpoint (for images, etc.) -app.get('/api/projects/:projectName/files/content', authenticateToken, async (req, res) => { +app.get(`${BASE_PATH}/api/projects/:projectName/files/content`, authenticateToken, async (req, res) => { try { const { projectName } = req.params; const { path: filePath } = req.query; @@ -866,7 +874,7 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re }); // Save file content endpoint -app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => { +app.put(`${BASE_PATH}/api/projects/:projectName/file`, authenticateToken, async (req, res) => { try { const { projectName } = req.params; const { filePath, content } = req.body; @@ -915,7 +923,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) = } }); -app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => { +app.get(`${BASE_PATH}/api/projects/:projectName/files`, authenticateToken, async (req, res) => { try { // Using fsPromises from import @@ -993,7 +1001,7 @@ function validateFilename(name) { } // POST /api/projects/:projectName/files/create - Create new file or directory -app.post('/api/projects/:projectName/files/create', authenticateToken, async (req, res) => { +app.post(`${BASE_PATH}/api/projects/:projectName/files/create`, authenticateToken, async (req, res) => { try { const { projectName } = req.params; const { path: parentPath, type, name } = req.body; @@ -1070,7 +1078,7 @@ app.post('/api/projects/:projectName/files/create', authenticateToken, async (re }); // PUT /api/projects/:projectName/files/rename - Rename file or directory -app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req, res) => { +app.put(`${BASE_PATH}/api/projects/:projectName/files/rename`, authenticateToken, async (req, res) => { try { const { projectName } = req.params; const { oldPath, newName } = req.body; @@ -1147,7 +1155,7 @@ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req }); // DELETE /api/projects/:projectName/files - Delete file or directory -app.delete('/api/projects/:projectName/files', authenticateToken, async (req, res) => { +app.delete(`${BASE_PATH}/api/projects/:projectName/files`, authenticateToken, async (req, res) => { try { const { projectName } = req.params; const { path: targetPath, type } = req.body; @@ -1373,14 +1381,14 @@ const uploadFilesHandler = async (req, res) => { }); }; -app.post('/api/projects/:projectName/files/upload', authenticateToken, uploadFilesHandler); +app.post(`${BASE_PATH}/api/projects/:projectName/files/upload`, authenticateToken, uploadFilesHandler); /** * Proxy an authenticated client WebSocket to a plugin's internal WS server. * Auth is enforced by verifyClient before this function is reached. */ function handlePluginWsProxy(clientWs, pathname) { - const pluginName = pathname.replace('/plugin-ws/', ''); + const pluginName = pathname.replace(`${BASE_PATH}/plugin-ws/`, ''); if (!pluginName || /[^a-zA-Z0-9_-]/.test(pluginName)) { clientWs.close(4400, 'Invalid plugin name'); return; @@ -1428,11 +1436,11 @@ wss.on('connection', (ws, request) => { const urlObj = new URL(url, 'http://localhost'); const pathname = urlObj.pathname; - if (pathname === '/shell') { + if (pathname === `${BASE_PATH}/shell`) { handleShellConnection(ws); - } else if (pathname === '/ws') { + } else if (pathname === `${BASE_PATH}/ws`) { handleChatConnection(ws, request); - } else if (pathname.startsWith('/plugin-ws/')) { + } else if (pathname.startsWith(`${BASE_PATH}/plugin-ws/`)) { handlePluginWsProxy(ws, pathname); } else { console.log('[WARN] Unknown WebSocket path:', pathname); @@ -1981,7 +1989,7 @@ function handleShellConnection(ws) { }); } // Audio transcription endpoint -app.post('/api/transcribe', authenticateToken, async (req, res) => { +app.post(`${BASE_PATH}/api/transcribe`, authenticateToken, async (req, res) => { try { const multer = (await import('multer')).default; const upload = multer({ storage: multer.memoryStorage() }); @@ -2130,7 +2138,7 @@ Agent instructions:`; }); // Image upload endpoint -app.post('/api/projects/:projectName/upload-images', authenticateToken, async (req, res) => { +app.post(`${BASE_PATH}/api/projects/:projectName/upload-images`, authenticateToken, async (req, res) => { try { const multer = (await import('multer')).default; const path = (await import('path')).default; @@ -2215,7 +2223,7 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r }); // Get token usage for a specific session -app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => { +app.get(`${BASE_PATH}/api/projects/:projectName/sessions/:sessionId/token-usage`, authenticateToken, async (req, res) => { try { const { projectName, sessionId } = req.params; const { provider = 'claude' } = req.query; @@ -2402,8 +2410,8 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica } }); -// Serve React app for all other routes (excluding static files) -app.get('*', (req, res) => { +// Serve React app for all other routes under BASE_PATH (excluding static files) +app.get(`${BASE_PATH}/*`, (req, res) => { // Skip requests for static assets (files with extensions) if (path.extname(req.path)) { return res.status(404).send('Not found'); @@ -2534,10 +2542,10 @@ async function startServer() { console.log(''); if (isProduction) { - console.log(`${c.info('[INFO]')} To run in production mode, go to http://${DISPLAY_HOST}:${SERVER_PORT}`); + console.log(`${c.info('[INFO]')} To run in production mode, go to http://${DISPLAY_HOST}:${SERVER_PORT}${BASE_PATH}/`); } - console.log(`${c.info('[INFO]')} To run in development mode with hot-module replacement, go to http://${DISPLAY_HOST}:${VITE_PORT}`); + console.log(`${c.info('[INFO]')} To run in development mode with hot-module replacement, go to http://${DISPLAY_HOST}:${VITE_PORT}${BASE_PATH}/`); server.listen(SERVER_PORT, HOST, async () => { const appInstallPath = path.join(__dirname, '..'); diff --git a/src/App.tsx b/src/App.tsx index bcbda8261..52fe966ff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { WebSocketProvider } from './contexts/WebSocketContext'; import { PluginsProvider } from './contexts/PluginsContext'; import AppContent from './components/app/AppContent'; import i18n from './i18n/config.js'; +import { BASE_PATH } from './utils/api'; export default function App() { return ( @@ -19,7 +20,7 @@ export default function App() { - + } /> } /> diff --git a/src/components/llm-logo-provider/ClaudeLogo.tsx b/src/components/llm-logo-provider/ClaudeLogo.tsx index d15a07110..4572ae905 100644 --- a/src/components/llm-logo-provider/ClaudeLogo.tsx +++ b/src/components/llm-logo-provider/ClaudeLogo.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { BASE_PATH } from '../../utils/api'; type ClaudeLogoProps = { className?: string; @@ -6,7 +7,7 @@ type ClaudeLogoProps = { const ClaudeLogo = ({ className = 'w-5 h-5' }: ClaudeLogoProps) => { return ( - Claude + Claude ); }; diff --git a/src/components/llm-logo-provider/CodexLogo.tsx b/src/components/llm-logo-provider/CodexLogo.tsx index 0c3a65f0a..097aa3a30 100644 --- a/src/components/llm-logo-provider/CodexLogo.tsx +++ b/src/components/llm-logo-provider/CodexLogo.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useTheme } from '../../contexts/ThemeContext'; +import { BASE_PATH } from '../../utils/api'; type CodexLogoProps = { className?: string; @@ -10,7 +11,7 @@ const CodexLogo = ({ className = 'w-5 h-5' }: CodexLogoProps) => { return ( Codex diff --git a/src/components/llm-logo-provider/CursorLogo.tsx b/src/components/llm-logo-provider/CursorLogo.tsx index a44064aca..a0033f962 100644 --- a/src/components/llm-logo-provider/CursorLogo.tsx +++ b/src/components/llm-logo-provider/CursorLogo.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useTheme } from '../../contexts/ThemeContext'; +import { BASE_PATH } from '../../utils/api'; type CursorLogoProps = { className?: string; @@ -10,7 +11,7 @@ const CursorLogo = ({ className = 'w-5 h-5' }: CursorLogoProps) => { return ( Cursor diff --git a/src/components/llm-logo-provider/GeminiLogo.tsx b/src/components/llm-logo-provider/GeminiLogo.tsx index 9954dd783..e27c7e739 100644 --- a/src/components/llm-logo-provider/GeminiLogo.tsx +++ b/src/components/llm-logo-provider/GeminiLogo.tsx @@ -1,6 +1,8 @@ +import { BASE_PATH } from '../../utils/api'; + const GeminiLogo = ({className = 'w-5 h-5'}) => { return ( - Gemini + Gemini ); }; diff --git a/src/components/project-creation-wizard/data/workspaceApi.ts b/src/components/project-creation-wizard/data/workspaceApi.ts index f4ca3baff..c38f7c390 100644 --- a/src/components/project-creation-wizard/data/workspaceApi.ts +++ b/src/components/project-creation-wizard/data/workspaceApi.ts @@ -1,4 +1,4 @@ -import { api } from '../../../utils/api'; +import { api, BASE_PATH } from '../../../utils/api'; import type { BrowseFilesystemResponse, CloneProgressEvent, @@ -110,7 +110,7 @@ export const cloneWorkspaceWithProgress = ( ) => new Promise | undefined>((resolve, reject) => { const query = buildCloneProgressQuery(params); - const eventSource = new EventSource(`/api/projects/clone-progress?${query}`); + const eventSource = new EventSource(`${BASE_PATH}/api/projects/clone-progress?${query}`); let settled = false; const settle = (callback: () => void) => { diff --git a/src/components/shell/utils/socket.ts b/src/components/shell/utils/socket.ts index 6cb18d626..201989ec3 100644 --- a/src/components/shell/utils/socket.ts +++ b/src/components/shell/utils/socket.ts @@ -1,11 +1,12 @@ import { IS_PLATFORM } from '../../../constants/config'; +import { BASE_PATH } from '../../../utils/api'; import type { ShellIncomingMessage, ShellOutgoingMessage } from '../types/types'; export function getShellWebSocketUrl(): string | null { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; if (IS_PLATFORM) { - return `${protocol}//${window.location.host}/shell`; + return `${protocol}//${window.location.host}${BASE_PATH}/shell`; } const token = localStorage.getItem('auth-token'); @@ -14,7 +15,7 @@ export function getShellWebSocketUrl(): string | null { return null; } - return `${protocol}//${window.location.host}/shell?token=${encodeURIComponent(token)}`; + return `${protocol}//${window.location.host}${BASE_PATH}/shell?token=${encodeURIComponent(token)}`; } export function parseShellMessage(payload: string): ShellIncomingMessage | null { diff --git a/src/contexts/WebSocketContext.tsx b/src/contexts/WebSocketContext.tsx index 116da6b96..9fcfb45ea 100644 --- a/src/contexts/WebSocketContext.tsx +++ b/src/contexts/WebSocketContext.tsx @@ -1,6 +1,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useAuth } from '../components/auth/context/AuthContext'; import { IS_PLATFORM } from '../constants/config'; +import { BASE_PATH } from '../utils/api'; type WebSocketContextType = { ws: WebSocket | null; @@ -21,9 +22,9 @@ export const useWebSocket = () => { const buildWebSocketUrl = (token: string | null) => { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - if (IS_PLATFORM) return `${protocol}//${window.location.host}/ws`; // Platform mode: Use same domain as the page (goes through proxy) + if (IS_PLATFORM) return `${protocol}//${window.location.host}${BASE_PATH}/ws`; // Platform mode: Use same domain as the page (goes through proxy) if (!token) return null; - return `${protocol}//${window.location.host}/ws?token=${encodeURIComponent(token)}`; // OSS mode: Use same host:port that served the page + return `${protocol}//${window.location.host}${BASE_PATH}/ws?token=${encodeURIComponent(token)}`; // OSS mode: Use same host:port that served the page }; const useWebSocketProviderState = (): WebSocketContextType => { diff --git a/src/hooks/useVersionCheck.ts b/src/hooks/useVersionCheck.ts index ba3a894f6..2fa1ad2a4 100644 --- a/src/hooks/useVersionCheck.ts +++ b/src/hooks/useVersionCheck.ts @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { version } from '../../package.json'; import { ReleaseInfo } from '../types/sharedTypes'; +import { BASE_PATH } from '../utils/api'; /** * Compare two semantic version strings @@ -32,7 +33,7 @@ export const useVersionCheck = (owner: string, repo: string) => { useEffect(() => { const fetchInstallMode = async () => { try { - const response = await fetch('/health'); + const response = await fetch(`${BASE_PATH}/health`); const data = await response.json(); if (data.installMode === 'npm' || data.installMode === 'git') { setInstallMode(data.installMode); diff --git a/src/main.jsx b/src/main.jsx index 0c88aea63..ccf4d6689 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -9,7 +9,7 @@ import './i18n/config.js' // Register service worker for PWA + Web Push support if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('/sw.js').catch(err => { + navigator.serviceWorker.register(`${import.meta.env.BASE_URL}sw.js`).catch(err => { console.warn('Service worker registration failed:', err); }); } diff --git a/src/types/global.d.ts b/src/types/global.d.ts index ba368e4b9..8c5bbdae7 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -2,7 +2,6 @@ export {}; declare global { interface Window { - __ROUTER_BASENAME__?: string; refreshProjects?: () => void | Promise; openSettings?: (tab?: string) => void; } diff --git a/src/utils/api.js b/src/utils/api.js index a3292b218..9c0b1ab17 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -1,5 +1,16 @@ import { IS_PLATFORM } from "../constants/config"; +// Derived from Vite's base config (which reads the BASE_PATH env var, defaulting to /) +export const BASE_PATH = import.meta.env.BASE_URL.replace(/\/+$/, ''); + +// Prefix relative URLs with the application base path +const prefixUrl = (url) => { + if (typeof url === 'string' && url.startsWith('/')) { + return `${BASE_PATH}${url}`; + } + return url; +}; + // Utility function for authenticated API calls export const authenticatedFetch = (url, options = {}) => { const token = localStorage.getItem('auth-token'); @@ -15,7 +26,7 @@ export const authenticatedFetch = (url, options = {}) => { defaultHeaders['Authorization'] = `Bearer ${token}`; } - return fetch(url, { + return fetch(prefixUrl(url), { ...options, headers: { ...defaultHeaders, @@ -34,13 +45,13 @@ export const authenticatedFetch = (url, options = {}) => { export const api = { // Auth endpoints (no token required) auth: { - status: () => fetch('/api/auth/status'), - login: (username, password) => fetch('/api/auth/login', { + status: () => fetch(prefixUrl('/api/auth/status')), + login: (username, password) => fetch(prefixUrl('/api/auth/login'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }), - register: (username, password) => fetch('/api/auth/register', { + register: (username, password) => fetch(prefixUrl('/api/auth/register'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), @@ -97,7 +108,7 @@ export const api = { const token = localStorage.getItem('auth-token'); const params = new URLSearchParams({ q: query, limit: String(limit) }); if (token) params.set('token', token); - return `/api/search/conversations?${params.toString()}`; + return `${BASE_PATH}/api/search/conversations?${params.toString()}`; }, createProject: (path) => authenticatedFetch('/api/projects/create', { diff --git a/vite.config.js b/vite.config.js index 88c8e28ee..df36eca9c 100755 --- a/vite.config.js +++ b/vite.config.js @@ -17,20 +17,30 @@ export default defineConfig(({ mode }) => { // TODO: Remove support for legacy PORT variables in all locations in a future major release, leaving only SERVER_PORT. const serverPort = env.SERVER_PORT || env.PORT || 3001 + // Application base path — configurable via BASE_PATH env var, defaults to / + const basePath = (env.BASE_PATH || '/').replace(/\/+$/, '') + const base = `${basePath}/` + return { + base, plugins: [react()], server: { host, port: parseInt(env.VITE_PORT) || 5173, proxy: { - '/api': `http://${proxyHost}:${serverPort}`, - '/ws': { + [`${basePath}/api`]: { + target: `http://${proxyHost}:${serverPort}`, + rewrite: (path) => path + }, + [`${basePath}/ws`]: { target: `ws://${proxyHost}:${serverPort}`, - ws: true + ws: true, + rewrite: (path) => path }, - '/shell': { + [`${basePath}/shell`]: { target: `ws://${proxyHost}:${serverPort}`, - ws: true + ws: true, + rewrite: (path) => path } } }, From f9a3dc2dad1a5a717f6114b659e5e1b49545017d Mon Sep 17 00:00:00 2001 From: AJ Date: Mon, 30 Mar 2026 08:35:15 -0700 Subject: [PATCH 2/7] fix: restore service worker registration and __ROUTER_BASENAME__ type --- index.html | 18 ++++++++++++++++++ src/types/global.d.ts | 1 + 2 files changed, 19 insertions(+) diff --git a/index.html b/index.html index fe3046be9..2713d849b 100644 --- a/index.html +++ b/index.html @@ -29,5 +29,23 @@
+ + + diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 8c5bbdae7..ba368e4b9 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -2,6 +2,7 @@ export {}; declare global { interface Window { + __ROUTER_BASENAME__?: string; refreshProjects?: () => void | Promise; openSettings?: (tab?: string) => void; } From 2217bf2a9b0734d924b0cfee6d6070d0683f504d Mon Sep 17 00:00:00 2001 From: AJ Date: Mon, 30 Mar 2026 16:24:26 -0700 Subject: [PATCH 3/7] fix: guard manifest href and simplify sw offline fallback --- index.html | 3 ++- public/sw.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 2713d849b..43429bc0e 100644 --- a/index.html +++ b/index.html @@ -35,7 +35,8 @@ if ('serviceWorker' in navigator) { // Derive base path from the manifest link href which Vite transforms with the correct base var m = document.querySelector('link[rel="manifest"]'); - var base = m ? m.getAttribute('href').replace('/manifest.json', '') || '' : ''; + var manifestHref = m ? m.getAttribute('href') : ''; + var base = manifestHref ? manifestHref.replace(/\/manifest\.json(?:[?#].*)?$/, '') : ''; window.addEventListener('load', function() { navigator.serviceWorker.register(base + '/sw.js') .then(function(registration) { diff --git a/public/sw.js b/public/sw.js index 561ab50e4..23a6ec746 100755 --- a/public/sw.js +++ b/public/sw.js @@ -31,11 +31,11 @@ self.addEventListener('fetch', event => { // Navigation requests (HTML) — always go to network, no caching if (event.request.mode === 'navigate') { event.respondWith( - fetch(event.request).catch(() => caches.match(`${BASE_PATH}/manifest.json`).then(() => + fetch(event.request).catch(() => new Response('

Offline

Please check your connection.

', { headers: { 'Content-Type': 'text/html' } }) - )) + ) ); return; } From 41cd07c84e9971a208f72f13c610470ec7a2b036 Mon Sep 17 00:00:00 2001 From: AJ Date: Thu, 2 Apr 2026 17:24:30 -0700 Subject: [PATCH 4/7] fix: update logo path to use BASE_PATH for better configurability --- src/components/auth/view/SetupForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/auth/view/SetupForm.tsx b/src/components/auth/view/SetupForm.tsx index 9b9800742..38578b47e 100644 --- a/src/components/auth/view/SetupForm.tsx +++ b/src/components/auth/view/SetupForm.tsx @@ -1,5 +1,6 @@ import { useCallback, useState } from 'react'; import type { FormEvent } from 'react'; +import { BASE_PATH } from '../../../utils/api'; import { useAuth } from '../context/AuthContext'; import AuthErrorAlert from './AuthErrorAlert'; import AuthInputField from './AuthInputField'; @@ -85,7 +86,7 @@ export default function SetupForm() { title="Welcome to Claude Code UI" description="Set up your account to get started" footerText="This is a single-user system. Only one account can be created." - logo={CloudCLI} + logo={CloudCLI} >
Date: Fri, 3 Apr 2026 12:40:54 -0700 Subject: [PATCH 5/7] fix: normalize BASE_PATH and scope service worker cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ensure BASE_PATH always has a leading slash even when set without one (e.g. BASE_PATH=cloudcli → /cloudcli) in both server and vite config - Namespace service worker cache by BASE_PATH to prevent collisions on shared origins; only purge own prefix caches during activation Co-Authored-By: Claude Opus 4.6 (1M context) --- public/sw.js | 5 +++-- server/index.js | 6 +++++- vite.config.js | 6 +++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/public/sw.js b/public/sw.js index 23a6ec746..d7c297083 100755 --- a/public/sw.js +++ b/public/sw.js @@ -5,7 +5,8 @@ // Derive base path from service worker URL (e.g. /prefix/sw.js → /prefix) const BASE_PATH = new URL('.', self.location).pathname.replace(/\/$/, ''); -const CACHE_NAME = 'claude-ui-v2'; +const CACHE_PREFIX = 'claude-ui'; +const CACHE_NAME = `${CACHE_PREFIX}:${encodeURIComponent(BASE_PATH || '/')}:v2`; const urlsToCache = [ `${BASE_PATH}/manifest.json` ]; @@ -67,7 +68,7 @@ self.addEventListener('activate', event => { caches.keys().then(cacheNames => Promise.all( cacheNames - .filter(name => name !== CACHE_NAME) + .filter(name => name.startsWith(CACHE_PREFIX) && name !== CACHE_NAME) .map(name => caches.delete(name)) ) ) diff --git a/server/index.js b/server/index.js index 8b878994e..e99a95f96 100755 --- a/server/index.js +++ b/server/index.js @@ -345,7 +345,11 @@ app.use(express.json({ app.use(express.urlencoded({ limit: '50mb', extended: true })); // Application base path — configurable via BASE_PATH env var, defaults to / -const BASE_PATH = (process.env.BASE_PATH || '/').replace(/\/+$/, ''); +const rawBasePath = (process.env.BASE_PATH || '/').trim(); +const BASE_PATH = + !rawBasePath || rawBasePath === '/' + ? '' + : `/${rawBasePath.replace(/^\/+|\/+$/g, '')}`; // Public health check endpoint (no authentication required) app.get(`${BASE_PATH}/health`, (req, res) => { diff --git a/vite.config.js b/vite.config.js index df36eca9c..46b7b21b7 100755 --- a/vite.config.js +++ b/vite.config.js @@ -18,7 +18,11 @@ export default defineConfig(({ mode }) => { const serverPort = env.SERVER_PORT || env.PORT || 3001 // Application base path — configurable via BASE_PATH env var, defaults to / - const basePath = (env.BASE_PATH || '/').replace(/\/+$/, '') + const rawBasePath = (env.BASE_PATH || '/').trim() + const basePath = + rawBasePath === '/' + ? '' + : `/${rawBasePath.replace(/^\/+|\/+$/g, '')}` const base = `${basePath}/` return { From 15b61fba044903e2d38cab831e56d374cad9472f Mon Sep 17 00:00:00 2001 From: AJ Date: Sun, 5 Apr 2026 08:00:24 -0700 Subject: [PATCH 6/7] fix: support native addons in plugin installs and plugin WebSocket without base path Add npm rebuild step after npm install --ignore-scripts so native addons (e.g. node-pty) compile during plugin install/update. Add plugin-ws WebSocket proxy to Vite dev config and handle plugin-ws paths without BASE_PATH prefix in the Express WS router for third-party plugin compat. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/index.js | 9 ++++++--- server/utils/plugin-loader.js | 30 ++++++++++++++++++++++++++++-- vite.config.js | 15 ++++++++++++++- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/server/index.js b/server/index.js index e99a95f96..4fd81a721 100755 --- a/server/index.js +++ b/server/index.js @@ -1392,8 +1392,11 @@ app.post(`${BASE_PATH}/api/projects/:projectName/files/upload`, authenticateToke * Auth is enforced by verifyClient before this function is reached. */ function handlePluginWsProxy(clientWs, pathname) { - const pluginName = pathname.replace(`${BASE_PATH}/plugin-ws/`, ''); - if (!pluginName || /[^a-zA-Z0-9_-]/.test(pluginName)) { + // Extract plugin name — works with or without BASE_PATH prefix + // (third-party plugins may omit the base path in their WebSocket URLs) + const match = pathname.match(/\/plugin-ws\/([a-zA-Z0-9_-]+)/); + const pluginName = match?.[1]; + if (!pluginName) { clientWs.close(4400, 'Invalid plugin name'); return; } @@ -1444,7 +1447,7 @@ wss.on('connection', (ws, request) => { handleShellConnection(ws); } else if (pathname === `${BASE_PATH}/ws`) { handleChatConnection(ws, request); - } else if (pathname.startsWith(`${BASE_PATH}/plugin-ws/`)) { + } else if (pathname.startsWith(`${BASE_PATH}/plugin-ws/`) || pathname.startsWith('/plugin-ws/')) { handlePluginWsProxy(ws, pathname); } else { console.log('[WARN] Unknown WebSocket path:', pathname); diff --git a/server/utils/plugin-loader.js b/server/utils/plugin-loader.js index 9d91068f8..4d402dd6d 100644 --- a/server/utils/plugin-loader.js +++ b/server/utils/plugin-loader.js @@ -348,7 +348,22 @@ export function installPluginFromGit(url) { cleanupTemp(); return reject(new Error(`npm install for ${repoName} failed (exit code ${npmCode})`)); } - runBuildIfNeeded(tempDir, packageJsonPath, () => finalize(manifest), (err) => { cleanupTemp(); reject(err); }); + // Rebuild native addons that were skipped by --ignore-scripts + const rebuildProcess = spawn('npm', ['rebuild'], { + cwd: tempDir, + stdio: ['ignore', 'pipe', 'pipe'], + }); + rebuildProcess.on('close', (rebuildCode) => { + if (rebuildCode !== 0) { + cleanupTemp(); + return reject(new Error(`npm rebuild for ${repoName} failed (exit code ${rebuildCode})`)); + } + runBuildIfNeeded(tempDir, packageJsonPath, () => finalize(manifest), (err) => { cleanupTemp(); reject(err); }); + }); + rebuildProcess.on('error', (err) => { + cleanupTemp(); + reject(err); + }); }); npmProcess.on('error', (err) => { @@ -413,7 +428,18 @@ export function updatePluginFromGit(name) { if (npmCode !== 0) { return reject(new Error(`npm install for ${name} failed (exit code ${npmCode})`)); } - runBuildIfNeeded(pluginDir, packageJsonPath, () => resolve(manifest), (err) => reject(err)); + // Rebuild native addons that were skipped by --ignore-scripts + const rebuildProcess = spawn('npm', ['rebuild'], { + cwd: pluginDir, + stdio: ['ignore', 'pipe', 'pipe'], + }); + rebuildProcess.on('close', (rebuildCode) => { + if (rebuildCode !== 0) { + return reject(new Error(`npm rebuild for ${name} failed (exit code ${rebuildCode})`)); + } + runBuildIfNeeded(pluginDir, packageJsonPath, () => resolve(manifest), (err) => reject(err)); + }); + rebuildProcess.on('error', (err) => reject(err)); }); npmProcess.on('error', (err) => reject(err)); } else { diff --git a/vite.config.js b/vite.config.js index 46b7b21b7..f488f67d5 100755 --- a/vite.config.js +++ b/vite.config.js @@ -45,7 +45,20 @@ export default defineConfig(({ mode }) => { target: `ws://${proxyHost}:${serverPort}`, ws: true, rewrite: (path) => path - } + }, + [`${basePath}/plugin-ws`]: { + target: `ws://${proxyHost}:${serverPort}`, + ws: true, + rewrite: (path) => path + }, + // Plugins may connect to /plugin-ws without the base path prefix + ...(basePath ? { + '/plugin-ws': { + target: `ws://${proxyHost}:${serverPort}`, + ws: true, + rewrite: (path) => path + } + } : {}) } }, build: { From 7d94ef81120234b325862525baf86fad143e4313 Mon Sep 17 00:00:00 2001 From: AJ Date: Sun, 5 Apr 2026 08:14:14 -0700 Subject: [PATCH 7/7] fix: fix native addon support for plugin installs Remove --ignore-scripts from npm install so lifecycle scripts run (handles native addon compilation on all platforms). Remove the separate npm rebuild step (no longer needed). Add generic fixPrebuildPermissions() that scans all packages prebuilds for the current platform and ensures helper binaries are executable -- fixes node-pty spawn-helper permission bug on macOS and any similar issues. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/utils/plugin-loader.js | 58 ++++++++++++++++------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/server/utils/plugin-loader.js b/server/utils/plugin-loader.js index 4d402dd6d..717a93190 100644 --- a/server/utils/plugin-loader.js +++ b/server/utils/plugin-loader.js @@ -93,6 +93,27 @@ export function validateManifest(manifest) { return { valid: true }; } +/** + * Fix permissions on prebuild binaries shipped without execute bits. + * Scans all packages with a prebuilds/ directory for the current platform + * and ensures helper binaries (e.g. spawn-helper) are executable. + */ +function fixPrebuildPermissions(dir) { + const nodeModules = path.join(dir, 'node_modules'); + let packages; + try { packages = fs.readdirSync(nodeModules); } catch { return; } + const platform = `${process.platform}-${process.arch}`; + for (const pkg of packages) { + const archDir = path.join(nodeModules, pkg, 'prebuilds', platform); + let files; + try { files = fs.readdirSync(archDir); } catch { continue; } + for (const file of files) { + if (file.endsWith('.node')) continue; // .node files don't need +x + try { fs.chmodSync(path.join(archDir, file), 0o755); } catch { /* ignore */ } + } + } +} + const BUILD_TIMEOUT_MS = 60_000; /** Run `npm run build` if the plugin's package.json declares a build script. */ @@ -335,10 +356,9 @@ export function installPluginFromGit(url) { } // Run npm install if package.json exists. - // --ignore-scripts prevents postinstall hooks from executing arbitrary code. const packageJsonPath = path.join(tempDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { - const npmProcess = spawn('npm', ['install', '--ignore-scripts'], { + const npmProcess = spawn('npm', ['install'], { cwd: tempDir, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -348,22 +368,8 @@ export function installPluginFromGit(url) { cleanupTemp(); return reject(new Error(`npm install for ${repoName} failed (exit code ${npmCode})`)); } - // Rebuild native addons that were skipped by --ignore-scripts - const rebuildProcess = spawn('npm', ['rebuild'], { - cwd: tempDir, - stdio: ['ignore', 'pipe', 'pipe'], - }); - rebuildProcess.on('close', (rebuildCode) => { - if (rebuildCode !== 0) { - cleanupTemp(); - return reject(new Error(`npm rebuild for ${repoName} failed (exit code ${rebuildCode})`)); - } - runBuildIfNeeded(tempDir, packageJsonPath, () => finalize(manifest), (err) => { cleanupTemp(); reject(err); }); - }); - rebuildProcess.on('error', (err) => { - cleanupTemp(); - reject(err); - }); + fixPrebuildPermissions(tempDir); + runBuildIfNeeded(tempDir, packageJsonPath, () => finalize(manifest), (err) => { cleanupTemp(); reject(err); }); }); npmProcess.on('error', (err) => { @@ -420,7 +426,7 @@ export function updatePluginFromGit(name) { // Re-run npm install if package.json exists const packageJsonPath = path.join(pluginDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { - const npmProcess = spawn('npm', ['install', '--ignore-scripts'], { + const npmProcess = spawn('npm', ['install'], { cwd: pluginDir, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -428,18 +434,8 @@ export function updatePluginFromGit(name) { if (npmCode !== 0) { return reject(new Error(`npm install for ${name} failed (exit code ${npmCode})`)); } - // Rebuild native addons that were skipped by --ignore-scripts - const rebuildProcess = spawn('npm', ['rebuild'], { - cwd: pluginDir, - stdio: ['ignore', 'pipe', 'pipe'], - }); - rebuildProcess.on('close', (rebuildCode) => { - if (rebuildCode !== 0) { - return reject(new Error(`npm rebuild for ${name} failed (exit code ${rebuildCode})`)); - } - runBuildIfNeeded(pluginDir, packageJsonPath, () => resolve(manifest), (err) => reject(err)); - }); - rebuildProcess.on('error', (err) => reject(err)); + fixPrebuildPermissions(pluginDir); + runBuildIfNeeded(pluginDir, packageJsonPath, () => resolve(manifest), (err) => reject(err)); }); npmProcess.on('error', (err) => reject(err)); } else {