kanbango 2.1.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 +32 -0
- package/LLM_AGENTS.md +141 -175
- package/README.md +31 -14
- package/bin/kanban.js +101 -26
- package/index.html +162 -85
- package/index.js +2 -0
- package/kan2.md +76 -0
- package/kanban.js +263 -34
- package/mcp-server.js +389 -185
- 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,11 +24,13 @@ const VIEW_FIELDS = {
|
|
|
20
24
|
'progress',
|
|
21
25
|
'description',
|
|
22
26
|
'specs',
|
|
27
|
+
'in_scope',
|
|
28
|
+
'out_of_scope',
|
|
23
29
|
'acceptance_criteria',
|
|
24
30
|
'test_cases'
|
|
25
31
|
],
|
|
26
32
|
execution: [
|
|
27
|
-
'
|
|
33
|
+
'task_number',
|
|
28
34
|
'title',
|
|
29
35
|
'column',
|
|
30
36
|
'epic_group',
|
|
@@ -32,12 +38,14 @@ const VIEW_FIELDS = {
|
|
|
32
38
|
'progress',
|
|
33
39
|
'description',
|
|
34
40
|
'specs',
|
|
41
|
+
'in_scope',
|
|
42
|
+
'out_of_scope',
|
|
35
43
|
'acceptance_criteria',
|
|
36
44
|
'test_cases',
|
|
37
45
|
'subtasks'
|
|
38
46
|
],
|
|
39
47
|
full: [
|
|
40
|
-
'
|
|
48
|
+
'task_number',
|
|
41
49
|
'title',
|
|
42
50
|
'column',
|
|
43
51
|
'epic_group',
|
|
@@ -45,6 +53,8 @@ const VIEW_FIELDS = {
|
|
|
45
53
|
'progress',
|
|
46
54
|
'description',
|
|
47
55
|
'specs',
|
|
56
|
+
'in_scope',
|
|
57
|
+
'out_of_scope',
|
|
48
58
|
'acceptance_criteria',
|
|
49
59
|
'test_cases',
|
|
50
60
|
'subtasks',
|
|
@@ -74,6 +84,11 @@ function normalizeString(value, fallback = '') {
|
|
|
74
84
|
return typeof value === 'string' ? value.trim() : fallback;
|
|
75
85
|
}
|
|
76
86
|
|
|
87
|
+
function extractTaskNumber(taskId) {
|
|
88
|
+
const match = normalizeString(taskId).match(/^(?:[A-Z]+-)?(\d+)/);
|
|
89
|
+
return match ? parseInt(match[1], 10) : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
function normalizeStringArray(value) {
|
|
78
93
|
if (!Array.isArray(value)) return [];
|
|
79
94
|
return value
|
|
@@ -91,34 +106,48 @@ function normalizeSubtasks(value) {
|
|
|
91
106
|
})).filter((subtask) => subtask.text);
|
|
92
107
|
}
|
|
93
108
|
|
|
94
|
-
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;
|
|
95
123
|
return {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
id: subtask.id,
|
|
99
|
-
text: subtask.text,
|
|
100
|
-
done: subtask.done,
|
|
101
|
-
description: subtask.description
|
|
102
|
-
}))
|
|
124
|
+
runner: value.runner && typeof value.runner === 'object' ? value.runner : null,
|
|
125
|
+
status: ['active', 'done'].includes(value.status) ? value.status : 'active'
|
|
103
126
|
};
|
|
104
127
|
}
|
|
105
128
|
|
|
106
129
|
function normalizeTask(task) {
|
|
130
|
+
const id = normalizeString(task.id);
|
|
107
131
|
const normalized = {
|
|
108
|
-
id
|
|
109
|
-
title: stripTitlePrefix(task.title ||
|
|
132
|
+
id,
|
|
133
|
+
title: stripTitlePrefix(task.title || id),
|
|
110
134
|
column: COLS.includes(task.column) ? task.column : 'planned',
|
|
111
135
|
epic_group: normalizeString(task.epic_group, '—') || '—',
|
|
112
136
|
created: normalizeString(task.created) || todayIso(),
|
|
113
137
|
description: normalizeString(task.description),
|
|
114
138
|
specs: normalizeString(task.specs),
|
|
139
|
+
in_scope: normalizeStringArray(task.in_scope),
|
|
140
|
+
out_of_scope: normalizeStringArray(task.out_of_scope),
|
|
115
141
|
acceptance_criteria: normalizeStringArray(task.acceptance_criteria),
|
|
116
142
|
test_cases: normalizeStringArray(task.test_cases),
|
|
117
|
-
subtasks: normalizeSubtasks(task.subtasks
|
|
118
|
-
notes: normalizeString(task.notes)
|
|
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)
|
|
119
148
|
};
|
|
120
149
|
|
|
121
|
-
return
|
|
150
|
+
return normalized;
|
|
122
151
|
}
|
|
123
152
|
|
|
124
153
|
function serializeTask(task) {
|
|
@@ -131,10 +160,15 @@ function serializeTask(task) {
|
|
|
131
160
|
created: normalized.created,
|
|
132
161
|
description: normalized.description,
|
|
133
162
|
specs: normalized.specs,
|
|
163
|
+
in_scope: normalized.in_scope,
|
|
164
|
+
out_of_scope: normalized.out_of_scope,
|
|
134
165
|
acceptance_criteria: normalized.acceptance_criteria,
|
|
135
166
|
test_cases: normalized.test_cases,
|
|
136
167
|
subtasks: normalized.subtasks,
|
|
137
|
-
notes: normalized.notes
|
|
168
|
+
notes: normalized.notes,
|
|
169
|
+
plan: normalized.plan,
|
|
170
|
+
evidence: normalized.evidence,
|
|
171
|
+
task_number: normalized.task_number
|
|
138
172
|
};
|
|
139
173
|
}
|
|
140
174
|
|
|
@@ -152,10 +186,6 @@ function pickFields(task, fieldNames) {
|
|
|
152
186
|
picked.progress = getProgress(task);
|
|
153
187
|
continue;
|
|
154
188
|
}
|
|
155
|
-
if (field === 'tasks') {
|
|
156
|
-
picked.tasks = task.tasks;
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
189
|
if (field in task) {
|
|
160
190
|
picked[field] = task[field];
|
|
161
191
|
}
|
|
@@ -231,6 +261,8 @@ async function parseMarkdownTask(filePath, column) {
|
|
|
231
261
|
created: createdMatch ? createdMatch[1].trim() : todayIso(),
|
|
232
262
|
description: extractSection(text, ['Opis', 'Description']),
|
|
233
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'])),
|
|
234
266
|
acceptance_criteria: parseListSection(extractSection(text, ['Acceptance Criteria', 'Kryteria Akceptacji'])),
|
|
235
267
|
test_cases: parseListSection(extractSection(text, ['Test Cases', 'Przypadki Testowe'])),
|
|
236
268
|
subtasks,
|
|
@@ -323,7 +355,8 @@ async function findFile(epicId) {
|
|
|
323
355
|
}
|
|
324
356
|
|
|
325
357
|
async function getTask(taskId) {
|
|
326
|
-
const
|
|
358
|
+
const resolvedId = await resolveTaskId(taskId);
|
|
359
|
+
const filePath = await findFile(resolvedId);
|
|
327
360
|
if (!filePath) {
|
|
328
361
|
throw createKanbanError(
|
|
329
362
|
'TASK_NOT_FOUND',
|
|
@@ -339,6 +372,34 @@ async function getTask(taskId) {
|
|
|
339
372
|
return parseEpic(filePath, column);
|
|
340
373
|
}
|
|
341
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
|
+
|
|
342
403
|
async function writeTask(task, previousFilePath = null) {
|
|
343
404
|
const normalized = normalizeTask(task);
|
|
344
405
|
await ensureBacklogDir();
|
|
@@ -403,7 +464,7 @@ async function migrateAll(options = {}) {
|
|
|
403
464
|
return { migrated, errors };
|
|
404
465
|
}
|
|
405
466
|
|
|
406
|
-
async function
|
|
467
|
+
async function nextTaskNumber() {
|
|
407
468
|
const ids = [];
|
|
408
469
|
|
|
409
470
|
for (const col of COLS) {
|
|
@@ -411,7 +472,7 @@ async function nextPiNumber() {
|
|
|
411
472
|
try {
|
|
412
473
|
const files = await fs.readdir(colDir);
|
|
413
474
|
for (const file of files) {
|
|
414
|
-
const match = file.match(/^
|
|
475
|
+
const match = file.match(/^(?:[A-Z]+-)?(\d+)/);
|
|
415
476
|
if (match) ids.push(parseInt(match[1], 10));
|
|
416
477
|
}
|
|
417
478
|
} catch (error) {
|
|
@@ -462,24 +523,28 @@ async function doCreate(title, column = 'planned', epicGroup = '—', extra = {}
|
|
|
462
523
|
|
|
463
524
|
validateColumn(column, 'col');
|
|
464
525
|
|
|
465
|
-
const nextId = await
|
|
526
|
+
const nextId = await nextTaskNumber();
|
|
466
527
|
const slug = title
|
|
467
528
|
.toLowerCase()
|
|
468
529
|
.replace(/[^a-z0-9]+/g, '-')
|
|
469
530
|
.replace(/^-+|-+$/g, '')
|
|
470
531
|
.substring(0, 25);
|
|
471
532
|
const task = normalizeTask({
|
|
472
|
-
id:
|
|
533
|
+
id: String(nextId).padStart(3, '0'),
|
|
473
534
|
title,
|
|
474
535
|
column,
|
|
475
536
|
epic_group: epicGroup || '—',
|
|
476
537
|
created: todayIso(),
|
|
477
538
|
description: extra.description,
|
|
478
539
|
specs: extra.specs,
|
|
540
|
+
in_scope: extra.in_scope,
|
|
541
|
+
out_of_scope: extra.out_of_scope,
|
|
479
542
|
acceptance_criteria: extra.acceptance_criteria,
|
|
480
543
|
test_cases: extra.test_cases,
|
|
481
544
|
subtasks: extra.subtasks,
|
|
482
|
-
notes: extra.notes
|
|
545
|
+
notes: extra.notes,
|
|
546
|
+
plan: extra.plan,
|
|
547
|
+
evidence: extra.evidence
|
|
483
548
|
});
|
|
484
549
|
|
|
485
550
|
return writeTask(task);
|
|
@@ -488,7 +553,8 @@ async function doCreate(title, column = 'planned', epicGroup = '—', extra = {}
|
|
|
488
553
|
async function updateTask(taskId, patch) {
|
|
489
554
|
validatePatch(patch);
|
|
490
555
|
|
|
491
|
-
const
|
|
556
|
+
const resolvedId = await resolveTaskId(taskId);
|
|
557
|
+
const previousFilePath = await findFile(resolvedId);
|
|
492
558
|
if (!previousFilePath) {
|
|
493
559
|
throw createKanbanError(
|
|
494
560
|
'TASK_NOT_FOUND',
|
|
@@ -524,6 +590,32 @@ async function updateTask(taskId, patch) {
|
|
|
524
590
|
if (patch.epic_group !== undefined) next.epic_group = normalizeString(patch.epic_group, '—') || '—';
|
|
525
591
|
if (patch.description !== undefined) next.description = normalizeString(patch.description);
|
|
526
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
|
+
}
|
|
527
619
|
if (patch.acceptance_criteria !== undefined) {
|
|
528
620
|
if (!Array.isArray(patch.acceptance_criteria)) {
|
|
529
621
|
throw createKanbanError(
|
|
@@ -550,9 +642,8 @@ async function updateTask(taskId, patch) {
|
|
|
550
642
|
}
|
|
551
643
|
next.test_cases = patch.test_cases;
|
|
552
644
|
}
|
|
553
|
-
if (patch.subtasks !== undefined
|
|
554
|
-
|
|
555
|
-
if (!Array.isArray(subtasks)) {
|
|
645
|
+
if (patch.subtasks !== undefined) {
|
|
646
|
+
if (!Array.isArray(patch.subtasks)) {
|
|
556
647
|
throw createKanbanError(
|
|
557
648
|
'VALIDATION_ERROR',
|
|
558
649
|
'subtasks must be an array',
|
|
@@ -562,9 +653,23 @@ async function updateTask(taskId, patch) {
|
|
|
562
653
|
400
|
|
563
654
|
);
|
|
564
655
|
}
|
|
565
|
-
next.subtasks = subtasks;
|
|
656
|
+
next.subtasks = patch.subtasks;
|
|
566
657
|
}
|
|
567
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
|
+
}
|
|
568
673
|
|
|
569
674
|
return writeTask(next, previousFilePath);
|
|
570
675
|
}
|
|
@@ -624,6 +729,118 @@ async function doUpdate(epicId, newTitle, newTasks) {
|
|
|
624
729
|
}
|
|
625
730
|
}
|
|
626
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
|
+
|
|
627
844
|
module.exports = {
|
|
628
845
|
ensureBacklogDir,
|
|
629
846
|
parseEpic,
|
|
@@ -639,7 +856,19 @@ module.exports = {
|
|
|
639
856
|
doCreate,
|
|
640
857
|
createKanbanError,
|
|
641
858
|
getProgress,
|
|
859
|
+
resolveTaskId,
|
|
860
|
+
hashCwdToPort,
|
|
861
|
+
normalizeGuiPort,
|
|
862
|
+
resolvePreferredGuiPort,
|
|
863
|
+
isPidAlive,
|
|
864
|
+
writeGuiPortFile,
|
|
865
|
+
readGuiPortFile,
|
|
866
|
+
clearGuiPortFile,
|
|
867
|
+
discoverRunningGui,
|
|
868
|
+
guiPortFilePath,
|
|
642
869
|
COLS,
|
|
643
870
|
STATUS_MAP,
|
|
644
|
-
VIEW_FIELDS
|
|
871
|
+
VIEW_FIELDS,
|
|
872
|
+
GUI_PORT_MIN,
|
|
873
|
+
GUI_PORT_MAX
|
|
645
874
|
};
|