kanbango 3.1.0 → 3.2.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/.ai/lessons.jsonl +1 -0
- package/.ai/retro/last-run.json +1 -1
- package/AGENTS.md +4 -2
- package/CHANGELOG.md +18 -0
- package/LLM_AGENTS.md +1 -1
- package/README.md +9 -2
- package/agent-playbook.js +13 -5
- package/bin/kanban.js +131 -10
- package/gui-registry.js +14 -1
- package/index.html +645 -39
- package/kanban.js +157 -5
- package/mcp-server.js +125 -21
- package/package.json +1 -1
- package/tests/run.js +1 -0
package/.ai/lessons.jsonl
CHANGED
|
@@ -2,3 +2,4 @@
|
|
|
2
2
|
{"id":"260729-1ur","ts":"2026-07-29","scope":"proj","tags":["javascript","objects"],"rule":"DON'T Use empty object fallbacks in spreads like `...(obj || {})` since spreading falsy is safe","when":"object spread","sev":2,"hits":1}
|
|
3
3
|
{"id":"260729-5eo","ts":"2026-07-29","scope":"proj","tags":["processes","mcp","ownership"],"rule":"DO Track process ownership before stopping or cleaning up discovered services","when":"gui-process management","sev":2,"hits":1}
|
|
4
4
|
{"id":"260729-1ch","ts":"2026-07-29","scope":"proj","tags":["gui","process","mcp"],"rule":"DO GUI/MCP stop must only SIGTERM processes spawned by the current process; treat port-file PIDs as external_running","when":"gui-process management","sev":2,"hits":1}
|
|
5
|
+
{"id":"260731-o6g","ts":"2026-07-31","scope":"proj","tags":["kanban","epics","mcp"],"rule":"DO Default list/list_epics hide done+archived; hard-delete cascades epic children; archive is reversible flag","when":"kanban cleanup context","sev":2,"hits":1}
|
package/.ai/retro/last-run.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"ts":"2026-07-
|
|
1
|
+
{"ts":"2026-07-31T13:58:00.629Z","sessionID":"ses_04792cf77ffec14LhSNoSJReg9","added":["260731-o6g"],"bumped":[]}
|
package/AGENTS.md
CHANGED
|
@@ -8,11 +8,13 @@ Source of truth: `agent-playbook.js` (also MCP tool descriptions + `kanban_read`
|
|
|
8
8
|
|
|
9
9
|
- hierarchy: epic (context) → task (work+plan) → subtasks (steps)
|
|
10
10
|
- list: `col` filter, `view=summary`; keep `task_id`s; no full-board re-list after every write
|
|
11
|
-
- list_epics
|
|
11
|
+
- list_epics default = live only (hides done + archived); `include_archived` / `status` to widen
|
|
12
|
+
- list tasks default hides tasks under done/archived epics; show/show_epic by id always works
|
|
12
13
|
- show: `view=execution` while coding; `full` only if needed
|
|
13
14
|
- epic_create with description/goals; create tasks with `epic=E001` (prefer id)
|
|
14
15
|
- create once with `description`, `specs`, `in_scope`, `out_of_scope`, `acceptance_criteria`
|
|
15
|
-
- move/update: `return=none`; subtasks = full array replace (no toggle)
|
|
16
|
+
- move/update/delete: `return=none`; subtasks = full array replace (no toggle)
|
|
17
|
+
- cleanup: `delete` (task), `epic_delete` (cascade), `epic_archive` / `epic_unarchive`
|
|
16
18
|
- non-trivial work: `plan_create` → `plan_advance` → `plan_evidence` (real tests, truncated logs) → `plan_done`
|
|
17
19
|
- gui: `status` before `start`; `stop` only owned; `external_running` = do not kill
|
|
18
20
|
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to kanbango will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [3.2.0] - 2026-07-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- GUI project label: browser tab title, header, port file, and `kanban_gui` start/status include `project` (basename of cwd; override with `KANBANGO_PROJECT_NAME`)
|
|
14
|
+
- Epic field `archived` + status `archived` (overrides derived child rollup)
|
|
15
|
+
- Hard delete: MCP `delete` / `epic_delete` (cascade child tasks), CLI `kanban rm` / `kanban epic rm`, HTTP `DELETE`
|
|
16
|
+
- Archive: MCP `epic_archive` / `epic_unarchive`, CLI `kanban epic archive|unarchive`, HTTP `POST .../archive|unarchive`
|
|
17
|
+
- `list_epics` default live-only (empty|planned|active); `include_archived`, `include_done`, `status` filters
|
|
18
|
+
- Task `list` hides tasks under done/archived epics unless `include_archived`/`include_done` (explicit `epic=` filter still shows them)
|
|
19
|
+
- GUI: Archive/Unarchive/Delete on epic swimlane, Delete on task card, Show archived toggle
|
|
20
|
+
- Tests: `tests/delete-archive.test.js`
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Agent playbook documents cleanup + live-only epic discovery
|
|
24
|
+
- `show` / `show_epic` by id still return archived items (explicit lookup)
|
|
25
|
+
|
|
8
26
|
## [3.1.0] - 2026-07-30
|
|
9
27
|
|
|
10
28
|
### Added
|
package/LLM_AGENTS.md
CHANGED
|
@@ -249,7 +249,7 @@ Control the web GUI server: start, stop, or check status.
|
|
|
249
249
|
2. Else `KANBANGO_GUI_PORT` env
|
|
250
250
|
3. Else stable hash of project cwd in range `5510–5999`
|
|
251
251
|
|
|
252
|
-
If the preferred port is busy, the server picks the next free port. Always trust the returned `url` / `port` (also written to `backlog/.kanbango-gui.json`).
|
|
252
|
+
If the preferred port is busy, the server picks the next free port. Always trust the returned `url` / `port` (also written to `backlog/.kanbango-gui.json`). Responses also include `project` (basename of project cwd, or `KANBANGO_PROJECT_NAME`) so agents and humans can tell boards apart; the GUI tab title and header show the same label.
|
|
253
253
|
|
|
254
254
|
**Auto-start with MCP:** set `KANBANGO_AUTO_GUI=1` in the MCP server env. GUI starts when MCP starts; use `status` to read the URL.
|
|
255
255
|
|
package/README.md
CHANGED
|
@@ -35,10 +35,17 @@ kanban list --json
|
|
|
35
35
|
kanban epic add "Phase 1" --description "Why this initiative" --goals "Ship X"
|
|
36
36
|
kanban add "My task" --col planned --epic E001
|
|
37
37
|
|
|
38
|
-
# List epics / show epic with child rollup
|
|
38
|
+
# List epics / show epic with child rollup (default: live only)
|
|
39
39
|
kanban epic list --json
|
|
40
|
+
kanban epic list --all --json
|
|
40
41
|
kanban epic show E001
|
|
41
42
|
|
|
43
|
+
# Archive or delete (epic_delete cascades child tasks)
|
|
44
|
+
kanban epic archive E001
|
|
45
|
+
kanban epic unarchive E001
|
|
46
|
+
kanban epic rm E001
|
|
47
|
+
kanban rm 001
|
|
48
|
+
|
|
42
49
|
# Show details
|
|
43
50
|
kanban show 001
|
|
44
51
|
|
|
@@ -130,7 +137,7 @@ Auto-start the web GUI with MCP (opt-in):
|
|
|
130
137
|
}
|
|
131
138
|
```
|
|
132
139
|
|
|
133
|
-
Optional: pin the port with `KANBANGO_GUI_PORT` (e.g. `"5821"`). Without it, each project gets a stable port in `5510–5999` derived from the project path. The real URL is always available via `kanban_gui` → `status` (and written to `backlog/.kanbango-gui.json` while the GUI runs).
|
|
140
|
+
Optional: pin the port with `KANBANGO_GUI_PORT` (e.g. `"5821"`). Without it, each project gets a stable port in `5510–5999` derived from the project path. The real URL is always available via `kanban_gui` → `status` (and written to `backlog/.kanbango-gui.json` while the GUI runs). The browser tab title, GUI header, and `kanban_gui` responses include `project` (folder name of the project cwd; override with `KANBANGO_PROJECT_NAME`).
|
|
134
141
|
|
|
135
142
|
Or generate the config files automatically:
|
|
136
143
|
|
package/agent-playbook.js
CHANGED
|
@@ -8,11 +8,13 @@ const DROP_IN_RULE = [
|
|
|
8
8
|
'Kanbango MCP — token rules:',
|
|
9
9
|
'- hierarchy: epic (context) → task (work+plan) → subtasks (steps)',
|
|
10
10
|
'- list: col filter, view=summary; keep task_ids; no full-board re-list after writes',
|
|
11
|
-
'- list_epics
|
|
11
|
+
'- list_epics default = live only (hides done + archived); include_archived / status to widen',
|
|
12
|
+
'- list tasks default hides tasks under done/archived epics; show/show_epic by id always works',
|
|
12
13
|
'- show: view=execution while coding; full only if needed',
|
|
13
14
|
'- epic_create with description/goals; create tasks with epic=E001 (prefer id)',
|
|
14
15
|
'- create once with description,specs,in_scope,out_of_scope,acceptance_criteria',
|
|
15
|
-
'- move/update: return=none; subtasks=full array replace',
|
|
16
|
+
'- move/update/delete: return=none; subtasks=full array replace',
|
|
17
|
+
'- cleanup: delete (task), epic_delete (cascade tasks), epic_archive / epic_unarchive',
|
|
16
18
|
'- non-trivial: plan_create → plan_advance → plan_evidence (real tests, truncated logs) → plan_done',
|
|
17
19
|
'- gui: status before start; stop only owned; external_running = do not kill'
|
|
18
20
|
].join('\n');
|
|
@@ -22,7 +24,9 @@ const TOOL_DESCRIPTIONS = {
|
|
|
22
24
|
'Read board. TOKEN RULES: list defaults to view=summary (id/title/col/progress only).',
|
|
23
25
|
'Hierarchy: epic (container/context) → task (work) → subtasks (steps).',
|
|
24
26
|
'Always pass col when possible. Prefer show+view=execution over full.',
|
|
25
|
-
'list_epics
|
|
27
|
+
'list_epics default live only (empty|planned|active; hides done+archived).',
|
|
28
|
+
'Pass include_archived=true or status=done|archived to see closed initiatives.',
|
|
29
|
+
'list hides tasks under done/archived epics unless include_archived/include_done; show/show_epic by id always works.',
|
|
26
30
|
'Do not re-list the whole board after every write — keep task_id from create/move.',
|
|
27
31
|
'Task IDs numeric ("014"); epic IDs "E001". views: summary|planning|execution|full; fields[] overrides view.',
|
|
28
32
|
'operation=help returns this playbook as short text (no board I/O).'
|
|
@@ -31,9 +35,11 @@ const TOOL_DESCRIPTIONS = {
|
|
|
31
35
|
kanban_manage: [
|
|
32
36
|
'Write board / plan. TOKEN RULES: one create with all planning fields beats many updates;',
|
|
33
37
|
'after write use return=none (or summary). Do not dump full task unless needed.',
|
|
34
|
-
'Actions: create|move|update (daily); epic_create|epic_update
|
|
38
|
+
'Actions: create|move|update|delete (daily); epic_create|epic_update|epic_archive|epic_unarchive|epic_delete;',
|
|
35
39
|
'plan_create→plan_advance→plan_evidence→plan_done (non-trivial only).',
|
|
36
40
|
'epic_create: title + description/goals/in_scope/out_of_scope. Link tasks via epic=E001.',
|
|
41
|
+
'delete: task_id (hard remove). epic_delete: epic_id, always cascades child tasks.',
|
|
42
|
+
'epic_archive/epic_unarchive: epic_id — hide/restore initiative without deleting history.',
|
|
37
43
|
'create/plan_create: title required; also send description,specs,in_scope,out_of_scope,acceptance_criteria',
|
|
38
44
|
'(missing → warnings, not failure). move: task_id+column. update: task_id + fields or subtasks[] full list',
|
|
39
45
|
'(no toggle). plan_evidence needs real test run: diff,test_command,stdout,stderr,exit_code — truncate logs.',
|
|
@@ -55,7 +61,9 @@ const MUST_CONTAIN = [
|
|
|
55
61
|
'plan_create',
|
|
56
62
|
'external_running',
|
|
57
63
|
'subtasks',
|
|
58
|
-
'epic_create'
|
|
64
|
+
'epic_create',
|
|
65
|
+
'epic_archive',
|
|
66
|
+
'epic_delete'
|
|
59
67
|
];
|
|
60
68
|
|
|
61
69
|
function playbookHelpPayload() {
|
package/bin/kanban.js
CHANGED
|
@@ -145,8 +145,9 @@ async function cliMcpInit(options) {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
async function cliList(colFilter, epicFilter, asJson) {
|
|
148
|
+
async function cliList(colFilter, epicFilter, asJson, listOptions = {}) {
|
|
149
149
|
await kanban.migrateEpicGroups();
|
|
150
|
+
const epics = await kanban.listEpicEntities();
|
|
150
151
|
let tasks = await kanban.allTasks();
|
|
151
152
|
|
|
152
153
|
if (colFilter) {
|
|
@@ -154,6 +155,8 @@ async function cliList(colFilter, epicFilter, asJson) {
|
|
|
154
155
|
}
|
|
155
156
|
if (epicFilter) {
|
|
156
157
|
tasks = tasks.filter((task) => kanban.taskMatchesEpicFilter(task, epicFilter));
|
|
158
|
+
} else {
|
|
159
|
+
tasks = kanban.filterTasksForList(tasks, epics, listOptions);
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
if (asJson) {
|
|
@@ -207,11 +210,14 @@ async function cliShow(taskId) {
|
|
|
207
210
|
}
|
|
208
211
|
}
|
|
209
212
|
|
|
210
|
-
async function cliEpicList(asJson) {
|
|
213
|
+
async function cliEpicList(asJson, listOptions = {}) {
|
|
211
214
|
await kanban.migrateEpicGroups();
|
|
212
215
|
const tasks = await kanban.allTasks();
|
|
213
216
|
const epics = await kanban.listEpicEntities();
|
|
214
|
-
const shaped =
|
|
217
|
+
const shaped = kanban.filterShapedEpics(
|
|
218
|
+
epics.map((epic) => kanban.shapeEpic(epic, tasks, { view: 'summary' })),
|
|
219
|
+
listOptions
|
|
220
|
+
);
|
|
215
221
|
|
|
216
222
|
if (asJson) {
|
|
217
223
|
console.log(JSON.stringify(shaped, null, 2));
|
|
@@ -225,8 +231,9 @@ async function cliEpicList(asJson) {
|
|
|
225
231
|
|
|
226
232
|
for (const epic of shaped) {
|
|
227
233
|
const p = epic.progress || {};
|
|
234
|
+
const flag = epic.archived ? ' [archived]' : '';
|
|
228
235
|
console.log(
|
|
229
|
-
` ${epic.id.padEnd(6)} ${epic.status.padEnd(8)} ${String(epic.title).substring(0, 40).padEnd(40)} ${p.tasks_done || 0}/${p.tasks_total || 0} tasks`
|
|
236
|
+
` ${epic.id.padEnd(6)} ${epic.status.padEnd(8)} ${String(epic.title).substring(0, 40).padEnd(40)} ${p.tasks_done || 0}/${p.tasks_total || 0} tasks${flag}`
|
|
230
237
|
);
|
|
231
238
|
}
|
|
232
239
|
}
|
|
@@ -265,6 +272,38 @@ async function cliEpicUpdate(epicId, patch) {
|
|
|
265
272
|
}
|
|
266
273
|
}
|
|
267
274
|
|
|
275
|
+
async function cliEpicArchive(epicId, archived) {
|
|
276
|
+
try {
|
|
277
|
+
const epic = archived
|
|
278
|
+
? await kanban.archiveEpic(epicId)
|
|
279
|
+
: await kanban.unarchiveEpic(epicId);
|
|
280
|
+
console.log(`✓ Epic ${epic.id} ${archived ? 'archived' : 'unarchived'}`);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error(`✗ ${error.message}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function cliEpicDelete(epicId) {
|
|
288
|
+
try {
|
|
289
|
+
const result = await kanban.deleteEpic(epicId);
|
|
290
|
+
console.log(`✓ Deleted epic ${result.epic_id} (+${result.deleted_task_count} tasks)`);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error(`✗ ${error.message}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function cliDelete(taskId) {
|
|
298
|
+
try {
|
|
299
|
+
const result = await kanban.deleteTask(taskId);
|
|
300
|
+
console.log(`✓ Deleted task ${shortId(result.task_id)}: ${result.title}`);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error(`✗ ${error.message}`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
268
307
|
async function cliMove(taskId, column) {
|
|
269
308
|
const success = await kanban.doMove(taskId, column);
|
|
270
309
|
if (success) {
|
|
@@ -286,8 +325,21 @@ async function cliAdd(title, column, epicGroup) {
|
|
|
286
325
|
}
|
|
287
326
|
}
|
|
288
327
|
|
|
328
|
+
function injectProjectIntoHtml(html, project) {
|
|
329
|
+
const safe = String(project)
|
|
330
|
+
.replace(/&/g, '&')
|
|
331
|
+
.replace(/</g, '<')
|
|
332
|
+
.replace(/>/g, '>')
|
|
333
|
+
.replace(/"/g, '"');
|
|
334
|
+
return html
|
|
335
|
+
.replace(/<title>[^<]*<\/title>/, `<title>${safe} · Kanban</title>`)
|
|
336
|
+
.replace(/<h1>Donna Kanban<\/h1>/, `<h1>${safe}</h1>`);
|
|
337
|
+
}
|
|
338
|
+
|
|
289
339
|
async function serveWeb(port) {
|
|
290
|
-
const
|
|
340
|
+
const htmlTemplate = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf-8');
|
|
341
|
+
const project = guiRegistry.projectLabel();
|
|
342
|
+
const html = injectProjectIntoHtml(htmlTemplate, project);
|
|
291
343
|
|
|
292
344
|
const server = http.createServer(async (req, res) => {
|
|
293
345
|
const url = new URL(req.url, `http://localhost:${port}`);
|
|
@@ -302,7 +354,13 @@ async function serveWeb(port) {
|
|
|
302
354
|
|
|
303
355
|
if (requestPath === '/api/board') {
|
|
304
356
|
await kanban.migrateEpicGroups();
|
|
305
|
-
const
|
|
357
|
+
const includeArchived = url.searchParams.get('include_archived') === 'true';
|
|
358
|
+
const epics = await kanban.listEpicEntities();
|
|
359
|
+
// GUI: show done-epic tasks; only hide archived unless toggled
|
|
360
|
+
const tasks = kanban.filterTasksForList(await kanban.allTasks(), epics, {
|
|
361
|
+
live_only: false,
|
|
362
|
+
include_archived: includeArchived
|
|
363
|
+
});
|
|
306
364
|
sendJson(res, 200, tasks);
|
|
307
365
|
return;
|
|
308
366
|
}
|
|
@@ -311,7 +369,16 @@ async function serveWeb(port) {
|
|
|
311
369
|
await kanban.migrateEpicGroups();
|
|
312
370
|
const tasks = await kanban.allTasks();
|
|
313
371
|
const epics = await kanban.listEpicEntities();
|
|
314
|
-
|
|
372
|
+
const shaped = epics.map((epic) => kanban.shapeEpic(epic, tasks, { view: 'full' }));
|
|
373
|
+
const includeArchived = url.searchParams.get('include_archived') === 'true';
|
|
374
|
+
const includeDone = url.searchParams.get('include_done') === 'true';
|
|
375
|
+
const status = url.searchParams.get('status') || undefined;
|
|
376
|
+
// GUI default: all non-archived (live + done). Agents use MCP live-only.
|
|
377
|
+
const liveOnly = url.searchParams.get('live_only') === 'true';
|
|
378
|
+
const filtered = kanban.filterShapedEpics(shaped, liveOnly
|
|
379
|
+
? { include_archived: includeArchived, include_done: includeDone, status }
|
|
380
|
+
: { live_only: false, include_archived: includeArchived, status });
|
|
381
|
+
sendJson(res, 200, filtered);
|
|
315
382
|
return;
|
|
316
383
|
}
|
|
317
384
|
|
|
@@ -370,6 +437,33 @@ async function serveWeb(port) {
|
|
|
370
437
|
return;
|
|
371
438
|
}
|
|
372
439
|
|
|
440
|
+
if (req.method === 'DELETE') {
|
|
441
|
+
const epicDeleteMatch = requestPath.match(/^\/api\/epic-entities\/([^/]+)$/);
|
|
442
|
+
if (epicDeleteMatch) {
|
|
443
|
+
const result = await kanban.deleteEpic(epicDeleteMatch[1]);
|
|
444
|
+
sendJson(res, 200, result);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const taskDeleteMatch = requestPath.match(/^\/api\/(?:epics|tasks)\/([^/]+)$/);
|
|
449
|
+
if (taskDeleteMatch) {
|
|
450
|
+
const result = await kanban.deleteTask(taskDeleteMatch[1]);
|
|
451
|
+
sendJson(res, 200, result);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (req.method === 'POST') {
|
|
457
|
+
const epicArchiveMatch = requestPath.match(/^\/api\/epic-entities\/([^/]+)\/(archive|unarchive)$/);
|
|
458
|
+
if (epicArchiveMatch) {
|
|
459
|
+
const epic = epicArchiveMatch[2] === 'archive'
|
|
460
|
+
? await kanban.archiveEpic(epicArchiveMatch[1])
|
|
461
|
+
: await kanban.unarchiveEpic(epicArchiveMatch[1]);
|
|
462
|
+
sendJson(res, 200, kanban.shapeEpic(epic, await kanban.allTasks(), { view: 'full' }));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
373
467
|
if (req.method === 'PATCH') {
|
|
374
468
|
const epicUpdateMatch = requestPath.match(/^\/api\/epic-entities\/([^/]+)$/);
|
|
375
469
|
if (epicUpdateMatch) {
|
|
@@ -504,6 +598,7 @@ async function serveWeb(port) {
|
|
|
504
598
|
});
|
|
505
599
|
|
|
506
600
|
console.log(`\x1b[1;32m→ Kanban GUI: ${portInfo.url}\x1b[0m`);
|
|
601
|
+
console.log(` Project: ${portInfo.project || project}`);
|
|
507
602
|
console.log(` Backlog: ${BACKLOG}`);
|
|
508
603
|
console.log(' Ctrl+C żeby zamknąć');
|
|
509
604
|
}
|
|
@@ -617,6 +712,7 @@ async function main() {
|
|
|
617
712
|
let colFilter = null;
|
|
618
713
|
let epicFilter = null;
|
|
619
714
|
let asJson = false;
|
|
715
|
+
let includeArchived = false;
|
|
620
716
|
|
|
621
717
|
for (let i = 1; i < args.length; i++) {
|
|
622
718
|
if (args[i] === '--col' && args[i + 1]) {
|
|
@@ -625,14 +721,31 @@ async function main() {
|
|
|
625
721
|
epicFilter = args[++i];
|
|
626
722
|
} else if (args[i] === '--json') {
|
|
627
723
|
asJson = true;
|
|
724
|
+
} else if (args[i] === '--include-archived') {
|
|
725
|
+
includeArchived = true;
|
|
628
726
|
}
|
|
629
727
|
}
|
|
630
728
|
|
|
631
|
-
await cliList(colFilter, epicFilter, asJson);
|
|
729
|
+
await cliList(colFilter, epicFilter, asJson, { include_archived: includeArchived });
|
|
632
730
|
} else if (cmd === 'epic') {
|
|
633
731
|
const sub = args[1];
|
|
634
732
|
if (sub === 'list') {
|
|
635
|
-
|
|
733
|
+
let includeArchived = false;
|
|
734
|
+
let includeDone = false;
|
|
735
|
+
let status;
|
|
736
|
+
for (let i = 2; i < args.length; i++) {
|
|
737
|
+
if (args[i] === '--include-archived') includeArchived = true;
|
|
738
|
+
else if (args[i] === '--include-done') includeDone = true;
|
|
739
|
+
else if (args[i] === '--all') {
|
|
740
|
+
includeArchived = true;
|
|
741
|
+
includeDone = true;
|
|
742
|
+
} else if (args[i] === '--status' && args[i + 1]) status = args[++i];
|
|
743
|
+
}
|
|
744
|
+
await cliEpicList(args.includes('--json'), {
|
|
745
|
+
include_archived: includeArchived,
|
|
746
|
+
include_done: includeDone,
|
|
747
|
+
status
|
|
748
|
+
});
|
|
636
749
|
} else if (sub === 'show' && args[2]) {
|
|
637
750
|
await cliEpicShow(args[2]);
|
|
638
751
|
} else if (sub === 'add' && args[2]) {
|
|
@@ -645,12 +758,20 @@ async function main() {
|
|
|
645
758
|
await cliEpicAdd(args[2], { description, goals });
|
|
646
759
|
} else if (sub === 'update' && args[2] && args[3]) {
|
|
647
760
|
await cliEpicUpdate(args[2], parseJsonPayload(args[3]));
|
|
761
|
+
} else if (sub === 'archive' && args[2]) {
|
|
762
|
+
await cliEpicArchive(args[2], true);
|
|
763
|
+
} else if (sub === 'unarchive' && args[2]) {
|
|
764
|
+
await cliEpicArchive(args[2], false);
|
|
765
|
+
} else if ((sub === 'rm' || sub === 'delete') && args[2]) {
|
|
766
|
+
await cliEpicDelete(args[2]);
|
|
648
767
|
} else {
|
|
649
|
-
console.error('Usage: kanban epic list|show|add|update ...');
|
|
768
|
+
console.error('Usage: kanban epic list|show|add|update|archive|unarchive|rm ...');
|
|
650
769
|
process.exit(1);
|
|
651
770
|
}
|
|
652
771
|
} else if (cmd === 'show' && args[1]) {
|
|
653
772
|
await cliShow(args[1]);
|
|
773
|
+
} else if ((cmd === 'rm' || cmd === 'delete') && args[1]) {
|
|
774
|
+
await cliDelete(args[1]);
|
|
654
775
|
} else if (cmd === 'move' && args[1] && args[2]) {
|
|
655
776
|
await cliMove(args[1], args[2]);
|
|
656
777
|
} else if (cmd === 'add' && args[1]) {
|
package/gui-registry.js
CHANGED
|
@@ -21,6 +21,15 @@ function guiPortFilePath() {
|
|
|
21
21
|
return path.join(BACKLOG, GUI_PORT_FILE);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function projectLabel(cwd = process.cwd()) {
|
|
25
|
+
const fromEnv = process.env.KANBANGO_PROJECT_NAME;
|
|
26
|
+
if (fromEnv !== undefined && fromEnv !== null && String(fromEnv).trim()) {
|
|
27
|
+
return String(fromEnv).trim();
|
|
28
|
+
}
|
|
29
|
+
const base = path.basename(String(cwd || '').replace(/[/\\]+$/, ''));
|
|
30
|
+
return base || 'kanban';
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
function hashCwdToPort(cwd = process.cwd()) {
|
|
25
34
|
let hash = 0;
|
|
26
35
|
const input = String(cwd);
|
|
@@ -79,11 +88,13 @@ async function writeGuiPortFile({ port, pid = process.pid } = {}) {
|
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
await ensureBacklogDir();
|
|
91
|
+
const cwd = process.cwd();
|
|
82
92
|
const data = {
|
|
83
93
|
port: normalizedPort,
|
|
84
94
|
pid,
|
|
85
95
|
url: `http://localhost:${normalizedPort}`,
|
|
86
|
-
cwd
|
|
96
|
+
cwd,
|
|
97
|
+
project: projectLabel(cwd),
|
|
87
98
|
started_at: new Date().toISOString()
|
|
88
99
|
};
|
|
89
100
|
await fs.writeFile(guiPortFilePath(), JSON.stringify(data, null, 2), 'utf-8');
|
|
@@ -129,11 +140,13 @@ async function discoverRunningGui() {
|
|
|
129
140
|
pid: info.pid,
|
|
130
141
|
url: info.url || `http://localhost:${info.port}`,
|
|
131
142
|
cwd: info.cwd,
|
|
143
|
+
project: info.project || projectLabel(info.cwd),
|
|
132
144
|
started_at: info.started_at
|
|
133
145
|
};
|
|
134
146
|
}
|
|
135
147
|
|
|
136
148
|
module.exports = {
|
|
149
|
+
projectLabel,
|
|
137
150
|
hashCwdToPort,
|
|
138
151
|
normalizeGuiPort,
|
|
139
152
|
resolvePreferredGuiPort,
|