kanbango 2.0.0 → 2.4.0
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/AGENTS.md +7 -7
- package/API.md +20 -12
- package/CHANGELOG.md +40 -0
- package/LLM_AGENTS.md +141 -175
- package/README.md +78 -266
- package/bin/kanban.js +101 -26
- package/index.html +162 -85
- package/index.js +2 -0
- package/kan2.md +76 -0
- package/kanban.js +284 -35
- package/mcp-server.js +393 -184
- package/package.json +1 -1
- package/plan.js +159 -0
- package/planv2.md +17 -7
- package/tests/run.js +1 -0
package/kanban.js
CHANGED
|
@@ -3,6 +3,10 @@ const path = require('path');
|
|
|
3
3
|
|
|
4
4
|
const BACKLOG = path.join(process.cwd(), 'backlog');
|
|
5
5
|
const COLS = ['active', 'planned', 'icebox', 'done'];
|
|
6
|
+
const GUI_PORT_FILE = '.kanbango-gui.json';
|
|
7
|
+
const GUI_PORT_MIN = 5510;
|
|
8
|
+
const GUI_PORT_MAX = 5999;
|
|
9
|
+
const GUI_PORT_SPAN = GUI_PORT_MAX - GUI_PORT_MIN + 1;
|
|
6
10
|
const STATUS_MAP = {
|
|
7
11
|
active: 'in_progress',
|
|
8
12
|
planned: 'planned',
|
|
@@ -10,9 +14,9 @@ const STATUS_MAP = {
|
|
|
10
14
|
done: 'done'
|
|
11
15
|
};
|
|
12
16
|
const VIEW_FIELDS = {
|
|
13
|
-
summary: ['
|
|
17
|
+
summary: ['task_number', 'title', 'column', 'epic_group', 'created', 'progress'],
|
|
14
18
|
planning: [
|
|
15
|
-
'
|
|
19
|
+
'task_number',
|
|
16
20
|
'title',
|
|
17
21
|
'column',
|
|
18
22
|
'epic_group',
|
|
@@ -20,10 +24,13 @@ const VIEW_FIELDS = {
|
|
|
20
24
|
'progress',
|
|
21
25
|
'description',
|
|
22
26
|
'specs',
|
|
23
|
-
'
|
|
27
|
+
'in_scope',
|
|
28
|
+
'out_of_scope',
|
|
29
|
+
'acceptance_criteria',
|
|
30
|
+
'test_cases'
|
|
24
31
|
],
|
|
25
32
|
execution: [
|
|
26
|
-
'
|
|
33
|
+
'task_number',
|
|
27
34
|
'title',
|
|
28
35
|
'column',
|
|
29
36
|
'epic_group',
|
|
@@ -31,11 +38,14 @@ const VIEW_FIELDS = {
|
|
|
31
38
|
'progress',
|
|
32
39
|
'description',
|
|
33
40
|
'specs',
|
|
41
|
+
'in_scope',
|
|
42
|
+
'out_of_scope',
|
|
34
43
|
'acceptance_criteria',
|
|
44
|
+
'test_cases',
|
|
35
45
|
'subtasks'
|
|
36
46
|
],
|
|
37
47
|
full: [
|
|
38
|
-
'
|
|
48
|
+
'task_number',
|
|
39
49
|
'title',
|
|
40
50
|
'column',
|
|
41
51
|
'epic_group',
|
|
@@ -43,7 +53,10 @@ const VIEW_FIELDS = {
|
|
|
43
53
|
'progress',
|
|
44
54
|
'description',
|
|
45
55
|
'specs',
|
|
56
|
+
'in_scope',
|
|
57
|
+
'out_of_scope',
|
|
46
58
|
'acceptance_criteria',
|
|
59
|
+
'test_cases',
|
|
47
60
|
'subtasks',
|
|
48
61
|
'notes'
|
|
49
62
|
]
|
|
@@ -71,6 +84,11 @@ function normalizeString(value, fallback = '') {
|
|
|
71
84
|
return typeof value === 'string' ? value.trim() : fallback;
|
|
72
85
|
}
|
|
73
86
|
|
|
87
|
+
function extractTaskNumber(taskId) {
|
|
88
|
+
const match = normalizeString(taskId).match(/^(?:[A-Z]+-)?(\d+)/);
|
|
89
|
+
return match ? parseInt(match[1], 10) : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
74
92
|
function normalizeStringArray(value) {
|
|
75
93
|
if (!Array.isArray(value)) return [];
|
|
76
94
|
return value
|
|
@@ -88,33 +106,48 @@ function normalizeSubtasks(value) {
|
|
|
88
106
|
})).filter((subtask) => subtask.text);
|
|
89
107
|
}
|
|
90
108
|
|
|
91
|
-
function
|
|
109
|
+
function normalizeEvidence(value) {
|
|
110
|
+
if (!Array.isArray(value)) return [];
|
|
111
|
+
return value.map((item) => ({
|
|
112
|
+
diff: normalizeString(item && item.diff),
|
|
113
|
+
test_command: normalizeString(item && item.test_command),
|
|
114
|
+
stdout: normalizeString(item && item.stdout),
|
|
115
|
+
stderr: normalizeString(item && item.stderr),
|
|
116
|
+
exit_code: Number.isInteger(item && item.exit_code) ? item.exit_code : null,
|
|
117
|
+
created: normalizeString(item && item.created) || todayIso()
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizePlan(value) {
|
|
122
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
92
123
|
return {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
id: subtask.id,
|
|
96
|
-
text: subtask.text,
|
|
97
|
-
done: subtask.done,
|
|
98
|
-
description: subtask.description
|
|
99
|
-
}))
|
|
124
|
+
runner: value.runner && typeof value.runner === 'object' ? value.runner : null,
|
|
125
|
+
status: ['active', 'done'].includes(value.status) ? value.status : 'active'
|
|
100
126
|
};
|
|
101
127
|
}
|
|
102
128
|
|
|
103
129
|
function normalizeTask(task) {
|
|
130
|
+
const id = normalizeString(task.id);
|
|
104
131
|
const normalized = {
|
|
105
|
-
id
|
|
106
|
-
title: stripTitlePrefix(task.title ||
|
|
132
|
+
id,
|
|
133
|
+
title: stripTitlePrefix(task.title || id),
|
|
107
134
|
column: COLS.includes(task.column) ? task.column : 'planned',
|
|
108
135
|
epic_group: normalizeString(task.epic_group, '—') || '—',
|
|
109
136
|
created: normalizeString(task.created) || todayIso(),
|
|
110
137
|
description: normalizeString(task.description),
|
|
111
138
|
specs: normalizeString(task.specs),
|
|
139
|
+
in_scope: normalizeStringArray(task.in_scope),
|
|
140
|
+
out_of_scope: normalizeStringArray(task.out_of_scope),
|
|
112
141
|
acceptance_criteria: normalizeStringArray(task.acceptance_criteria),
|
|
113
|
-
|
|
114
|
-
|
|
142
|
+
test_cases: normalizeStringArray(task.test_cases),
|
|
143
|
+
subtasks: normalizeSubtasks(task.subtasks),
|
|
144
|
+
notes: normalizeString(task.notes),
|
|
145
|
+
plan: normalizePlan(task.plan),
|
|
146
|
+
evidence: normalizeEvidence(task.evidence),
|
|
147
|
+
task_number: extractTaskNumber(id)
|
|
115
148
|
};
|
|
116
149
|
|
|
117
|
-
return
|
|
150
|
+
return normalized;
|
|
118
151
|
}
|
|
119
152
|
|
|
120
153
|
function serializeTask(task) {
|
|
@@ -127,9 +160,15 @@ function serializeTask(task) {
|
|
|
127
160
|
created: normalized.created,
|
|
128
161
|
description: normalized.description,
|
|
129
162
|
specs: normalized.specs,
|
|
163
|
+
in_scope: normalized.in_scope,
|
|
164
|
+
out_of_scope: normalized.out_of_scope,
|
|
130
165
|
acceptance_criteria: normalized.acceptance_criteria,
|
|
166
|
+
test_cases: normalized.test_cases,
|
|
131
167
|
subtasks: normalized.subtasks,
|
|
132
|
-
notes: normalized.notes
|
|
168
|
+
notes: normalized.notes,
|
|
169
|
+
plan: normalized.plan,
|
|
170
|
+
evidence: normalized.evidence,
|
|
171
|
+
task_number: normalized.task_number
|
|
133
172
|
};
|
|
134
173
|
}
|
|
135
174
|
|
|
@@ -147,10 +186,6 @@ function pickFields(task, fieldNames) {
|
|
|
147
186
|
picked.progress = getProgress(task);
|
|
148
187
|
continue;
|
|
149
188
|
}
|
|
150
|
-
if (field === 'tasks') {
|
|
151
|
-
picked.tasks = task.tasks;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
189
|
if (field in task) {
|
|
155
190
|
picked[field] = task[field];
|
|
156
191
|
}
|
|
@@ -226,7 +261,10 @@ async function parseMarkdownTask(filePath, column) {
|
|
|
226
261
|
created: createdMatch ? createdMatch[1].trim() : todayIso(),
|
|
227
262
|
description: extractSection(text, ['Opis', 'Description']),
|
|
228
263
|
specs: extractSection(text, ['Specs', 'Specyfikacja']),
|
|
264
|
+
in_scope: parseListSection(extractSection(text, ['In Scope', 'W Zakresie'])),
|
|
265
|
+
out_of_scope: parseListSection(extractSection(text, ['Out of Scope', 'Poza Zakresem'])),
|
|
229
266
|
acceptance_criteria: parseListSection(extractSection(text, ['Acceptance Criteria', 'Kryteria Akceptacji'])),
|
|
267
|
+
test_cases: parseListSection(extractSection(text, ['Test Cases', 'Przypadki Testowe'])),
|
|
230
268
|
subtasks,
|
|
231
269
|
notes: extractSection(text, ['Notes'])
|
|
232
270
|
});
|
|
@@ -317,7 +355,8 @@ async function findFile(epicId) {
|
|
|
317
355
|
}
|
|
318
356
|
|
|
319
357
|
async function getTask(taskId) {
|
|
320
|
-
const
|
|
358
|
+
const resolvedId = await resolveTaskId(taskId);
|
|
359
|
+
const filePath = await findFile(resolvedId);
|
|
321
360
|
if (!filePath) {
|
|
322
361
|
throw createKanbanError(
|
|
323
362
|
'TASK_NOT_FOUND',
|
|
@@ -333,6 +372,34 @@ async function getTask(taskId) {
|
|
|
333
372
|
return parseEpic(filePath, column);
|
|
334
373
|
}
|
|
335
374
|
|
|
375
|
+
async function resolveTaskId(input) {
|
|
376
|
+
if (!input) return null;
|
|
377
|
+
|
|
378
|
+
const exact = await findFile(String(input));
|
|
379
|
+
if (exact) return String(input);
|
|
380
|
+
|
|
381
|
+
const num = parseInt(String(input), 10);
|
|
382
|
+
if (!Number.isFinite(num)) return null;
|
|
383
|
+
|
|
384
|
+
for (const col of COLS) {
|
|
385
|
+
const colDir = path.join(BACKLOG, col);
|
|
386
|
+
try {
|
|
387
|
+
const files = await fs.readdir(colDir);
|
|
388
|
+
const rawPattern = new RegExp(`^(?:[A-Z]+-)?${String(num)}(?:-|$)`);
|
|
389
|
+
const paddedPattern = new RegExp(`^(?:[A-Z]+-)?${String(num).padStart(3, '0')}(?:-|$)`);
|
|
390
|
+
const match = files.find((file) => {
|
|
391
|
+
const base = path.basename(file, path.extname(file));
|
|
392
|
+
return rawPattern.test(base) || paddedPattern.test(base + '-');
|
|
393
|
+
});
|
|
394
|
+
if (match) return path.basename(match, path.extname(match));
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error.code !== 'ENOENT') throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return String(input);
|
|
401
|
+
}
|
|
402
|
+
|
|
336
403
|
async function writeTask(task, previousFilePath = null) {
|
|
337
404
|
const normalized = normalizeTask(task);
|
|
338
405
|
await ensureBacklogDir();
|
|
@@ -397,7 +464,7 @@ async function migrateAll(options = {}) {
|
|
|
397
464
|
return { migrated, errors };
|
|
398
465
|
}
|
|
399
466
|
|
|
400
|
-
async function
|
|
467
|
+
async function nextTaskNumber() {
|
|
401
468
|
const ids = [];
|
|
402
469
|
|
|
403
470
|
for (const col of COLS) {
|
|
@@ -405,7 +472,7 @@ async function nextPiNumber() {
|
|
|
405
472
|
try {
|
|
406
473
|
const files = await fs.readdir(colDir);
|
|
407
474
|
for (const file of files) {
|
|
408
|
-
const match = file.match(/^
|
|
475
|
+
const match = file.match(/^(?:[A-Z]+-)?(\d+)/);
|
|
409
476
|
if (match) ids.push(parseInt(match[1], 10));
|
|
410
477
|
}
|
|
411
478
|
} catch (error) {
|
|
@@ -456,23 +523,28 @@ async function doCreate(title, column = 'planned', epicGroup = '—', extra = {}
|
|
|
456
523
|
|
|
457
524
|
validateColumn(column, 'col');
|
|
458
525
|
|
|
459
|
-
const nextId = await
|
|
526
|
+
const nextId = await nextTaskNumber();
|
|
460
527
|
const slug = title
|
|
461
528
|
.toLowerCase()
|
|
462
529
|
.replace(/[^a-z0-9]+/g, '-')
|
|
463
530
|
.replace(/^-+|-+$/g, '')
|
|
464
531
|
.substring(0, 25);
|
|
465
532
|
const task = normalizeTask({
|
|
466
|
-
id:
|
|
533
|
+
id: String(nextId).padStart(3, '0'),
|
|
467
534
|
title,
|
|
468
535
|
column,
|
|
469
536
|
epic_group: epicGroup || '—',
|
|
470
537
|
created: todayIso(),
|
|
471
538
|
description: extra.description,
|
|
472
539
|
specs: extra.specs,
|
|
540
|
+
in_scope: extra.in_scope,
|
|
541
|
+
out_of_scope: extra.out_of_scope,
|
|
473
542
|
acceptance_criteria: extra.acceptance_criteria,
|
|
543
|
+
test_cases: extra.test_cases,
|
|
474
544
|
subtasks: extra.subtasks,
|
|
475
|
-
notes: extra.notes
|
|
545
|
+
notes: extra.notes,
|
|
546
|
+
plan: extra.plan,
|
|
547
|
+
evidence: extra.evidence
|
|
476
548
|
});
|
|
477
549
|
|
|
478
550
|
return writeTask(task);
|
|
@@ -481,7 +553,8 @@ async function doCreate(title, column = 'planned', epicGroup = '—', extra = {}
|
|
|
481
553
|
async function updateTask(taskId, patch) {
|
|
482
554
|
validatePatch(patch);
|
|
483
555
|
|
|
484
|
-
const
|
|
556
|
+
const resolvedId = await resolveTaskId(taskId);
|
|
557
|
+
const previousFilePath = await findFile(resolvedId);
|
|
485
558
|
if (!previousFilePath) {
|
|
486
559
|
throw createKanbanError(
|
|
487
560
|
'TASK_NOT_FOUND',
|
|
@@ -517,6 +590,32 @@ async function updateTask(taskId, patch) {
|
|
|
517
590
|
if (patch.epic_group !== undefined) next.epic_group = normalizeString(patch.epic_group, '—') || '—';
|
|
518
591
|
if (patch.description !== undefined) next.description = normalizeString(patch.description);
|
|
519
592
|
if (patch.specs !== undefined) next.specs = normalizeString(patch.specs);
|
|
593
|
+
if (patch.in_scope !== undefined) {
|
|
594
|
+
if (!Array.isArray(patch.in_scope)) {
|
|
595
|
+
throw createKanbanError(
|
|
596
|
+
'VALIDATION_ERROR',
|
|
597
|
+
'in_scope must be an array of strings',
|
|
598
|
+
'Send in_scope as an array',
|
|
599
|
+
{ field: 'in_scope' },
|
|
600
|
+
false,
|
|
601
|
+
400
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
next.in_scope = patch.in_scope;
|
|
605
|
+
}
|
|
606
|
+
if (patch.out_of_scope !== undefined) {
|
|
607
|
+
if (!Array.isArray(patch.out_of_scope)) {
|
|
608
|
+
throw createKanbanError(
|
|
609
|
+
'VALIDATION_ERROR',
|
|
610
|
+
'out_of_scope must be an array of strings',
|
|
611
|
+
'Send out_of_scope as an array',
|
|
612
|
+
{ field: 'out_of_scope' },
|
|
613
|
+
false,
|
|
614
|
+
400
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
next.out_of_scope = patch.out_of_scope;
|
|
618
|
+
}
|
|
520
619
|
if (patch.acceptance_criteria !== undefined) {
|
|
521
620
|
if (!Array.isArray(patch.acceptance_criteria)) {
|
|
522
621
|
throw createKanbanError(
|
|
@@ -530,9 +629,21 @@ async function updateTask(taskId, patch) {
|
|
|
530
629
|
}
|
|
531
630
|
next.acceptance_criteria = patch.acceptance_criteria;
|
|
532
631
|
}
|
|
533
|
-
if (patch.
|
|
534
|
-
|
|
535
|
-
|
|
632
|
+
if (patch.test_cases !== undefined) {
|
|
633
|
+
if (!Array.isArray(patch.test_cases)) {
|
|
634
|
+
throw createKanbanError(
|
|
635
|
+
'VALIDATION_ERROR',
|
|
636
|
+
'test_cases must be an array of strings',
|
|
637
|
+
'Send test_cases as an array',
|
|
638
|
+
{ field: 'test_cases' },
|
|
639
|
+
false,
|
|
640
|
+
400
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
next.test_cases = patch.test_cases;
|
|
644
|
+
}
|
|
645
|
+
if (patch.subtasks !== undefined) {
|
|
646
|
+
if (!Array.isArray(patch.subtasks)) {
|
|
536
647
|
throw createKanbanError(
|
|
537
648
|
'VALIDATION_ERROR',
|
|
538
649
|
'subtasks must be an array',
|
|
@@ -542,9 +653,23 @@ async function updateTask(taskId, patch) {
|
|
|
542
653
|
400
|
|
543
654
|
);
|
|
544
655
|
}
|
|
545
|
-
next.subtasks = subtasks;
|
|
656
|
+
next.subtasks = patch.subtasks;
|
|
546
657
|
}
|
|
547
658
|
if (patch.notes !== undefined) next.notes = normalizeString(patch.notes);
|
|
659
|
+
if (patch.plan !== undefined) next.plan = patch.plan;
|
|
660
|
+
if (patch.evidence !== undefined) {
|
|
661
|
+
if (!Array.isArray(patch.evidence)) {
|
|
662
|
+
throw createKanbanError(
|
|
663
|
+
'VALIDATION_ERROR',
|
|
664
|
+
'evidence must be an array',
|
|
665
|
+
'Send evidence as an array of evidence objects',
|
|
666
|
+
{ field: 'evidence' },
|
|
667
|
+
false,
|
|
668
|
+
400
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
next.evidence = patch.evidence;
|
|
672
|
+
}
|
|
548
673
|
|
|
549
674
|
return writeTask(next, previousFilePath);
|
|
550
675
|
}
|
|
@@ -604,6 +729,118 @@ async function doUpdate(epicId, newTitle, newTasks) {
|
|
|
604
729
|
}
|
|
605
730
|
}
|
|
606
731
|
|
|
732
|
+
function guiPortFilePath() {
|
|
733
|
+
return path.join(BACKLOG, GUI_PORT_FILE);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function hashCwdToPort(cwd = process.cwd()) {
|
|
737
|
+
let hash = 0;
|
|
738
|
+
const input = String(cwd);
|
|
739
|
+
for (let i = 0; i < input.length; i++) {
|
|
740
|
+
hash = ((hash << 5) - hash + input.charCodeAt(i)) | 0;
|
|
741
|
+
}
|
|
742
|
+
return GUI_PORT_MIN + (Math.abs(hash) % GUI_PORT_SPAN);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function normalizeGuiPort(value) {
|
|
746
|
+
const parsed = Number.parseInt(value, 10);
|
|
747
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
return parsed;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function resolvePreferredGuiPort(explicitPort) {
|
|
754
|
+
if (explicitPort !== undefined && explicitPort !== null && explicitPort !== '') {
|
|
755
|
+
const fromArg = normalizeGuiPort(explicitPort);
|
|
756
|
+
if (fromArg) return fromArg;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const fromEnv = normalizeGuiPort(process.env.KANBANGO_GUI_PORT);
|
|
760
|
+
if (fromEnv) return fromEnv;
|
|
761
|
+
|
|
762
|
+
return hashCwdToPort(process.cwd());
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function isPidAlive(pid) {
|
|
766
|
+
const n = Number.parseInt(pid, 10);
|
|
767
|
+
if (!Number.isFinite(n) || n <= 0) return false;
|
|
768
|
+
try {
|
|
769
|
+
process.kill(n, 0);
|
|
770
|
+
return true;
|
|
771
|
+
} catch {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function writeGuiPortFile({ port, pid = process.pid } = {}) {
|
|
777
|
+
const normalizedPort = normalizeGuiPort(port);
|
|
778
|
+
if (!normalizedPort) {
|
|
779
|
+
throw createKanbanError(
|
|
780
|
+
'VALIDATION_ERROR',
|
|
781
|
+
'Invalid GUI port',
|
|
782
|
+
'Use an integer between 1 and 65535',
|
|
783
|
+
{ port },
|
|
784
|
+
false,
|
|
785
|
+
400
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
await ensureBacklogDir();
|
|
790
|
+
const data = {
|
|
791
|
+
port: normalizedPort,
|
|
792
|
+
pid,
|
|
793
|
+
url: `http://localhost:${normalizedPort}`,
|
|
794
|
+
cwd: process.cwd(),
|
|
795
|
+
started_at: new Date().toISOString()
|
|
796
|
+
};
|
|
797
|
+
await fs.writeFile(guiPortFilePath(), JSON.stringify(data, null, 2), 'utf-8');
|
|
798
|
+
return data;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async function readGuiPortFile() {
|
|
802
|
+
try {
|
|
803
|
+
const raw = await fs.readFile(guiPortFilePath(), 'utf-8');
|
|
804
|
+
const data = JSON.parse(raw);
|
|
805
|
+
if (!data || !normalizeGuiPort(data.port)) return null;
|
|
806
|
+
return data;
|
|
807
|
+
} catch (error) {
|
|
808
|
+
if (error.code === 'ENOENT') return null;
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async function clearGuiPortFile({ pid, force = false } = {}) {
|
|
814
|
+
const info = await readGuiPortFile();
|
|
815
|
+
if (!info) return false;
|
|
816
|
+
if (!force && pid !== undefined && info.pid !== pid) return false;
|
|
817
|
+
if (!force && pid === undefined && info.pid !== process.pid) return false;
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
await fs.unlink(guiPortFilePath());
|
|
821
|
+
return true;
|
|
822
|
+
} catch (error) {
|
|
823
|
+
if (error.code === 'ENOENT') return false;
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async function discoverRunningGui() {
|
|
829
|
+
const info = await readGuiPortFile();
|
|
830
|
+
if (!info || !isPidAlive(info.pid)) {
|
|
831
|
+
if (info) await clearGuiPortFile({ force: true });
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
status: 'running',
|
|
836
|
+
port: info.port,
|
|
837
|
+
pid: info.pid,
|
|
838
|
+
url: info.url || `http://localhost:${info.port}`,
|
|
839
|
+
cwd: info.cwd,
|
|
840
|
+
started_at: info.started_at
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
607
844
|
module.exports = {
|
|
608
845
|
ensureBacklogDir,
|
|
609
846
|
parseEpic,
|
|
@@ -619,7 +856,19 @@ module.exports = {
|
|
|
619
856
|
doCreate,
|
|
620
857
|
createKanbanError,
|
|
621
858
|
getProgress,
|
|
859
|
+
resolveTaskId,
|
|
860
|
+
hashCwdToPort,
|
|
861
|
+
normalizeGuiPort,
|
|
862
|
+
resolvePreferredGuiPort,
|
|
863
|
+
isPidAlive,
|
|
864
|
+
writeGuiPortFile,
|
|
865
|
+
readGuiPortFile,
|
|
866
|
+
clearGuiPortFile,
|
|
867
|
+
discoverRunningGui,
|
|
868
|
+
guiPortFilePath,
|
|
622
869
|
COLS,
|
|
623
870
|
STATUS_MAP,
|
|
624
|
-
VIEW_FIELDS
|
|
871
|
+
VIEW_FIELDS,
|
|
872
|
+
GUI_PORT_MIN,
|
|
873
|
+
GUI_PORT_MAX
|
|
625
874
|
};
|