@siteboon/claude-code-ui 1.13.6 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/README.zh-CN.md +371 -0
- package/dist/assets/index-BQGOOBNa.css +32 -0
- package/dist/assets/index-Dqg05I_l.js +1239 -0
- package/dist/assets/{vendor-codemirror-CnTQH7Pk.js → vendor-codemirror-CJLzwpLB.js} +3 -3
- package/dist/assets/{vendor-react-DVSKlM5e.js → vendor-react-DcyRfQm3.js} +10 -10
- package/dist/index.html +4 -4
- package/package.json +5 -1
- package/server/claude-sdk.js +217 -23
- package/server/cursor-cli.js +17 -9
- package/server/index.js +102 -9
- package/server/middleware/auth.js +6 -1
- package/server/openai-codex.js +4 -2
- package/server/projects.js +119 -34
- package/server/routes/codex.js +56 -22
- package/server/routes/projects.js +204 -32
- package/dist/assets/index-Cc6pl7ji.css +0 -32
- package/dist/assets/index-lf1GuHwT.js +0 -1206
- package/server/database/auth.db +0 -0
|
@@ -7,11 +7,17 @@ import { addProjectManually } from '../projects.js';
|
|
|
7
7
|
|
|
8
8
|
const router = express.Router();
|
|
9
9
|
|
|
10
|
+
function sanitizeGitError(message, token) {
|
|
11
|
+
if (!message || !token) return message;
|
|
12
|
+
return message.replace(new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '***');
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
// Configure allowed workspace root (defaults to user's home directory)
|
|
11
16
|
const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir();
|
|
12
17
|
|
|
13
18
|
// System-critical paths that should never be used as workspace directories
|
|
14
|
-
const FORBIDDEN_PATHS = [
|
|
19
|
+
export const FORBIDDEN_PATHS = [
|
|
20
|
+
// Unix
|
|
15
21
|
'/',
|
|
16
22
|
'/etc',
|
|
17
23
|
'/bin',
|
|
@@ -27,7 +33,14 @@ const FORBIDDEN_PATHS = [
|
|
|
27
33
|
'/lib64',
|
|
28
34
|
'/opt',
|
|
29
35
|
'/tmp',
|
|
30
|
-
'/run'
|
|
36
|
+
'/run',
|
|
37
|
+
// Windows
|
|
38
|
+
'C:\\Windows',
|
|
39
|
+
'C:\\Program Files',
|
|
40
|
+
'C:\\Program Files (x86)',
|
|
41
|
+
'C:\\ProgramData',
|
|
42
|
+
'C:\\System Volume Information',
|
|
43
|
+
'C:\\$Recycle.Bin'
|
|
31
44
|
];
|
|
32
45
|
|
|
33
46
|
/**
|
|
@@ -212,20 +225,7 @@ router.post('/create-workspace', async (req, res) => {
|
|
|
212
225
|
|
|
213
226
|
// Handle new workspace creation
|
|
214
227
|
if (workspaceType === 'new') {
|
|
215
|
-
//
|
|
216
|
-
try {
|
|
217
|
-
await fs.access(absolutePath);
|
|
218
|
-
return res.status(400).json({
|
|
219
|
-
error: 'Path already exists. Please choose a different path or use "existing workspace" option.'
|
|
220
|
-
});
|
|
221
|
-
} catch (error) {
|
|
222
|
-
if (error.code !== 'ENOENT') {
|
|
223
|
-
throw error;
|
|
224
|
-
}
|
|
225
|
-
// Path doesn't exist - good, we can create it
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Create the directory
|
|
228
|
+
// Create the directory if it doesn't exist
|
|
229
229
|
await fs.mkdir(absolutePath, { recursive: true });
|
|
230
230
|
|
|
231
231
|
// If GitHub URL is provided, clone the repository
|
|
@@ -246,30 +246,55 @@ router.post('/create-workspace', async (req, res) => {
|
|
|
246
246
|
githubToken = newGithubToken;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
//
|
|
249
|
+
// Extract repo name from URL for the clone destination
|
|
250
|
+
const normalizedUrl = githubUrl.replace(/\/+$/, '').replace(/\.git$/, '');
|
|
251
|
+
const repoName = normalizedUrl.split('/').pop() || 'repository';
|
|
252
|
+
const clonePath = path.join(absolutePath, repoName);
|
|
253
|
+
|
|
254
|
+
// Check if clone destination already exists to prevent data loss
|
|
250
255
|
try {
|
|
251
|
-
await
|
|
256
|
+
await fs.access(clonePath);
|
|
257
|
+
return res.status(409).json({
|
|
258
|
+
error: 'Directory already exists',
|
|
259
|
+
details: `The destination path "${clonePath}" already exists. Please choose a different location or remove the existing directory.`
|
|
260
|
+
});
|
|
261
|
+
} catch (err) {
|
|
262
|
+
// Directory doesn't exist, which is what we want
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Clone the repository into a subfolder
|
|
266
|
+
try {
|
|
267
|
+
await cloneGitHubRepository(githubUrl, clonePath, githubToken);
|
|
252
268
|
} catch (error) {
|
|
253
|
-
//
|
|
269
|
+
// Only clean up if clone created partial data (check if dir exists and is empty or partial)
|
|
254
270
|
try {
|
|
255
|
-
await fs.
|
|
271
|
+
const stats = await fs.stat(clonePath);
|
|
272
|
+
if (stats.isDirectory()) {
|
|
273
|
+
await fs.rm(clonePath, { recursive: true, force: true });
|
|
274
|
+
}
|
|
256
275
|
} catch (cleanupError) {
|
|
257
|
-
|
|
258
|
-
// Continue to throw original error
|
|
276
|
+
// Directory doesn't exist or cleanup failed - ignore
|
|
259
277
|
}
|
|
260
278
|
throw new Error(`Failed to clone repository: ${error.message}`);
|
|
261
279
|
}
|
|
280
|
+
|
|
281
|
+
// Add the cloned repo path to the project list
|
|
282
|
+
const project = await addProjectManually(clonePath);
|
|
283
|
+
|
|
284
|
+
return res.json({
|
|
285
|
+
success: true,
|
|
286
|
+
project,
|
|
287
|
+
message: 'New workspace created and repository cloned successfully'
|
|
288
|
+
});
|
|
262
289
|
}
|
|
263
290
|
|
|
264
|
-
// Add the new workspace to the project list
|
|
291
|
+
// Add the new workspace to the project list (no clone)
|
|
265
292
|
const project = await addProjectManually(absolutePath);
|
|
266
293
|
|
|
267
294
|
return res.json({
|
|
268
295
|
success: true,
|
|
269
296
|
project,
|
|
270
|
-
message:
|
|
271
|
-
? 'New workspace created and repository cloned successfully'
|
|
272
|
-
: 'New workspace created successfully'
|
|
297
|
+
message: 'New workspace created successfully'
|
|
273
298
|
});
|
|
274
299
|
}
|
|
275
300
|
|
|
@@ -305,31 +330,179 @@ async function getGithubTokenById(tokenId, userId) {
|
|
|
305
330
|
return null;
|
|
306
331
|
}
|
|
307
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Clone repository with progress streaming (SSE)
|
|
335
|
+
* GET /api/projects/clone-progress
|
|
336
|
+
*/
|
|
337
|
+
router.get('/clone-progress', async (req, res) => {
|
|
338
|
+
const { path: workspacePath, githubUrl, githubTokenId, newGithubToken } = req.query;
|
|
339
|
+
|
|
340
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
341
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
342
|
+
res.setHeader('Connection', 'keep-alive');
|
|
343
|
+
res.flushHeaders();
|
|
344
|
+
|
|
345
|
+
const sendEvent = (type, data) => {
|
|
346
|
+
res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
if (!workspacePath || !githubUrl) {
|
|
351
|
+
sendEvent('error', { message: 'workspacePath and githubUrl are required' });
|
|
352
|
+
res.end();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const validation = await validateWorkspacePath(workspacePath);
|
|
357
|
+
if (!validation.valid) {
|
|
358
|
+
sendEvent('error', { message: validation.error });
|
|
359
|
+
res.end();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const absolutePath = validation.resolvedPath;
|
|
364
|
+
|
|
365
|
+
await fs.mkdir(absolutePath, { recursive: true });
|
|
366
|
+
|
|
367
|
+
let githubToken = null;
|
|
368
|
+
if (githubTokenId) {
|
|
369
|
+
const token = await getGithubTokenById(parseInt(githubTokenId), req.user.id);
|
|
370
|
+
if (!token) {
|
|
371
|
+
await fs.rm(absolutePath, { recursive: true, force: true });
|
|
372
|
+
sendEvent('error', { message: 'GitHub token not found' });
|
|
373
|
+
res.end();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
githubToken = token.github_token;
|
|
377
|
+
} else if (newGithubToken) {
|
|
378
|
+
githubToken = newGithubToken;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const normalizedUrl = githubUrl.replace(/\/+$/, '').replace(/\.git$/, '');
|
|
382
|
+
const repoName = normalizedUrl.split('/').pop() || 'repository';
|
|
383
|
+
const clonePath = path.join(absolutePath, repoName);
|
|
384
|
+
|
|
385
|
+
// Check if clone destination already exists to prevent data loss
|
|
386
|
+
try {
|
|
387
|
+
await fs.access(clonePath);
|
|
388
|
+
sendEvent('error', { message: `Directory "${repoName}" already exists. Please choose a different location or remove the existing directory.` });
|
|
389
|
+
res.end();
|
|
390
|
+
return;
|
|
391
|
+
} catch (err) {
|
|
392
|
+
// Directory doesn't exist, which is what we want
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let cloneUrl = githubUrl;
|
|
396
|
+
if (githubToken) {
|
|
397
|
+
try {
|
|
398
|
+
const url = new URL(githubUrl);
|
|
399
|
+
url.username = githubToken;
|
|
400
|
+
url.password = '';
|
|
401
|
+
cloneUrl = url.toString();
|
|
402
|
+
} catch (error) {
|
|
403
|
+
// SSH URL or invalid - use as-is
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
sendEvent('progress', { message: `Cloning into '${repoName}'...` });
|
|
408
|
+
|
|
409
|
+
const gitProcess = spawn('git', ['clone', '--progress', cloneUrl, clonePath], {
|
|
410
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
411
|
+
env: {
|
|
412
|
+
...process.env,
|
|
413
|
+
GIT_TERMINAL_PROMPT: '0'
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
let lastError = '';
|
|
418
|
+
|
|
419
|
+
gitProcess.stdout.on('data', (data) => {
|
|
420
|
+
const message = data.toString().trim();
|
|
421
|
+
if (message) {
|
|
422
|
+
sendEvent('progress', { message });
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
gitProcess.stderr.on('data', (data) => {
|
|
427
|
+
const message = data.toString().trim();
|
|
428
|
+
lastError = message;
|
|
429
|
+
if (message) {
|
|
430
|
+
sendEvent('progress', { message });
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
gitProcess.on('close', async (code) => {
|
|
435
|
+
if (code === 0) {
|
|
436
|
+
try {
|
|
437
|
+
const project = await addProjectManually(clonePath);
|
|
438
|
+
sendEvent('complete', { project, message: 'Repository cloned successfully' });
|
|
439
|
+
} catch (error) {
|
|
440
|
+
sendEvent('error', { message: `Clone succeeded but failed to add project: ${error.message}` });
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
const sanitizedError = sanitizeGitError(lastError, githubToken);
|
|
444
|
+
let errorMessage = 'Git clone failed';
|
|
445
|
+
if (lastError.includes('Authentication failed') || lastError.includes('could not read Username')) {
|
|
446
|
+
errorMessage = 'Authentication failed. Please check your credentials.';
|
|
447
|
+
} else if (lastError.includes('Repository not found')) {
|
|
448
|
+
errorMessage = 'Repository not found. Please check the URL and ensure you have access.';
|
|
449
|
+
} else if (lastError.includes('already exists')) {
|
|
450
|
+
errorMessage = 'Directory already exists';
|
|
451
|
+
} else if (sanitizedError) {
|
|
452
|
+
errorMessage = sanitizedError;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
await fs.rm(clonePath, { recursive: true, force: true });
|
|
456
|
+
} catch (cleanupError) {
|
|
457
|
+
console.error('Failed to clean up after clone failure:', sanitizeGitError(cleanupError.message, githubToken));
|
|
458
|
+
}
|
|
459
|
+
sendEvent('error', { message: errorMessage });
|
|
460
|
+
}
|
|
461
|
+
res.end();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
gitProcess.on('error', (error) => {
|
|
465
|
+
if (error.code === 'ENOENT') {
|
|
466
|
+
sendEvent('error', { message: 'Git is not installed or not in PATH' });
|
|
467
|
+
} else {
|
|
468
|
+
sendEvent('error', { message: error.message });
|
|
469
|
+
}
|
|
470
|
+
res.end();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
req.on('close', () => {
|
|
474
|
+
gitProcess.kill();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
} catch (error) {
|
|
478
|
+
sendEvent('error', { message: error.message });
|
|
479
|
+
res.end();
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
308
483
|
/**
|
|
309
484
|
* Helper function to clone a GitHub repository
|
|
310
485
|
*/
|
|
311
486
|
function cloneGitHubRepository(githubUrl, destinationPath, githubToken = null) {
|
|
312
487
|
return new Promise((resolve, reject) => {
|
|
313
|
-
// Parse GitHub URL and inject token if provided
|
|
314
488
|
let cloneUrl = githubUrl;
|
|
315
489
|
|
|
316
490
|
if (githubToken) {
|
|
317
491
|
try {
|
|
318
492
|
const url = new URL(githubUrl);
|
|
319
|
-
// Format: https://TOKEN@github.com/user/repo.git
|
|
320
493
|
url.username = githubToken;
|
|
321
494
|
url.password = '';
|
|
322
495
|
cloneUrl = url.toString();
|
|
323
496
|
} catch (error) {
|
|
324
|
-
|
|
497
|
+
// SSH URL - use as-is
|
|
325
498
|
}
|
|
326
499
|
}
|
|
327
500
|
|
|
328
|
-
const gitProcess = spawn('git', ['clone', cloneUrl, destinationPath], {
|
|
501
|
+
const gitProcess = spawn('git', ['clone', '--progress', cloneUrl, destinationPath], {
|
|
329
502
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
330
503
|
env: {
|
|
331
504
|
...process.env,
|
|
332
|
-
GIT_TERMINAL_PROMPT: '0'
|
|
505
|
+
GIT_TERMINAL_PROMPT: '0'
|
|
333
506
|
}
|
|
334
507
|
});
|
|
335
508
|
|
|
@@ -348,7 +521,6 @@ function cloneGitHubRepository(githubUrl, destinationPath, githubToken = null) {
|
|
|
348
521
|
if (code === 0) {
|
|
349
522
|
resolve({ stdout, stderr });
|
|
350
523
|
} else {
|
|
351
|
-
// Parse git error messages to provide helpful feedback
|
|
352
524
|
let errorMessage = 'Git clone failed';
|
|
353
525
|
|
|
354
526
|
if (stderr.includes('Authentication failed') || stderr.includes('could not read Username')) {
|