diff --git a/server/index.js b/server/index.js index 2f19dcd..719e1c3 100755 --- a/server/index.js +++ b/server/index.js @@ -70,7 +70,7 @@ import mcpUtilsRoutes from './routes/mcp-utils.js'; import commandsRoutes from './routes/commands.js'; import settingsRoutes from './routes/settings.js'; import agentRoutes from './routes/agent.js'; -import projectsRoutes from './routes/projects.js'; +import projectsRoutes, { FORBIDDEN_PATHS } from './routes/projects.js'; import cliAuthRoutes from './routes/cli-auth.js'; import userRoutes from './routes/user.js'; import codexRoutes from './routes/codex.js'; @@ -550,6 +550,55 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => { } }); +app.post('/api/create-folder', authenticateToken, async (req, res) => { + try { + const { path: folderPath } = req.body; + if (!folderPath) { + return res.status(400).json({ error: 'Path is required' }); + } + const homeDir = os.homedir(); + const targetPath = path.resolve(folderPath.replace('~', homeDir)); + const normalizedPath = path.normalize(targetPath); + const comparePath = normalizedPath.toLowerCase(); + const forbiddenLower = FORBIDDEN_PATHS.map(p => p.toLowerCase()); + if (forbiddenLower.includes(comparePath) || comparePath === '/') { + return res.status(403).json({ error: 'Cannot create folders in system directories' }); + } + for (const forbidden of forbiddenLower) { + if (comparePath.startsWith(forbidden + path.sep)) { + if (forbidden === '/var' && (comparePath.startsWith('/var/tmp') || comparePath.startsWith('/var/folders'))) { + continue; + } + return res.status(403).json({ error: `Cannot create folders in system directory: ${forbidden}` }); + } + } + const parentDir = path.dirname(targetPath); + try { + await fs.promises.access(parentDir); + } catch (err) { + return res.status(404).json({ error: 'Parent directory does not exist' }); + } + try { + await fs.promises.access(targetPath); + return res.status(409).json({ error: 'Folder already exists' }); + } catch (err) { + // Folder doesn't exist, which is what we want + } + try { + await fs.promises.mkdir(targetPath, { recursive: false }); + res.json({ success: true, path: targetPath }); + } catch (mkdirError) { + if (mkdirError.code === 'EEXIST') { + return res.status(409).json({ error: 'Folder already exists' }); + } + throw mkdirError; + } + } catch (error) { + console.error('Error creating folder:', error); + res.status(500).json({ error: 'Failed to create folder' }); + } +}); + // Read file content endpoint app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => { try { diff --git a/server/middleware/auth.js b/server/middleware/auth.js index b9ff24f..9231e4e 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -37,7 +37,12 @@ const authenticateToken = async (req, res, next) => { // Normal OSS JWT validation const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + let token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + + // Also check query param for SSE endpoints (EventSource can't set headers) + if (!token && req.query.token) { + token = req.query.token; + } if (!token) { return res.status(401).json({ error: 'Access denied. No token provided.' }); diff --git a/server/projects.js b/server/projects.js index b4606f8..475b323 100755 --- a/server/projects.js +++ b/server/projects.js @@ -1095,7 +1095,7 @@ async function addProjectManually(projectPath, displayName = null) { } // Generate project name (encode path for use as directory name) - const projectName = absolutePath.replace(/\//g, '-'); + const projectName = absolutePath.replace(/[\\/:\s~_]/g, '-'); // Check if project already exists in config const config = await loadProjectConfig(); diff --git a/server/routes/projects.js b/server/routes/projects.js index 042de51..7db0537 100644 --- a/server/routes/projects.js +++ b/server/routes/projects.js @@ -7,11 +7,17 @@ import { addProjectManually } from '../projects.js'; const router = express.Router(); +function sanitizeGitError(message, token) { + if (!message || !token) return message; + return message.replace(new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '***'); +} + // Configure allowed workspace root (defaults to user's home directory) const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir(); // System-critical paths that should never be used as workspace directories -const FORBIDDEN_PATHS = [ +export const FORBIDDEN_PATHS = [ + // Unix '/', '/etc', '/bin', @@ -27,7 +33,14 @@ const FORBIDDEN_PATHS = [ '/lib64', '/opt', '/tmp', - '/run' + '/run', + // Windows + 'C:\\Windows', + 'C:\\Program Files', + 'C:\\Program Files (x86)', + 'C:\\ProgramData', + 'C:\\System Volume Information', + 'C:\\$Recycle.Bin' ]; /** @@ -212,20 +225,7 @@ router.post('/create-workspace', async (req, res) => { // Handle new workspace creation if (workspaceType === 'new') { - // Check if path already exists - try { - await fs.access(absolutePath); - return res.status(400).json({ - error: 'Path already exists. Please choose a different path or use "existing workspace" option.' - }); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - // Path doesn't exist - good, we can create it - } - - // Create the directory + // Create the directory if it doesn't exist await fs.mkdir(absolutePath, { recursive: true }); // If GitHub URL is provided, clone the repository @@ -246,30 +246,55 @@ router.post('/create-workspace', async (req, res) => { githubToken = newGithubToken; } - // Clone the repository + // Extract repo name from URL for the clone destination + const normalizedUrl = githubUrl.replace(/\/+$/, '').replace(/\.git$/, ''); + const repoName = normalizedUrl.split('/').pop() || 'repository'; + const clonePath = path.join(absolutePath, repoName); + + // Check if clone destination already exists to prevent data loss try { - await cloneGitHubRepository(githubUrl, absolutePath, githubToken); + await fs.access(clonePath); + return res.status(409).json({ + error: 'Directory already exists', + details: `The destination path "${clonePath}" already exists. Please choose a different location or remove the existing directory.` + }); + } catch (err) { + // Directory doesn't exist, which is what we want + } + + // Clone the repository into a subfolder + try { + await cloneGitHubRepository(githubUrl, clonePath, githubToken); } catch (error) { - // Clean up created directory on failure + // Only clean up if clone created partial data (check if dir exists and is empty or partial) try { - await fs.rm(absolutePath, { recursive: true, force: true }); + const stats = await fs.stat(clonePath); + if (stats.isDirectory()) { + await fs.rm(clonePath, { recursive: true, force: true }); + } } catch (cleanupError) { - console.error('Failed to clean up directory after clone failure:', cleanupError); - // Continue to throw original error + // Directory doesn't exist or cleanup failed - ignore } throw new Error(`Failed to clone repository: ${error.message}`); } + + // Add the cloned repo path to the project list + const project = await addProjectManually(clonePath); + + return res.json({ + success: true, + project, + message: 'New workspace created and repository cloned successfully' + }); } - // Add the new workspace to the project list + // Add the new workspace to the project list (no clone) const project = await addProjectManually(absolutePath); return res.json({ success: true, project, - message: githubUrl - ? 'New workspace created and repository cloned successfully' - : 'New workspace created successfully' + message: 'New workspace created successfully' }); } @@ -305,31 +330,179 @@ async function getGithubTokenById(tokenId, userId) { return null; } +/** + * Clone repository with progress streaming (SSE) + * GET /api/projects/clone-progress + */ +router.get('/clone-progress', async (req, res) => { + const { path: workspacePath, githubUrl, githubTokenId, newGithubToken } = req.query; + + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders(); + + const sendEvent = (type, data) => { + res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`); + }; + + try { + if (!workspacePath || !githubUrl) { + sendEvent('error', { message: 'workspacePath and githubUrl are required' }); + res.end(); + return; + } + + const validation = await validateWorkspacePath(workspacePath); + if (!validation.valid) { + sendEvent('error', { message: validation.error }); + res.end(); + return; + } + + const absolutePath = validation.resolvedPath; + + await fs.mkdir(absolutePath, { recursive: true }); + + let githubToken = null; + if (githubTokenId) { + const token = await getGithubTokenById(parseInt(githubTokenId), req.user.id); + if (!token) { + await fs.rm(absolutePath, { recursive: true, force: true }); + sendEvent('error', { message: 'GitHub token not found' }); + res.end(); + return; + } + githubToken = token.github_token; + } else if (newGithubToken) { + githubToken = newGithubToken; + } + + const normalizedUrl = githubUrl.replace(/\/+$/, '').replace(/\.git$/, ''); + const repoName = normalizedUrl.split('/').pop() || 'repository'; + const clonePath = path.join(absolutePath, repoName); + + // Check if clone destination already exists to prevent data loss + try { + await fs.access(clonePath); + sendEvent('error', { message: `Directory "${repoName}" already exists. Please choose a different location or remove the existing directory.` }); + res.end(); + return; + } catch (err) { + // Directory doesn't exist, which is what we want + } + + let cloneUrl = githubUrl; + if (githubToken) { + try { + const url = new URL(githubUrl); + url.username = githubToken; + url.password = ''; + cloneUrl = url.toString(); + } catch (error) { + // SSH URL or invalid - use as-is + } + } + + sendEvent('progress', { message: `Cloning into '${repoName}'...` }); + + const gitProcess = spawn('git', ['clone', '--progress', cloneUrl, clonePath], { + stdio: ['ignore', 'pipe', 'pipe'], + env: { + ...process.env, + GIT_TERMINAL_PROMPT: '0' + } + }); + + let lastError = ''; + + gitProcess.stdout.on('data', (data) => { + const message = data.toString().trim(); + if (message) { + sendEvent('progress', { message }); + } + }); + + gitProcess.stderr.on('data', (data) => { + const message = data.toString().trim(); + lastError = message; + if (message) { + sendEvent('progress', { message }); + } + }); + + gitProcess.on('close', async (code) => { + if (code === 0) { + try { + const project = await addProjectManually(clonePath); + sendEvent('complete', { project, message: 'Repository cloned successfully' }); + } catch (error) { + sendEvent('error', { message: `Clone succeeded but failed to add project: ${error.message}` }); + } + } else { + const sanitizedError = sanitizeGitError(lastError, githubToken); + let errorMessage = 'Git clone failed'; + if (lastError.includes('Authentication failed') || lastError.includes('could not read Username')) { + errorMessage = 'Authentication failed. Please check your credentials.'; + } else if (lastError.includes('Repository not found')) { + errorMessage = 'Repository not found. Please check the URL and ensure you have access.'; + } else if (lastError.includes('already exists')) { + errorMessage = 'Directory already exists'; + } else if (sanitizedError) { + errorMessage = sanitizedError; + } + try { + await fs.rm(clonePath, { recursive: true, force: true }); + } catch (cleanupError) { + console.error('Failed to clean up after clone failure:', sanitizeGitError(cleanupError.message, githubToken)); + } + sendEvent('error', { message: errorMessage }); + } + res.end(); + }); + + gitProcess.on('error', (error) => { + if (error.code === 'ENOENT') { + sendEvent('error', { message: 'Git is not installed or not in PATH' }); + } else { + sendEvent('error', { message: error.message }); + } + res.end(); + }); + + req.on('close', () => { + gitProcess.kill(); + }); + + } catch (error) { + sendEvent('error', { message: error.message }); + res.end(); + } +}); + /** * Helper function to clone a GitHub repository */ function cloneGitHubRepository(githubUrl, destinationPath, githubToken = null) { return new Promise((resolve, reject) => { - // Parse GitHub URL and inject token if provided let cloneUrl = githubUrl; if (githubToken) { try { const url = new URL(githubUrl); - // Format: https://TOKEN@github.com/user/repo.git url.username = githubToken; url.password = ''; cloneUrl = url.toString(); } catch (error) { - return reject(new Error('Invalid GitHub URL format')); + // SSH URL - use as-is } } - const gitProcess = spawn('git', ['clone', cloneUrl, destinationPath], { + const gitProcess = spawn('git', ['clone', '--progress', cloneUrl, destinationPath], { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, - GIT_TERMINAL_PROMPT: '0' // Disable git password prompts + GIT_TERMINAL_PROMPT: '0' } }); @@ -348,7 +521,6 @@ function cloneGitHubRepository(githubUrl, destinationPath, githubToken = null) { if (code === 0) { resolve({ stdout, stderr }); } else { - // Parse git error messages to provide helpful feedback let errorMessage = 'Git clone failed'; if (stderr.includes('Authentication failed') || stderr.includes('could not read Username')) { diff --git a/src/components/ProjectCreationWizard.jsx b/src/components/ProjectCreationWizard.jsx index 2109e56..dd6ab72 100644 --- a/src/components/ProjectCreationWizard.jsx +++ b/src/components/ProjectCreationWizard.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { X, FolderPlus, GitBranch, Key, ChevronRight, ChevronLeft, Check, Loader2, AlertCircle, FolderOpen, Eye, EyeOff } from 'lucide-react'; +import { X, FolderPlus, GitBranch, Key, ChevronRight, ChevronLeft, Check, Loader2, AlertCircle, FolderOpen, Eye, EyeOff, Plus } from 'lucide-react'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { api } from '../utils/api'; @@ -30,6 +30,10 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { const [browserFolders, setBrowserFolders] = useState([]); const [loadingFolders, setLoadingFolders] = useState(false); const [showHiddenFolders, setShowHiddenFolders] = useState(false); + const [showNewFolderInput, setShowNewFolderInput] = useState(false); + const [newFolderName, setNewFolderName] = useState(''); + const [creatingFolder, setCreatingFolder] = useState(false); + const [cloneProgress, setCloneProgress] = useState(''); // Load available GitHub tokens when needed useEffect(() => { @@ -78,9 +82,10 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { const data = await response.json(); if (data.suggestions) { - // Filter suggestions based on the input + // Filter suggestions based on the input, excluding exact match const filtered = data.suggestions.filter(s => - s.path.toLowerCase().startsWith(inputPath.toLowerCase()) + s.path.toLowerCase().startsWith(inputPath.toLowerCase()) && + s.path.toLowerCase() !== inputPath.toLowerCase() ); setPathSuggestions(filtered.slice(0, 5)); setShowPathDropdown(filtered.length > 0); @@ -118,24 +123,62 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { const handleCreate = async () => { setIsCreating(true); setError(null); + setCloneProgress(''); try { + if (workspaceType === 'new' && githubUrl) { + const params = new URLSearchParams({ + path: workspacePath.trim(), + githubUrl: githubUrl.trim(), + }); + + if (tokenMode === 'stored' && selectedGithubToken) { + params.append('githubTokenId', selectedGithubToken); + } else if (tokenMode === 'new' && newGithubToken) { + params.append('newGithubToken', newGithubToken.trim()); + } + + const token = localStorage.getItem('auth-token'); + const url = `/api/projects/clone-progress?${params}${token ? `&token=${token}` : ''}`; + + await new Promise((resolve, reject) => { + const eventSource = new EventSource(url); + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + + if (data.type === 'progress') { + setCloneProgress(data.message); + } else if (data.type === 'complete') { + eventSource.close(); + if (onProjectCreated) { + onProjectCreated(data.project); + } + onClose(); + resolve(); + } else if (data.type === 'error') { + eventSource.close(); + reject(new Error(data.message)); + } + } catch (e) { + console.error('Error parsing SSE event:', e); + } + }; + + eventSource.onerror = () => { + eventSource.close(); + reject(new Error('Connection lost during clone')); + }; + }); + return; + } + const payload = { workspaceType, path: workspacePath.trim(), }; - // Add GitHub info if creating new workspace with GitHub URL - if (workspaceType === 'new' && githubUrl) { - payload.githubUrl = githubUrl.trim(); - - if (tokenMode === 'stored' && selectedGithubToken) { - payload.githubTokenId = parseInt(selectedGithubToken); - } else if (tokenMode === 'new' && newGithubToken) { - payload.newGithubToken = newGithubToken.trim(); - } - } - const response = await api.createWorkspace(payload); const data = await response.json(); @@ -143,7 +186,6 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { throw new Error(data.error || t('projectWizard.errors.failedToCreate')); } - // Success! if (onProjectCreated) { onProjectCreated(data.project); } @@ -170,9 +212,9 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { const loadBrowserFolders = async (path) => { try { setLoadingFolders(true); - setBrowserCurrentPath(path); const response = await api.browseFilesystem(path); const data = await response.json(); + setBrowserCurrentPath(data.path || path); setBrowserFolders(data.suggestions || []); } catch (error) { console.error('Error loading folders:', error); @@ -193,6 +235,29 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { await loadBrowserFolders(folderPath); }; + const createNewFolder = async () => { + if (!newFolderName.trim()) return; + setCreatingFolder(true); + setError(null); + try { + const separator = browserCurrentPath.includes('\\') ? '\\' : '/'; + const folderPath = `${browserCurrentPath}${separator}${newFolderName.trim()}`; + const response = await api.createFolder(folderPath); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || t('projectWizard.errors.failedToCreateFolder', 'Failed to create folder')); + } + setNewFolderName(''); + setShowNewFolderInput(false); + await loadBrowserFolders(data.path || folderPath); + } catch (error) { + console.error('Error creating folder:', error); + setError(error.message || t('projectWizard.errors.failedToCreateFolder', 'Failed to create folder')); + } finally { + setCreatingFolder(false); + } + }; + return (
@@ -388,8 +453,8 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {

- {/* GitHub Token (only if GitHub URL is provided) */} - {githubUrl && ( + {/* GitHub Token (only for HTTPS URLs - SSH uses SSH keys) */} + {githubUrl && !githubUrl.startsWith('git@') && !githubUrl.startsWith('ssh://') && (
@@ -551,6 +616,8 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { ? `${t('projectWizard.step3.usingStoredToken')} ${availableTokens.find(t => t.id.toString() === selectedGithubToken)?.credential_name || 'Unknown'}` : tokenMode === 'new' && newGithubToken ? t('projectWizard.step3.usingProvidedToken') + : (githubUrl.startsWith('git@') || githubUrl.startsWith('ssh://')) + ? t('projectWizard.step3.sshKey', 'SSH Key') : t('projectWizard.step3.noAuthentication')}
@@ -560,13 +627,22 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
-

- {workspaceType === 'existing' - ? t('projectWizard.step3.existingInfo') - : githubUrl - ? t('projectWizard.step3.newWithClone') - : t('projectWizard.step3.newEmpty')} -

+ {isCreating && cloneProgress ? ( +
+

{t('projectWizard.step3.cloningRepository', 'Cloning repository...')}

+ + {cloneProgress} + +
+ ) : ( +

+ {workspaceType === 'existing' + ? t('projectWizard.step3.existingInfo') + : githubUrl + ? t('projectWizard.step3.newWithClone') + : t('projectWizard.step3.newEmpty')} +

+ )}
)} @@ -596,7 +672,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { {isCreating ? ( <> - {t('projectWizard.buttons.creating')} + {githubUrl ? t('projectWizard.buttons.cloning', 'Cloning...') : t('projectWizard.buttons.creating')} ) : step === 3 ? ( <> @@ -639,6 +715,17 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { > {showHiddenFolders ? : } + + + + + )} + {/* Folder List */}
{loadingFolders ? (
- ) : browserFolders.length === 0 ? ( -
- No folders found -
) : (
- {/* Parent Directory */} - {browserCurrentPath !== '~' && browserCurrentPath !== '/' && ( + {/* Parent Directory - check for Windows root (e.g., C:\) and Unix root */} + {browserCurrentPath !== '~' && browserCurrentPath !== '/' && !/^[A-Za-z]:\\?$/.test(browserCurrentPath) && ( - + {browserFolders.length === 0 ? ( +
+ No subfolders found
- ))} + ) : ( + browserFolders + .filter(folder => showHiddenFolders || !folder.name.startsWith('.')) + .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) + .map((folder, index) => ( +
+ + +
+ )) + )}
)}
@@ -712,13 +849,17 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index bb2d6bc..18f2b80 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -136,14 +136,14 @@ }, "step2": { "existingPath": "Workspace Path", - "newPath": "Where should the workspace be created?", + "newPath": "Workspace Path", "existingPlaceholder": "/path/to/existing/workspace", "newPlaceholder": "/path/to/new/workspace", "existingHelp": "Full path to your existing workspace directory", - "newHelp": "Full path where the new workspace will be created", + "newHelp": "Full path to your workspace directory", "githubUrl": "GitHub URL (Optional)", "githubPlaceholder": "https://github.com/username/repository", - "githubHelp": "Leave empty to create an empty workspace, or provide a GitHub URL to clone", + "githubHelp": "Optional: provide a GitHub URL to clone a repository", "githubAuth": "GitHub Authentication (Optional)", "githubAuthHelp": "Only required for private repositories. Public repos can be cloned without authentication.", "loadingTokens": "Loading stored tokens...", @@ -170,21 +170,25 @@ "usingStoredToken": "Using stored token:", "usingProvidedToken": "Using provided token", "noAuthentication": "No authentication", + "sshKey": "SSH Key", "existingInfo": "The workspace will be added to your project list and will be available for Claude/Cursor sessions.", - "newWithClone": "A new workspace will be created and the repository will be cloned from GitHub.", - "newEmpty": "An empty workspace directory will be created at the specified path." + "newWithClone": "The repository will be cloned from this folder.", + "newEmpty": "The workspace will be added to your project list and will be available for Claude/Cursor sessions.", + "cloningRepository": "Cloning repository..." }, "buttons": { "cancel": "Cancel", "back": "Back", "next": "Next", "createProject": "Create Project", - "creating": "Creating..." + "creating": "Creating...", + "cloning": "Cloning..." }, "errors": { "selectType": "Please select whether you have an existing workspace or want to create a new one", "providePath": "Please provide a workspace path", - "failedToCreate": "Failed to create workspace" + "failedToCreate": "Failed to create workspace", + "failedToCreateFolder": "Failed to create folder" } }, "versionUpdate": { diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 9c66ca8..c5db190 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -136,14 +136,14 @@ }, "step2": { "existingPath": "工作区路径", - "newPath": "应该在哪里创建工作区?", + "newPath": "工作区路径", "existingPlaceholder": "/path/to/existing/workspace", "newPlaceholder": "/path/to/new/workspace", "existingHelp": "您现有工作区目录的完整路径", - "newHelp": "将创建新工作区的完整路径", + "newHelp": "工作区目录的完整路径", "githubUrl": "GitHub URL(可选)", "githubPlaceholder": "https://github.com/username/repository", - "githubHelp": "留空以创建空工作区,或提供 GitHub URL 以克隆", + "githubHelp": "可选:提供 GitHub URL 以克隆仓库", "githubAuth": "GitHub 身份验证(可选)", "githubAuthHelp": "仅私有仓库需要。公共仓库无需身份验证即可克隆。", "loadingTokens": "正在加载已保存的令牌...", @@ -170,21 +170,25 @@ "usingStoredToken": "使用已保存的令牌:", "usingProvidedToken": "使用提供的令牌", "noAuthentication": "无身份验证", + "sshKey": "SSH 密钥", "existingInfo": "工作区将被添加到您的项目列表中,并可用于 Claude/Cursor 会话。", - "newWithClone": "将创建新工作区,并从 GitHub 克隆仓库。", - "newEmpty": "将在指定路径创建一个空的工作区目录。" + "newWithClone": "仓库将从此文件夹克隆。", + "newEmpty": "工作区将被添加到您的项目列表中,并可用于 Claude/Cursor 会话。", + "cloningRepository": "正在克隆仓库..." }, "buttons": { "cancel": "取消", "back": "返回", "next": "下一步", "createProject": "创建项目", - "creating": "创建中..." + "creating": "创建中...", + "cloning": "正在克隆..." }, "errors": { "selectType": "请选择您已有现有工作区还是想创建新工作区", "providePath": "请提供工作区路径", - "failedToCreate": "创建工作区失败" + "failedToCreate": "创建工作区失败", + "failedToCreateFolder": "创建文件夹失败" } }, "versionUpdate": { diff --git a/src/utils/api.js b/src/utils/api.js index 7f497cd..1625546 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -158,6 +158,12 @@ export const api = { return authenticatedFetch(`/api/browse-filesystem?${params}`); }, + createFolder: (folderPath) => + authenticatedFetch('/api/create-folder', { + method: 'POST', + body: JSON.stringify({ path: folderPath }), + }), + // User endpoints user: { gitConfig: () => authenticatedFetch('/api/user/git-config'),