claude-code-workflow 6.3.29 → 6.3.31
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/cli.js +1 -1
- package/ccw/dist/cli.js.map +1 -1
- package/ccw/dist/commands/cli.d.ts +0 -1
- package/ccw/dist/commands/cli.d.ts.map +1 -1
- package/ccw/dist/commands/cli.js +3 -3
- package/ccw/dist/commands/cli.js.map +1 -1
- 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 +316 -4
- package/ccw/dist/core/routes/issue-routes.js.map +1 -1
- package/ccw/dist/tools/cli-executor-core.d.ts.map +1 -1
- package/ccw/dist/tools/cli-executor-core.js +6 -32
- package/ccw/dist/tools/cli-executor-core.js.map +1 -1
- package/ccw/src/cli.ts +1 -1
- package/ccw/src/commands/cli.ts +4 -4
- package/ccw/src/core/data-aggregator.ts +19 -7
- package/ccw/src/core/routes/issue-routes.ts +356 -4
- package/ccw/src/templates/dashboard-css/32-issue-manager.css +463 -37
- package/ccw/src/templates/dashboard-js/i18n.js +38 -0
- package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +5 -5
- package/ccw/src/templates/dashboard-js/views/issue-manager.js +852 -29
- package/ccw/src/templates/dashboard-js/views/skills-manager.js +2 -4
- package/ccw/src/tools/cli-executor-core.ts +7 -33
- 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 ==========
|
|
@@ -67,6 +67,12 @@ function readIssueHistoryJsonl(issuesDir: string): any[] {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function writeIssueHistoryJsonl(issuesDir: string, issues: any[]) {
|
|
71
|
+
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
|
|
72
|
+
const historyPath = join(issuesDir, 'issue-history.jsonl');
|
|
73
|
+
writeFileSync(historyPath, issues.map(i => JSON.stringify(i)).join('\n'));
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
function writeSolutionsJsonl(issuesDir: string, issueId: string, solutions: any[]) {
|
|
71
77
|
const solutionsDir = join(issuesDir, 'solutions');
|
|
72
78
|
if (!existsSync(solutionsDir)) mkdirSync(solutionsDir, { recursive: true });
|
|
@@ -156,7 +162,30 @@ function writeQueue(issuesDir: string, queue: any) {
|
|
|
156
162
|
|
|
157
163
|
function getIssueDetail(issuesDir: string, issueId: string) {
|
|
158
164
|
const issues = readIssuesJsonl(issuesDir);
|
|
159
|
-
|
|
165
|
+
let issue = issues.find(i => i.id === issueId);
|
|
166
|
+
|
|
167
|
+
// Fallback: Reconstruct issue from solution file if issue not in issues.jsonl
|
|
168
|
+
if (!issue) {
|
|
169
|
+
const solutionPath = join(issuesDir, 'solutions', `${issueId}.jsonl`);
|
|
170
|
+
if (existsSync(solutionPath)) {
|
|
171
|
+
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
|
172
|
+
if (solutions.length > 0) {
|
|
173
|
+
const boundSolution = solutions.find(s => s.is_bound) || solutions[0];
|
|
174
|
+
issue = {
|
|
175
|
+
id: issueId,
|
|
176
|
+
title: boundSolution?.description || issueId,
|
|
177
|
+
status: 'completed',
|
|
178
|
+
priority: 3,
|
|
179
|
+
context: boundSolution?.approach || '',
|
|
180
|
+
bound_solution_id: boundSolution?.id || null,
|
|
181
|
+
created_at: boundSolution?.created_at || new Date().toISOString(),
|
|
182
|
+
updated_at: new Date().toISOString(),
|
|
183
|
+
_reconstructed: true
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
160
189
|
if (!issue) return null;
|
|
161
190
|
|
|
162
191
|
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
|
@@ -254,11 +283,46 @@ function bindSolutionToIssue(issuesDir: string, issueId: string, solutionId: str
|
|
|
254
283
|
return { success: true, bound: solutionId };
|
|
255
284
|
}
|
|
256
285
|
|
|
286
|
+
// ========== Path Validation ==========
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Validate that the provided path is safe (no path traversal)
|
|
290
|
+
* Returns the resolved, normalized path or null if invalid
|
|
291
|
+
*/
|
|
292
|
+
function validateProjectPath(requestedPath: string, basePath: string): string | null {
|
|
293
|
+
if (!requestedPath) return basePath;
|
|
294
|
+
|
|
295
|
+
// Resolve to absolute path and normalize
|
|
296
|
+
const resolvedPath = resolve(normalize(requestedPath));
|
|
297
|
+
const resolvedBase = resolve(normalize(basePath));
|
|
298
|
+
|
|
299
|
+
// For local development tool, we allow any absolute path
|
|
300
|
+
// but prevent obvious traversal attempts
|
|
301
|
+
if (requestedPath.includes('..') && !resolvedPath.startsWith(resolvedBase)) {
|
|
302
|
+
// Check if it's trying to escape with ..
|
|
303
|
+
const normalizedRequested = normalize(requestedPath);
|
|
304
|
+
if (normalizedRequested.startsWith('..')) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return resolvedPath;
|
|
310
|
+
}
|
|
311
|
+
|
|
257
312
|
// ========== Route Handler ==========
|
|
258
313
|
|
|
259
314
|
export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
260
315
|
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
|
|
261
|
-
const
|
|
316
|
+
const rawProjectPath = url.searchParams.get('path') || initialPath;
|
|
317
|
+
|
|
318
|
+
// Validate project path to prevent path traversal
|
|
319
|
+
const projectPath = validateProjectPath(rawProjectPath, initialPath);
|
|
320
|
+
if (!projectPath) {
|
|
321
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
322
|
+
res.end(JSON.stringify({ error: 'Invalid project path' }));
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
262
326
|
const issuesDir = join(projectPath, '.workflow', 'issues');
|
|
263
327
|
|
|
264
328
|
// ===== Queue Routes (top-level /api/queue) =====
|
|
@@ -295,7 +359,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
295
359
|
|
|
296
360
|
// GET /api/queue/:id - Get specific queue by ID
|
|
297
361
|
const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
|
|
298
|
-
|
|
362
|
+
const reservedQueuePaths = ['history', 'reorder', 'switch', 'deactivate', 'merge'];
|
|
363
|
+
if (queueDetailMatch && req.method === 'GET' && !reservedQueuePaths.includes(queueDetailMatch[1])) {
|
|
299
364
|
const queueId = queueDetailMatch[1];
|
|
300
365
|
const queuesDir = join(issuesDir, 'queues');
|
|
301
366
|
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
|
@@ -347,6 +412,29 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
347
412
|
return true;
|
|
348
413
|
}
|
|
349
414
|
|
|
415
|
+
// POST /api/queue/deactivate - Deactivate current queue (set active to null)
|
|
416
|
+
if (pathname === '/api/queue/deactivate' && req.method === 'POST') {
|
|
417
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
418
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
419
|
+
const indexPath = join(queuesDir, 'index.json');
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const index = existsSync(indexPath)
|
|
423
|
+
? JSON.parse(readFileSync(indexPath, 'utf8'))
|
|
424
|
+
: { active_queue_id: null, queues: [] };
|
|
425
|
+
|
|
426
|
+
const previousActiveId = index.active_queue_id;
|
|
427
|
+
index.active_queue_id = null;
|
|
428
|
+
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
429
|
+
|
|
430
|
+
return { success: true, previous_active_id: previousActiveId };
|
|
431
|
+
} catch (err) {
|
|
432
|
+
return { error: 'Failed to deactivate queue' };
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
350
438
|
// POST /api/queue/reorder - Reorder queue items (supports both solutions and tasks)
|
|
351
439
|
if (pathname === '/api/queue/reorder' && req.method === 'POST') {
|
|
352
440
|
handlePostRequest(req, res, async (body: any) => {
|
|
@@ -399,6 +487,237 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
399
487
|
return true;
|
|
400
488
|
}
|
|
401
489
|
|
|
490
|
+
// DELETE /api/queue/:queueId/item/:itemId - Delete item from queue
|
|
491
|
+
const queueItemDeleteMatch = pathname.match(/^\/api\/queue\/([^/]+)\/item\/([^/]+)$/);
|
|
492
|
+
if (queueItemDeleteMatch && req.method === 'DELETE') {
|
|
493
|
+
const queueId = queueItemDeleteMatch[1];
|
|
494
|
+
const itemId = decodeURIComponent(queueItemDeleteMatch[2]);
|
|
495
|
+
|
|
496
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
497
|
+
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
|
498
|
+
|
|
499
|
+
if (!existsSync(queueFilePath)) {
|
|
500
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
501
|
+
res.end(JSON.stringify({ error: `Queue ${queueId} not found` }));
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
const queue = JSON.parse(readFileSync(queueFilePath, 'utf8'));
|
|
507
|
+
const items = queue.solutions || queue.tasks || [];
|
|
508
|
+
const filteredItems = items.filter((item: any) => item.item_id !== itemId);
|
|
509
|
+
|
|
510
|
+
if (filteredItems.length === items.length) {
|
|
511
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
512
|
+
res.end(JSON.stringify({ error: `Item ${itemId} not found in queue` }));
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Update queue items
|
|
517
|
+
if (queue.solutions) {
|
|
518
|
+
queue.solutions = filteredItems;
|
|
519
|
+
} else {
|
|
520
|
+
queue.tasks = filteredItems;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Recalculate metadata
|
|
524
|
+
const completedCount = filteredItems.filter((i: any) => i.status === 'completed').length;
|
|
525
|
+
queue._metadata = {
|
|
526
|
+
...queue._metadata,
|
|
527
|
+
updated_at: new Date().toISOString(),
|
|
528
|
+
...(queue.solutions
|
|
529
|
+
? { total_solutions: filteredItems.length, completed_solutions: completedCount }
|
|
530
|
+
: { total_tasks: filteredItems.length, completed_tasks: completedCount })
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
writeFileSync(queueFilePath, JSON.stringify(queue, null, 2));
|
|
534
|
+
|
|
535
|
+
// Update index counts
|
|
536
|
+
const indexPath = join(queuesDir, 'index.json');
|
|
537
|
+
if (existsSync(indexPath)) {
|
|
538
|
+
try {
|
|
539
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
540
|
+
const queueEntry = index.queues?.find((q: any) => q.id === queueId);
|
|
541
|
+
if (queueEntry) {
|
|
542
|
+
if (queue.solutions) {
|
|
543
|
+
queueEntry.total_solutions = filteredItems.length;
|
|
544
|
+
queueEntry.completed_solutions = completedCount;
|
|
545
|
+
} else {
|
|
546
|
+
queueEntry.total_tasks = filteredItems.length;
|
|
547
|
+
queueEntry.completed_tasks = completedCount;
|
|
548
|
+
}
|
|
549
|
+
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
550
|
+
}
|
|
551
|
+
} catch (err) {
|
|
552
|
+
console.error('Failed to update queue index:', err);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
557
|
+
res.end(JSON.stringify({ success: true, queueId, deletedItemId: itemId }));
|
|
558
|
+
} catch (err) {
|
|
559
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
560
|
+
res.end(JSON.stringify({ error: 'Failed to delete item' }));
|
|
561
|
+
}
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// DELETE /api/queue/:queueId - Delete entire queue
|
|
566
|
+
const queueDeleteMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
|
|
567
|
+
if (queueDeleteMatch && req.method === 'DELETE') {
|
|
568
|
+
const queueId = queueDeleteMatch[1];
|
|
569
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
570
|
+
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
|
571
|
+
const indexPath = join(queuesDir, 'index.json');
|
|
572
|
+
|
|
573
|
+
if (!existsSync(queueFilePath)) {
|
|
574
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
575
|
+
res.end(JSON.stringify({ error: `Queue ${queueId} not found` }));
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
// Delete queue file
|
|
581
|
+
unlinkSync(queueFilePath);
|
|
582
|
+
|
|
583
|
+
// Update index
|
|
584
|
+
if (existsSync(indexPath)) {
|
|
585
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
586
|
+
|
|
587
|
+
// Remove from queues array
|
|
588
|
+
index.queues = (index.queues || []).filter((q: any) => q.id !== queueId);
|
|
589
|
+
|
|
590
|
+
// Clear active if this was the active queue
|
|
591
|
+
if (index.active_queue_id === queueId) {
|
|
592
|
+
index.active_queue_id = null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
599
|
+
res.end(JSON.stringify({ success: true, deletedQueueId: queueId }));
|
|
600
|
+
} catch (err) {
|
|
601
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
602
|
+
res.end(JSON.stringify({ error: 'Failed to delete queue' }));
|
|
603
|
+
}
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// POST /api/queue/merge - Merge source queue into target queue
|
|
608
|
+
if (pathname === '/api/queue/merge' && req.method === 'POST') {
|
|
609
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
610
|
+
const { sourceQueueId, targetQueueId } = body;
|
|
611
|
+
if (!sourceQueueId || !targetQueueId) {
|
|
612
|
+
return { error: 'sourceQueueId and targetQueueId required' };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (sourceQueueId === targetQueueId) {
|
|
616
|
+
return { error: 'Cannot merge queue into itself' };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
620
|
+
const sourcePath = join(queuesDir, `${sourceQueueId}.json`);
|
|
621
|
+
const targetPath = join(queuesDir, `${targetQueueId}.json`);
|
|
622
|
+
|
|
623
|
+
if (!existsSync(sourcePath)) return { error: `Source queue ${sourceQueueId} not found` };
|
|
624
|
+
if (!existsSync(targetPath)) return { error: `Target queue ${targetQueueId} not found` };
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
const sourceQueue = JSON.parse(readFileSync(sourcePath, 'utf8'));
|
|
628
|
+
const targetQueue = JSON.parse(readFileSync(targetPath, 'utf8'));
|
|
629
|
+
|
|
630
|
+
const sourceItems = sourceQueue.solutions || sourceQueue.tasks || [];
|
|
631
|
+
const targetItems = targetQueue.solutions || targetQueue.tasks || [];
|
|
632
|
+
const isSolutionBased = !!targetQueue.solutions;
|
|
633
|
+
|
|
634
|
+
// Re-index source items to avoid ID conflicts
|
|
635
|
+
const maxOrder = targetItems.reduce((max: number, i: any) => Math.max(max, i.execution_order || 0), 0);
|
|
636
|
+
const reindexedSourceItems = sourceItems.map((item: any, idx: number) => ({
|
|
637
|
+
...item,
|
|
638
|
+
item_id: `${item.item_id}-merged`,
|
|
639
|
+
execution_order: maxOrder + idx + 1,
|
|
640
|
+
execution_group: item.execution_group ? `M-${item.execution_group}` : 'M-ungrouped'
|
|
641
|
+
}));
|
|
642
|
+
|
|
643
|
+
// Merge items
|
|
644
|
+
const mergedItems = [...targetItems, ...reindexedSourceItems];
|
|
645
|
+
|
|
646
|
+
if (isSolutionBased) {
|
|
647
|
+
targetQueue.solutions = mergedItems;
|
|
648
|
+
} else {
|
|
649
|
+
targetQueue.tasks = mergedItems;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Merge issue_ids
|
|
653
|
+
const mergedIssueIds = [...new Set([
|
|
654
|
+
...(targetQueue.issue_ids || []),
|
|
655
|
+
...(sourceQueue.issue_ids || [])
|
|
656
|
+
])];
|
|
657
|
+
targetQueue.issue_ids = mergedIssueIds;
|
|
658
|
+
|
|
659
|
+
// Update metadata
|
|
660
|
+
const completedCount = mergedItems.filter((i: any) => i.status === 'completed').length;
|
|
661
|
+
targetQueue._metadata = {
|
|
662
|
+
...targetQueue._metadata,
|
|
663
|
+
updated_at: new Date().toISOString(),
|
|
664
|
+
...(isSolutionBased
|
|
665
|
+
? { total_solutions: mergedItems.length, completed_solutions: completedCount }
|
|
666
|
+
: { total_tasks: mergedItems.length, completed_tasks: completedCount })
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// Write merged queue
|
|
670
|
+
writeFileSync(targetPath, JSON.stringify(targetQueue, null, 2));
|
|
671
|
+
|
|
672
|
+
// Update source queue status
|
|
673
|
+
sourceQueue.status = 'merged';
|
|
674
|
+
sourceQueue._metadata = {
|
|
675
|
+
...sourceQueue._metadata,
|
|
676
|
+
merged_into: targetQueueId,
|
|
677
|
+
merged_at: new Date().toISOString()
|
|
678
|
+
};
|
|
679
|
+
writeFileSync(sourcePath, JSON.stringify(sourceQueue, null, 2));
|
|
680
|
+
|
|
681
|
+
// Update index
|
|
682
|
+
const indexPath = join(queuesDir, 'index.json');
|
|
683
|
+
if (existsSync(indexPath)) {
|
|
684
|
+
try {
|
|
685
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
686
|
+
const sourceEntry = index.queues?.find((q: any) => q.id === sourceQueueId);
|
|
687
|
+
const targetEntry = index.queues?.find((q: any) => q.id === targetQueueId);
|
|
688
|
+
if (sourceEntry) {
|
|
689
|
+
sourceEntry.status = 'merged';
|
|
690
|
+
}
|
|
691
|
+
if (targetEntry) {
|
|
692
|
+
if (isSolutionBased) {
|
|
693
|
+
targetEntry.total_solutions = mergedItems.length;
|
|
694
|
+
targetEntry.completed_solutions = completedCount;
|
|
695
|
+
} else {
|
|
696
|
+
targetEntry.total_tasks = mergedItems.length;
|
|
697
|
+
targetEntry.completed_tasks = completedCount;
|
|
698
|
+
}
|
|
699
|
+
targetEntry.issue_ids = mergedIssueIds;
|
|
700
|
+
}
|
|
701
|
+
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
702
|
+
} catch {
|
|
703
|
+
// Ignore index update errors
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
success: true,
|
|
709
|
+
sourceQueueId,
|
|
710
|
+
targetQueueId,
|
|
711
|
+
mergedItemCount: sourceItems.length,
|
|
712
|
+
totalItems: mergedItems.length
|
|
713
|
+
};
|
|
714
|
+
} catch (err) {
|
|
715
|
+
return { error: 'Failed to merge queues' };
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
|
|
402
721
|
// Legacy: GET /api/issues/queue (backward compat)
|
|
403
722
|
if (pathname === '/api/issues/queue' && req.method === 'GET') {
|
|
404
723
|
const queue = groupQueueByExecutionGroup(readQueue(issuesDir));
|
|
@@ -546,6 +865,39 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
546
865
|
return true;
|
|
547
866
|
}
|
|
548
867
|
|
|
868
|
+
// POST /api/issues/:id/archive - Archive issue (move to history)
|
|
869
|
+
const archiveMatch = pathname.match(/^\/api\/issues\/([^/]+)\/archive$/);
|
|
870
|
+
if (archiveMatch && req.method === 'POST') {
|
|
871
|
+
const issueId = decodeURIComponent(archiveMatch[1]);
|
|
872
|
+
|
|
873
|
+
const issues = readIssuesJsonl(issuesDir);
|
|
874
|
+
const issueIndex = issues.findIndex(i => i.id === issueId);
|
|
875
|
+
|
|
876
|
+
if (issueIndex === -1) {
|
|
877
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
878
|
+
res.end(JSON.stringify({ error: 'Issue not found' }));
|
|
879
|
+
return true;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Get the issue and add archive metadata
|
|
883
|
+
const issue = issues[issueIndex];
|
|
884
|
+
issue.archived_at = new Date().toISOString();
|
|
885
|
+
issue.status = 'completed';
|
|
886
|
+
|
|
887
|
+
// Move to history
|
|
888
|
+
const history = readIssueHistoryJsonl(issuesDir);
|
|
889
|
+
history.push(issue);
|
|
890
|
+
writeIssueHistoryJsonl(issuesDir, history);
|
|
891
|
+
|
|
892
|
+
// Remove from active issues
|
|
893
|
+
issues.splice(issueIndex, 1);
|
|
894
|
+
writeIssuesJsonl(issuesDir, issues);
|
|
895
|
+
|
|
896
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
897
|
+
res.end(JSON.stringify({ success: true, issueId, archivedAt: issue.archived_at }));
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
|
|
549
901
|
// POST /api/issues/:id/solutions - Add solution
|
|
550
902
|
const addSolMatch = pathname.match(/^\/api\/issues\/([^/]+)\/solutions$/);
|
|
551
903
|
if (addSolMatch && req.method === 'POST') {
|