claude-code-workflow 6.3.29 → 6.3.30
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/ccw/dist/core/data-aggregator.js +20 -7
- package/ccw/dist/core/data-aggregator.js.map +1 -1
- package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/issue-routes.js +249 -4
- package/ccw/dist/core/routes/issue-routes.js.map +1 -1
- package/ccw/src/core/data-aggregator.ts +19 -7
- package/ccw/src/core/routes/issue-routes.ts +275 -4
- package/ccw/src/templates/dashboard-css/32-issue-manager.css +435 -37
- package/ccw/src/templates/dashboard-js/i18n.js +18 -0
- package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +5 -5
- package/ccw/src/templates/dashboard-js/views/issue-manager.js +744 -29
- package/ccw/src/templates/dashboard-js/views/skills-manager.js +2 -4
- package/codex-lens/src/codexlens/cli/__pycache__/commands.cpython-312.pyc +0 -0
- package/codex-lens/src/codexlens/cli/__pycache__/commands.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/cli/commands.py +78 -0
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* - POST /api/queue/reorder - Reorder queue items
|
|
24
24
|
*/
|
|
25
25
|
import { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
26
|
-
import { join } from 'path';
|
|
26
|
+
import { join, resolve, normalize } from 'path';
|
|
27
27
|
import type { RouteContext } from './types.js';
|
|
28
28
|
|
|
29
29
|
// ========== JSONL Helper Functions ==========
|
|
@@ -156,7 +156,30 @@ function writeQueue(issuesDir: string, queue: any) {
|
|
|
156
156
|
|
|
157
157
|
function getIssueDetail(issuesDir: string, issueId: string) {
|
|
158
158
|
const issues = readIssuesJsonl(issuesDir);
|
|
159
|
-
|
|
159
|
+
let issue = issues.find(i => i.id === issueId);
|
|
160
|
+
|
|
161
|
+
// Fallback: Reconstruct issue from solution file if issue not in issues.jsonl
|
|
162
|
+
if (!issue) {
|
|
163
|
+
const solutionPath = join(issuesDir, 'solutions', `${issueId}.jsonl`);
|
|
164
|
+
if (existsSync(solutionPath)) {
|
|
165
|
+
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
|
166
|
+
if (solutions.length > 0) {
|
|
167
|
+
const boundSolution = solutions.find(s => s.is_bound) || solutions[0];
|
|
168
|
+
issue = {
|
|
169
|
+
id: issueId,
|
|
170
|
+
title: boundSolution?.description || issueId,
|
|
171
|
+
status: 'completed',
|
|
172
|
+
priority: 3,
|
|
173
|
+
context: boundSolution?.approach || '',
|
|
174
|
+
bound_solution_id: boundSolution?.id || null,
|
|
175
|
+
created_at: boundSolution?.created_at || new Date().toISOString(),
|
|
176
|
+
updated_at: new Date().toISOString(),
|
|
177
|
+
_reconstructed: true
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
160
183
|
if (!issue) return null;
|
|
161
184
|
|
|
162
185
|
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
|
@@ -254,11 +277,46 @@ function bindSolutionToIssue(issuesDir: string, issueId: string, solutionId: str
|
|
|
254
277
|
return { success: true, bound: solutionId };
|
|
255
278
|
}
|
|
256
279
|
|
|
280
|
+
// ========== Path Validation ==========
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Validate that the provided path is safe (no path traversal)
|
|
284
|
+
* Returns the resolved, normalized path or null if invalid
|
|
285
|
+
*/
|
|
286
|
+
function validateProjectPath(requestedPath: string, basePath: string): string | null {
|
|
287
|
+
if (!requestedPath) return basePath;
|
|
288
|
+
|
|
289
|
+
// Resolve to absolute path and normalize
|
|
290
|
+
const resolvedPath = resolve(normalize(requestedPath));
|
|
291
|
+
const resolvedBase = resolve(normalize(basePath));
|
|
292
|
+
|
|
293
|
+
// For local development tool, we allow any absolute path
|
|
294
|
+
// but prevent obvious traversal attempts
|
|
295
|
+
if (requestedPath.includes('..') && !resolvedPath.startsWith(resolvedBase)) {
|
|
296
|
+
// Check if it's trying to escape with ..
|
|
297
|
+
const normalizedRequested = normalize(requestedPath);
|
|
298
|
+
if (normalizedRequested.startsWith('..')) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return resolvedPath;
|
|
304
|
+
}
|
|
305
|
+
|
|
257
306
|
// ========== Route Handler ==========
|
|
258
307
|
|
|
259
308
|
export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
260
309
|
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
|
|
261
|
-
const
|
|
310
|
+
const rawProjectPath = url.searchParams.get('path') || initialPath;
|
|
311
|
+
|
|
312
|
+
// Validate project path to prevent path traversal
|
|
313
|
+
const projectPath = validateProjectPath(rawProjectPath, initialPath);
|
|
314
|
+
if (!projectPath) {
|
|
315
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
316
|
+
res.end(JSON.stringify({ error: 'Invalid project path' }));
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
262
320
|
const issuesDir = join(projectPath, '.workflow', 'issues');
|
|
263
321
|
|
|
264
322
|
// ===== Queue Routes (top-level /api/queue) =====
|
|
@@ -295,7 +353,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
295
353
|
|
|
296
354
|
// GET /api/queue/:id - Get specific queue by ID
|
|
297
355
|
const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
|
|
298
|
-
|
|
356
|
+
const reservedQueuePaths = ['history', 'reorder', 'switch', 'deactivate', 'merge'];
|
|
357
|
+
if (queueDetailMatch && req.method === 'GET' && !reservedQueuePaths.includes(queueDetailMatch[1])) {
|
|
299
358
|
const queueId = queueDetailMatch[1];
|
|
300
359
|
const queuesDir = join(issuesDir, 'queues');
|
|
301
360
|
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
|
@@ -347,6 +406,29 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
347
406
|
return true;
|
|
348
407
|
}
|
|
349
408
|
|
|
409
|
+
// POST /api/queue/deactivate - Deactivate current queue (set active to null)
|
|
410
|
+
if (pathname === '/api/queue/deactivate' && req.method === 'POST') {
|
|
411
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
412
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
413
|
+
const indexPath = join(queuesDir, 'index.json');
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
const index = existsSync(indexPath)
|
|
417
|
+
? JSON.parse(readFileSync(indexPath, 'utf8'))
|
|
418
|
+
: { active_queue_id: null, queues: [] };
|
|
419
|
+
|
|
420
|
+
const previousActiveId = index.active_queue_id;
|
|
421
|
+
index.active_queue_id = null;
|
|
422
|
+
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
423
|
+
|
|
424
|
+
return { success: true, previous_active_id: previousActiveId };
|
|
425
|
+
} catch (err) {
|
|
426
|
+
return { error: 'Failed to deactivate queue' };
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
|
|
350
432
|
// POST /api/queue/reorder - Reorder queue items (supports both solutions and tasks)
|
|
351
433
|
if (pathname === '/api/queue/reorder' && req.method === 'POST') {
|
|
352
434
|
handlePostRequest(req, res, async (body: any) => {
|
|
@@ -399,6 +481,195 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
399
481
|
return true;
|
|
400
482
|
}
|
|
401
483
|
|
|
484
|
+
// DELETE /api/queue/:queueId/item/:itemId - Delete item from queue
|
|
485
|
+
const queueItemDeleteMatch = pathname.match(/^\/api\/queue\/([^/]+)\/item\/([^/]+)$/);
|
|
486
|
+
if (queueItemDeleteMatch && req.method === 'DELETE') {
|
|
487
|
+
const queueId = queueItemDeleteMatch[1];
|
|
488
|
+
const itemId = decodeURIComponent(queueItemDeleteMatch[2]);
|
|
489
|
+
|
|
490
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
491
|
+
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
|
492
|
+
|
|
493
|
+
if (!existsSync(queueFilePath)) {
|
|
494
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
495
|
+
res.end(JSON.stringify({ error: `Queue ${queueId} not found` }));
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const queue = JSON.parse(readFileSync(queueFilePath, 'utf8'));
|
|
501
|
+
const items = queue.solutions || queue.tasks || [];
|
|
502
|
+
const filteredItems = items.filter((item: any) => item.item_id !== itemId);
|
|
503
|
+
|
|
504
|
+
if (filteredItems.length === items.length) {
|
|
505
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
506
|
+
res.end(JSON.stringify({ error: `Item ${itemId} not found in queue` }));
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Update queue items
|
|
511
|
+
if (queue.solutions) {
|
|
512
|
+
queue.solutions = filteredItems;
|
|
513
|
+
} else {
|
|
514
|
+
queue.tasks = filteredItems;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Recalculate metadata
|
|
518
|
+
const completedCount = filteredItems.filter((i: any) => i.status === 'completed').length;
|
|
519
|
+
queue._metadata = {
|
|
520
|
+
...queue._metadata,
|
|
521
|
+
updated_at: new Date().toISOString(),
|
|
522
|
+
...(queue.solutions
|
|
523
|
+
? { total_solutions: filteredItems.length, completed_solutions: completedCount }
|
|
524
|
+
: { total_tasks: filteredItems.length, completed_tasks: completedCount })
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
writeFileSync(queueFilePath, JSON.stringify(queue, null, 2));
|
|
528
|
+
|
|
529
|
+
// Update index counts
|
|
530
|
+
const indexPath = join(queuesDir, 'index.json');
|
|
531
|
+
if (existsSync(indexPath)) {
|
|
532
|
+
try {
|
|
533
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
534
|
+
const queueEntry = index.queues?.find((q: any) => q.id === queueId);
|
|
535
|
+
if (queueEntry) {
|
|
536
|
+
if (queue.solutions) {
|
|
537
|
+
queueEntry.total_solutions = filteredItems.length;
|
|
538
|
+
queueEntry.completed_solutions = completedCount;
|
|
539
|
+
} else {
|
|
540
|
+
queueEntry.total_tasks = filteredItems.length;
|
|
541
|
+
queueEntry.completed_tasks = completedCount;
|
|
542
|
+
}
|
|
543
|
+
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
544
|
+
}
|
|
545
|
+
} catch (err) {
|
|
546
|
+
console.error('Failed to update queue index:', err);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
551
|
+
res.end(JSON.stringify({ success: true, queueId, deletedItemId: itemId }));
|
|
552
|
+
} catch (err) {
|
|
553
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
554
|
+
res.end(JSON.stringify({ error: 'Failed to delete item' }));
|
|
555
|
+
}
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// POST /api/queue/merge - Merge source queue into target queue
|
|
560
|
+
if (pathname === '/api/queue/merge' && req.method === 'POST') {
|
|
561
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
562
|
+
const { sourceQueueId, targetQueueId } = body;
|
|
563
|
+
if (!sourceQueueId || !targetQueueId) {
|
|
564
|
+
return { error: 'sourceQueueId and targetQueueId required' };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (sourceQueueId === targetQueueId) {
|
|
568
|
+
return { error: 'Cannot merge queue into itself' };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
572
|
+
const sourcePath = join(queuesDir, `${sourceQueueId}.json`);
|
|
573
|
+
const targetPath = join(queuesDir, `${targetQueueId}.json`);
|
|
574
|
+
|
|
575
|
+
if (!existsSync(sourcePath)) return { error: `Source queue ${sourceQueueId} not found` };
|
|
576
|
+
if (!existsSync(targetPath)) return { error: `Target queue ${targetQueueId} not found` };
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
const sourceQueue = JSON.parse(readFileSync(sourcePath, 'utf8'));
|
|
580
|
+
const targetQueue = JSON.parse(readFileSync(targetPath, 'utf8'));
|
|
581
|
+
|
|
582
|
+
const sourceItems = sourceQueue.solutions || sourceQueue.tasks || [];
|
|
583
|
+
const targetItems = targetQueue.solutions || targetQueue.tasks || [];
|
|
584
|
+
const isSolutionBased = !!targetQueue.solutions;
|
|
585
|
+
|
|
586
|
+
// Re-index source items to avoid ID conflicts
|
|
587
|
+
const maxOrder = targetItems.reduce((max: number, i: any) => Math.max(max, i.execution_order || 0), 0);
|
|
588
|
+
const reindexedSourceItems = sourceItems.map((item: any, idx: number) => ({
|
|
589
|
+
...item,
|
|
590
|
+
item_id: `${item.item_id}-merged`,
|
|
591
|
+
execution_order: maxOrder + idx + 1,
|
|
592
|
+
execution_group: item.execution_group ? `M-${item.execution_group}` : 'M-ungrouped'
|
|
593
|
+
}));
|
|
594
|
+
|
|
595
|
+
// Merge items
|
|
596
|
+
const mergedItems = [...targetItems, ...reindexedSourceItems];
|
|
597
|
+
|
|
598
|
+
if (isSolutionBased) {
|
|
599
|
+
targetQueue.solutions = mergedItems;
|
|
600
|
+
} else {
|
|
601
|
+
targetQueue.tasks = mergedItems;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Merge issue_ids
|
|
605
|
+
const mergedIssueIds = [...new Set([
|
|
606
|
+
...(targetQueue.issue_ids || []),
|
|
607
|
+
...(sourceQueue.issue_ids || [])
|
|
608
|
+
])];
|
|
609
|
+
targetQueue.issue_ids = mergedIssueIds;
|
|
610
|
+
|
|
611
|
+
// Update metadata
|
|
612
|
+
const completedCount = mergedItems.filter((i: any) => i.status === 'completed').length;
|
|
613
|
+
targetQueue._metadata = {
|
|
614
|
+
...targetQueue._metadata,
|
|
615
|
+
updated_at: new Date().toISOString(),
|
|
616
|
+
...(isSolutionBased
|
|
617
|
+
? { total_solutions: mergedItems.length, completed_solutions: completedCount }
|
|
618
|
+
: { total_tasks: mergedItems.length, completed_tasks: completedCount })
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// Write merged queue
|
|
622
|
+
writeFileSync(targetPath, JSON.stringify(targetQueue, null, 2));
|
|
623
|
+
|
|
624
|
+
// Update source queue status
|
|
625
|
+
sourceQueue.status = 'merged';
|
|
626
|
+
sourceQueue._metadata = {
|
|
627
|
+
...sourceQueue._metadata,
|
|
628
|
+
merged_into: targetQueueId,
|
|
629
|
+
merged_at: new Date().toISOString()
|
|
630
|
+
};
|
|
631
|
+
writeFileSync(sourcePath, JSON.stringify(sourceQueue, null, 2));
|
|
632
|
+
|
|
633
|
+
// Update index
|
|
634
|
+
const indexPath = join(queuesDir, 'index.json');
|
|
635
|
+
if (existsSync(indexPath)) {
|
|
636
|
+
try {
|
|
637
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
638
|
+
const sourceEntry = index.queues?.find((q: any) => q.id === sourceQueueId);
|
|
639
|
+
const targetEntry = index.queues?.find((q: any) => q.id === targetQueueId);
|
|
640
|
+
if (sourceEntry) {
|
|
641
|
+
sourceEntry.status = 'merged';
|
|
642
|
+
}
|
|
643
|
+
if (targetEntry) {
|
|
644
|
+
if (isSolutionBased) {
|
|
645
|
+
targetEntry.total_solutions = mergedItems.length;
|
|
646
|
+
targetEntry.completed_solutions = completedCount;
|
|
647
|
+
} else {
|
|
648
|
+
targetEntry.total_tasks = mergedItems.length;
|
|
649
|
+
targetEntry.completed_tasks = completedCount;
|
|
650
|
+
}
|
|
651
|
+
targetEntry.issue_ids = mergedIssueIds;
|
|
652
|
+
}
|
|
653
|
+
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
654
|
+
} catch {
|
|
655
|
+
// Ignore index update errors
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
success: true,
|
|
661
|
+
sourceQueueId,
|
|
662
|
+
targetQueueId,
|
|
663
|
+
mergedItemCount: sourceItems.length,
|
|
664
|
+
totalItems: mergedItems.length
|
|
665
|
+
};
|
|
666
|
+
} catch (err) {
|
|
667
|
+
return { error: 'Failed to merge queues' };
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
|
|
402
673
|
// Legacy: GET /api/issues/queue (backward compat)
|
|
403
674
|
if (pathname === '/api/issues/queue' && req.method === 'GET') {
|
|
404
675
|
const queue = groupQueueByExecutionGroup(readQueue(issuesDir));
|