backlog.md 0.1.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/.backlog/archive/drafts/readme.md +3 -0
- package/.backlog/archive/drafts/task-41 - temporary-test-task.md +13 -0
- package/.backlog/archive/readme.md +6 -0
- package/.backlog/archive/tasks/readme.md +3 -0
- package/.backlog/archive/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +14 -0
- package/.backlog/config.yml +7 -0
- package/.backlog/decisions/readme.md +7 -0
- package/.backlog/docs/readme.md +20 -0
- package/.backlog/drafts/readme.md +3 -0
- package/.backlog/drafts/task-26 - docs-add-board-export-step-to-agent-dod.md +21 -0
- package/.backlog/drafts/task-28 - add-code-of-conduct.md +20 -0
- package/.backlog/drafts/task-30 - create-changelog.md +19 -0
- package/.backlog/milestones/m-0 - project-setup.md +8 -0
- package/.backlog/milestones/m-1 - cli.md +8 -0
- package/.backlog/milestones/m-2 - cli-kanban.md +8 -0
- package/.backlog/milestones/m-3 - gui.md +8 -0
- package/.backlog/milestones/m-4 - gui-kanban.md +8 -0
- package/.backlog/milestones/m-5 - gui-advanced.md +12 -0
- package/.backlog/milestones/readme.md +3 -0
- package/.backlog/readme.md +5 -0
- package/.backlog/tasks/readme.md +37 -0
- package/.backlog/tasks/task-1 - cli-setup-core-project.md +23 -0
- package/.backlog/tasks/task-10 - gui-init-packaging.md +23 -0
- package/.backlog/tasks/task-11 - gui-kanban-board.md +26 -0
- package/.backlog/tasks/task-12 - gui-advanced.md +25 -0
- package/.backlog/tasks/task-13 - cli-add-agent-instruction-prompt.md +53 -0
- package/.backlog/tasks/task-13.1 - cli-agent-instruction-file-selection.md +40 -0
- package/.backlog/tasks/task-14 - gui-introduction-screens.md +21 -0
- package/.backlog/tasks/task-15 - improve-tasks-readme-with-generic-example-and-cli-reference.md +20 -0
- package/.backlog/tasks/task-16 - improve-docs-readme-with-generic-example-and-cli-reference.md +20 -0
- package/.backlog/tasks/task-17 - improve-drafts-readme-with-generic-example-and-cli-reference.md +20 -0
- package/.backlog/tasks/task-18 - improve-decisions-readme-with-generic-example-and-cli-reference.md +20 -0
- package/.backlog/tasks/task-19 - cli-fix-default-task-status-and-remove-draft-from-statuses.md +55 -0
- package/.backlog/tasks/task-2 - cli-core-logic-library.md +28 -0
- package/.backlog/tasks/task-20 - add-agent-guideline-to-mark-tasks-in-progress-on-start.md +32 -0
- package/.backlog/tasks/task-21 - kanban-board-vertical-layout.md +31 -0
- package/.backlog/tasks/task-22 - cli-prevent-double-dash-in-task-filenames.md +24 -0
- package/.backlog/tasks/task-23 - cli-kanban-board-order-tasks-by-id-asc.md +30 -0
- package/.backlog/tasks/task-24 - handle-subtasks-in-the-kanban-view.md +38 -0
- package/.backlog/tasks/task-24.1 - cli-kanban-board-milestone-view.md +19 -0
- package/.backlog/tasks/task-25 - cli-export-kanban-board-to-readme.md +28 -0
- package/.backlog/tasks/task-27 - add-contributing-guidelines.md +27 -0
- package/.backlog/tasks/task-29 - add-github-templates.md +28 -0
- package/.backlog/tasks/task-3 - cli-implement-backlog-init.md +63 -0
- package/.backlog/tasks/task-31 - update-readme-for-open-source.md +26 -0
- package/.backlog/tasks/task-32 - cli-hide-empty-'no-status'-column.md +31 -0
- package/.backlog/tasks/task-33 - cli-export-milestones-board-as-roadmap.md +20 -0
- package/.backlog/tasks/task-34 - split-readme.md-for-users-and-contributors.md +26 -0
- package/.backlog/tasks/task-35 - finalize-package.json-metadata-for-publishing.md +24 -0
- package/.backlog/tasks/task-36 - cli-prompt-for-project-name-in-init.md +24 -0
- package/.backlog/tasks/task-37 - cli-board-view-open-tasks-in-ide.md +19 -0
- package/.backlog/tasks/task-38 - cli-improved-agent-selection-for-init.md +25 -0
- package/.backlog/tasks/task-39 - cli-fix-empty-agent-instruction-files-on-init.md +31 -0
- package/.backlog/tasks/task-4 - cli-task-management-commands.md +28 -0
- package/.backlog/tasks/task-4.1 - cli-task-create.md +27 -0
- package/.backlog/tasks/task-4.10 - use-cli-to-mark-tasks-done.md +51 -0
- package/.backlog/tasks/task-4.11 - docs-add-definition-of-done-to-agent-guidelines.md +23 -0
- package/.backlog/tasks/task-4.12 - cli-handle-task-id-conflicts-across-branches.md +53 -0
- package/.backlog/tasks/task-4.13 - cli-fix-config-command-local-global-logic.md +58 -0
- package/.backlog/tasks/task-4.2 - cli-task-list-view.md +25 -0
- package/.backlog/tasks/task-4.3 - cli-task-edit.md +24 -0
- package/.backlog/tasks/task-4.4 - cli-task-archive-transition.md +27 -0
- package/.backlog/tasks/task-4.5 - cli-init-prompts-for-reporter-name-and-global-local-config.md +28 -0
- package/.backlog/tasks/task-4.6 - cli-add-empty-assignee-array-field-for-new-tasks.md +35 -0
- package/.backlog/tasks/task-4.7 - cli-parse-unquoted-created_date.md +40 -0
- package/.backlog/tasks/task-4.8 - cli-enforce-description-header.md +48 -0
- package/.backlog/tasks/task-4.9 - cli-normalize-task-id-inputs.md +66 -0
- package/.backlog/tasks/task-40 - cli-board-command-defaults-to-view.md +38 -0
- package/.backlog/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +93 -0
- package/.backlog/tasks/task-41.1 - cli-bblessed-init-wizard.md +42 -0
- package/.backlog/tasks/task-41.2 - cli-bblessed-task-view.md +44 -0
- package/.backlog/tasks/task-41.3 - cli-bblessed-doc-view.md +45 -0
- package/.backlog/tasks/task-41.4 - cli-bblessed-board-view.md +49 -0
- package/.backlog/tasks/task-41.5 - cli-audit-remaining-ui-for-bblessed.md +55 -0
- package/.backlog/tasks/task-42 - visual-hierarchy.md +54 -0
- package/.backlog/tasks/task-43 - remove-duplicate-acceptance-criteria-and-style-metadata.md +56 -0
- package/.backlog/tasks/task-44 - checklist-alignment.md +24 -0
- package/.backlog/tasks/task-45 - safe-line-wrapping.md +23 -0
- package/.backlog/tasks/task-46 - split-pane-layout.md +24 -0
- package/.backlog/tasks/task-47 - sticky-header-in-detail-view.md +43 -0
- package/.backlog/tasks/task-48 - footer-hint-line.md +21 -0
- package/.backlog/tasks/task-49 - status-styling.md +53 -0
- package/.backlog/tasks/task-5 - cli-docs-decisions.md +57 -0
- package/.backlog/tasks/task-50 - borders-&-padding.md +22 -0
- package/.backlog/tasks/task-51 - code-path-styling.md +23 -0
- package/.backlog/tasks/task-52 - cli-filter-tasks-list-by-status-or-assignee.md +29 -0
- package/.backlog/tasks/task-6 - cli-packaging.md +65 -0
- package/.backlog/tasks/task-6.1 - cli-local-installation-support-for-bunx-npx.md +49 -0
- package/.backlog/tasks/task-6.2 - cli-github-actions-for-build-&-publish.md +64 -0
- package/.backlog/tasks/task-7 - cli-kanban-view.md +60 -0
- package/.backlog/tasks/task-7.1 - cli-kanban-board-detect-remote-task-status.md +62 -0
- package/.backlog/tasks/task-8 - gui-project-setup.md +21 -0
- package/.backlog/tasks/task-9 - gui-task-crud.md +24 -0
- package/.cursorrules +223 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +25 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +8 -0
- package/.github/workflows/ci.yml +36 -0
- package/.husky/pre-commit +1 -0
- package/AGENTS.md +65 -0
- package/CLAUDE.md +87 -0
- package/CONTRIBUTING.md +19 -0
- package/DEVELOPMENT.md +37 -0
- package/LICENSE +21 -0
- package/biome.json +31 -0
- package/bun.lock +152 -0
- package/cli/.cursorrules-xh86jabm.md +82 -0
- package/cli/AGENTS-xh86jabm.md +82 -0
- package/cli/CLAUDE-xh86jabm.md +82 -0
- package/cli/backlog +0 -0
- package/cli/cli.js +19622 -0
- package/cli/index.js +2 -0
- package/docs/npm-publishing.md +69 -0
- package/package.json +47 -0
- package/readme.md +97 -0
- package/scripts/build.js +73 -0
- package/src/agent-instructions.ts +54 -0
- package/src/board.ts +263 -0
- package/src/cli.ts +806 -0
- package/src/constants/index.ts +48 -0
- package/src/core/backlog.ts +183 -0
- package/src/core/remote-tasks.ts +168 -0
- package/src/file-system/operations.ts +515 -0
- package/src/git/operations.ts +189 -0
- package/src/guidelines/.cursorrules.md +82 -0
- package/src/guidelines/AGENTS.md +82 -0
- package/src/guidelines/CLAUDE.md +82 -0
- package/src/guidelines/index.ts +7 -0
- package/src/index.ts +30 -0
- package/src/markdown/parser.ts +145 -0
- package/src/markdown/serializer.ts +71 -0
- package/src/test/agent-instructions.test.ts +62 -0
- package/src/test/board.test.ts +291 -0
- package/src/test/build.test.ts +28 -0
- package/src/test/checklist.test.ts +273 -0
- package/src/test/cli.test.ts +1300 -0
- package/src/test/code-path.test.ts +204 -0
- package/src/test/core.test.ts +330 -0
- package/src/test/filesystem.test.ts +435 -0
- package/src/test/git.test.ts +26 -0
- package/src/test/heading.test.ts +102 -0
- package/src/test/line-wrapping.test.ts +252 -0
- package/src/test/local-install.test.ts +34 -0
- package/src/test/markdown.test.ts +526 -0
- package/src/test/parallel-loading.test.ts +160 -0
- package/src/test/parent-id-normalization.test.ts +48 -0
- package/src/test/remote-id-conflict.test.ts +60 -0
- package/src/test/status-icon.test.ts +93 -0
- package/src/types/blessed.d.ts +14 -0
- package/src/types/index.ts +55 -0
- package/src/types/raw.d.ts +4 -0
- package/src/ui/board.ts +322 -0
- package/src/ui/checklist.ts +103 -0
- package/src/ui/code-path.ts +113 -0
- package/src/ui/heading.ts +121 -0
- package/src/ui/loading.ts +216 -0
- package/src/ui/status-icon.ts +53 -0
- package/src/ui/task-list.ts +168 -0
- package/src/ui/task-viewer.ts +640 -0
- package/src/ui/tui.ts +301 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Core } from "../index.ts";
|
|
5
|
+
import type { Task } from "../types/index.ts";
|
|
6
|
+
|
|
7
|
+
const TEST_DIR = join(process.cwd(), "test-parent-normalization");
|
|
8
|
+
const CLI_PATH = join(process.cwd(), "src", "cli.ts");
|
|
9
|
+
|
|
10
|
+
async function initGitRepo(dir: string) {
|
|
11
|
+
await Bun.spawn(["git", "init"], { cwd: dir }).exited;
|
|
12
|
+
await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: dir }).exited;
|
|
13
|
+
await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: dir }).exited;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("CLI parent task id normalization", () => {
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
await rm(TEST_DIR, { recursive: true, force: true }).catch(() => {});
|
|
19
|
+
await mkdir(TEST_DIR, { recursive: true });
|
|
20
|
+
await initGitRepo(TEST_DIR);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await rm(TEST_DIR, { recursive: true, force: true }).catch(() => {});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should normalize parent task id when creating subtasks", async () => {
|
|
28
|
+
const core = new Core(TEST_DIR);
|
|
29
|
+
await core.initializeProject("Normalization Test");
|
|
30
|
+
|
|
31
|
+
const parent: Task = {
|
|
32
|
+
id: "task-4",
|
|
33
|
+
title: "Parent",
|
|
34
|
+
status: "To Do",
|
|
35
|
+
assignee: [],
|
|
36
|
+
createdDate: "2025-06-08",
|
|
37
|
+
labels: [],
|
|
38
|
+
dependencies: [],
|
|
39
|
+
description: "",
|
|
40
|
+
};
|
|
41
|
+
await core.createTask(parent, true);
|
|
42
|
+
|
|
43
|
+
await Bun.spawn(["bun", "run", CLI_PATH, "task", "create", "Child", "--parent", "4"], { cwd: TEST_DIR }).exited;
|
|
44
|
+
|
|
45
|
+
const child = await core.filesystem.loadTask("task-4.1");
|
|
46
|
+
expect(child?.parentTaskId).toBe("task-4");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Core } from "../index.ts";
|
|
5
|
+
|
|
6
|
+
const TEST_DIR = join(process.cwd(), "test-remote-id");
|
|
7
|
+
const REMOTE_DIR = join(TEST_DIR, "remote.git");
|
|
8
|
+
const LOCAL_DIR = join(TEST_DIR, "local");
|
|
9
|
+
const CLI_PATH = join(process.cwd(), "src", "cli.ts");
|
|
10
|
+
|
|
11
|
+
async function initRepo(dir: string) {
|
|
12
|
+
await Bun.spawn(["git", "init"], { cwd: dir }).exited;
|
|
13
|
+
await Bun.spawn(["git", "config", "user.name", "Test"], { cwd: dir }).exited;
|
|
14
|
+
await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: dir }).exited;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("next id across remote branches", () => {
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
await rm(TEST_DIR, { recursive: true, force: true }).catch(() => {});
|
|
20
|
+
await mkdir(REMOTE_DIR, { recursive: true });
|
|
21
|
+
await Bun.spawn(["git", "init", "--bare"], { cwd: REMOTE_DIR }).exited;
|
|
22
|
+
await mkdir(LOCAL_DIR, { recursive: true });
|
|
23
|
+
await initRepo(LOCAL_DIR);
|
|
24
|
+
await Bun.spawn(["git", "remote", "add", "origin", REMOTE_DIR], { cwd: LOCAL_DIR }).exited;
|
|
25
|
+
|
|
26
|
+
const core = new Core(LOCAL_DIR);
|
|
27
|
+
await core.initializeProject("Remote Test");
|
|
28
|
+
await Bun.spawn(["git", "branch", "-M", "main"], { cwd: LOCAL_DIR }).exited;
|
|
29
|
+
await Bun.spawn(["git", "push", "-u", "origin", "main"], { cwd: LOCAL_DIR }).exited;
|
|
30
|
+
|
|
31
|
+
await Bun.spawn(["git", "checkout", "-b", "feature"], { cwd: LOCAL_DIR }).exited;
|
|
32
|
+
await core.createTask(
|
|
33
|
+
{
|
|
34
|
+
id: "task-1",
|
|
35
|
+
title: "Remote Task",
|
|
36
|
+
status: "To Do",
|
|
37
|
+
assignee: [],
|
|
38
|
+
createdDate: "2025-06-08",
|
|
39
|
+
labels: [],
|
|
40
|
+
dependencies: [],
|
|
41
|
+
description: "",
|
|
42
|
+
},
|
|
43
|
+
true,
|
|
44
|
+
);
|
|
45
|
+
await Bun.spawn(["git", "push", "-u", "origin", "feature"], { cwd: LOCAL_DIR }).exited;
|
|
46
|
+
await Bun.spawn(["git", "checkout", "main"], { cwd: LOCAL_DIR }).exited;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterAll(async () => {
|
|
50
|
+
await rm(TEST_DIR, { recursive: true, force: true }).catch(() => {});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("uses id after highest remote task", async () => {
|
|
54
|
+
const result = Bun.spawnSync(["bun", "run", CLI_PATH, "task", "create", "Local Task"], { cwd: LOCAL_DIR });
|
|
55
|
+
expect(result.stdout.toString()).toContain("Created task task-2");
|
|
56
|
+
const core = new Core(LOCAL_DIR);
|
|
57
|
+
const task = await core.filesystem.loadTask("task-2");
|
|
58
|
+
expect(task).not.toBeNull();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { formatStatusWithIcon, getStatusColor, getStatusIcon, getStatusStyle } from "../ui/status-icon.ts";
|
|
3
|
+
|
|
4
|
+
describe("Status Icon Component", () => {
|
|
5
|
+
describe("getStatusStyle", () => {
|
|
6
|
+
test("returns correct style for Done status", () => {
|
|
7
|
+
const style = getStatusStyle("Done");
|
|
8
|
+
expect(style.icon).toBe("✔");
|
|
9
|
+
expect(style.color).toBe("green");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("returns correct style for In Progress status", () => {
|
|
13
|
+
const style = getStatusStyle("In Progress");
|
|
14
|
+
expect(style.icon).toBe("◒");
|
|
15
|
+
expect(style.color).toBe("yellow");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("returns correct style for Blocked status", () => {
|
|
19
|
+
const style = getStatusStyle("Blocked");
|
|
20
|
+
expect(style.icon).toBe("●");
|
|
21
|
+
expect(style.color).toBe("red");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("returns correct style for To Do status", () => {
|
|
25
|
+
const style = getStatusStyle("To Do");
|
|
26
|
+
expect(style.icon).toBe("○");
|
|
27
|
+
expect(style.color).toBe("white");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("returns correct style for Review status", () => {
|
|
31
|
+
const style = getStatusStyle("Review");
|
|
32
|
+
expect(style.icon).toBe("◆");
|
|
33
|
+
expect(style.color).toBe("blue");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("returns correct style for Testing status", () => {
|
|
37
|
+
const style = getStatusStyle("Testing");
|
|
38
|
+
expect(style.icon).toBe("▣");
|
|
39
|
+
expect(style.color).toBe("cyan");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("returns default style for unknown status", () => {
|
|
43
|
+
const style = getStatusStyle("Unknown Status");
|
|
44
|
+
expect(style.icon).toBe("○");
|
|
45
|
+
expect(style.color).toBe("white");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("getStatusColor", () => {
|
|
50
|
+
test("returns correct color for each status", () => {
|
|
51
|
+
expect(getStatusColor("Done")).toBe("green");
|
|
52
|
+
expect(getStatusColor("In Progress")).toBe("yellow");
|
|
53
|
+
expect(getStatusColor("Blocked")).toBe("red");
|
|
54
|
+
expect(getStatusColor("To Do")).toBe("white");
|
|
55
|
+
expect(getStatusColor("Review")).toBe("blue");
|
|
56
|
+
expect(getStatusColor("Testing")).toBe("cyan");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("returns default color for unknown status", () => {
|
|
60
|
+
expect(getStatusColor("Unknown")).toBe("white");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("getStatusIcon", () => {
|
|
65
|
+
test("returns correct icon for each status", () => {
|
|
66
|
+
expect(getStatusIcon("Done")).toBe("✔");
|
|
67
|
+
expect(getStatusIcon("In Progress")).toBe("◒");
|
|
68
|
+
expect(getStatusIcon("Blocked")).toBe("●");
|
|
69
|
+
expect(getStatusIcon("To Do")).toBe("○");
|
|
70
|
+
expect(getStatusIcon("Review")).toBe("◆");
|
|
71
|
+
expect(getStatusIcon("Testing")).toBe("▣");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("returns default icon for unknown status", () => {
|
|
75
|
+
expect(getStatusIcon("Unknown")).toBe("○");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("formatStatusWithIcon", () => {
|
|
80
|
+
test("formats status with correct icon", () => {
|
|
81
|
+
expect(formatStatusWithIcon("Done")).toBe("✔ Done");
|
|
82
|
+
expect(formatStatusWithIcon("In Progress")).toBe("◒ In Progress");
|
|
83
|
+
expect(formatStatusWithIcon("Blocked")).toBe("● Blocked");
|
|
84
|
+
expect(formatStatusWithIcon("To Do")).toBe("○ To Do");
|
|
85
|
+
expect(formatStatusWithIcon("Review")).toBe("◆ Review");
|
|
86
|
+
expect(formatStatusWithIcon("Testing")).toBe("▣ Testing");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("formats unknown status with default icon", () => {
|
|
90
|
+
expect(formatStatusWithIcon("Custom Status")).toBe("○ Custom Status");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Minimal declaration to satisfy TypeScript when importing "bblessed".
|
|
2
|
+
// The actual library is dynamically imported at runtime. We only need the
|
|
3
|
+
// "any" type shape to avoid compile-time errors when the package is not
|
|
4
|
+
// present during type checking.
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
declare module "blessed" {
|
|
8
|
+
// We intentionally use the most permissive type to keep the shim minimal.
|
|
9
|
+
// Downstream code performs runtime feature detection and falls back to a
|
|
10
|
+
// non-TUI implementation when the real library cannot be resolved.
|
|
11
|
+
// biome-ignore lint/suspicious/noExplicitAny: blessed types are complex
|
|
12
|
+
const blessed: any;
|
|
13
|
+
export = blessed;
|
|
14
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface Task {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
status: string;
|
|
5
|
+
assignee: string[];
|
|
6
|
+
reporter?: string;
|
|
7
|
+
createdDate: string;
|
|
8
|
+
updatedDate?: string;
|
|
9
|
+
labels: string[];
|
|
10
|
+
milestone?: string;
|
|
11
|
+
dependencies: string[];
|
|
12
|
+
description: string;
|
|
13
|
+
acceptanceCriteria?: string[];
|
|
14
|
+
parentTaskId?: string;
|
|
15
|
+
subtasks?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DecisionLog {
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
date: string;
|
|
22
|
+
status: "proposed" | "accepted" | "rejected" | "superseded";
|
|
23
|
+
context: string;
|
|
24
|
+
decision: string;
|
|
25
|
+
consequences: string;
|
|
26
|
+
alternatives?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Document {
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
type: "readme" | "guide" | "specification" | "other";
|
|
33
|
+
createdDate: string;
|
|
34
|
+
updatedDate?: string;
|
|
35
|
+
content: string;
|
|
36
|
+
tags?: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BacklogConfig {
|
|
40
|
+
projectName: string;
|
|
41
|
+
defaultAssignee?: string;
|
|
42
|
+
defaultReporter?: string;
|
|
43
|
+
statuses: string[];
|
|
44
|
+
labels: string[];
|
|
45
|
+
milestones: string[];
|
|
46
|
+
defaultStatus?: string;
|
|
47
|
+
dateFormat: string;
|
|
48
|
+
maxColumnWidth?: number;
|
|
49
|
+
taskResolutionStrategy?: "most_recent" | "most_progressed";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ParsedMarkdown {
|
|
53
|
+
frontmatter: Record<string, unknown>;
|
|
54
|
+
content: string;
|
|
55
|
+
}
|
package/src/ui/board.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/* Kanban board renderer for the bblessed TUI. */
|
|
2
|
+
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
6
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
7
|
+
import { type BoardLayout, compareIds, generateKanbanBoard } from "../board.ts";
|
|
8
|
+
import { Core } from "../core/backlog.ts";
|
|
9
|
+
import type { Task } from "../types/index.ts";
|
|
10
|
+
import { formatStatusWithIcon, getStatusColor, getStatusIcon } from "./status-icon.ts";
|
|
11
|
+
import { createTaskPopup } from "./task-viewer.ts";
|
|
12
|
+
|
|
13
|
+
// Load blessed dynamically
|
|
14
|
+
// biome-ignore lint/suspicious/noExplicitAny: blessed is dynamically loaded
|
|
15
|
+
async function loadBlessed(): Promise<any | null> {
|
|
16
|
+
// Don't check TTY in Bun - let blessed handle it
|
|
17
|
+
try {
|
|
18
|
+
// Try using createRequire for better compatibility
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const blessed = require("blessed");
|
|
21
|
+
return blessed;
|
|
22
|
+
} catch {
|
|
23
|
+
try {
|
|
24
|
+
// Fallback to dynamic import
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
26
|
+
// @ts-ignore — module may not exist at runtime.
|
|
27
|
+
const mod = await import("blessed");
|
|
28
|
+
return mod.default ?? mod;
|
|
29
|
+
} catch {
|
|
30
|
+
// Blessed may not work in bundled executables
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Render the provided tasks in a TUI. Falls back to plain text when the
|
|
38
|
+
* terminal UI cannot be initialized.
|
|
39
|
+
*/
|
|
40
|
+
export async function renderBoardTui(
|
|
41
|
+
tasks: Task[],
|
|
42
|
+
statuses: string[],
|
|
43
|
+
layout: BoardLayout,
|
|
44
|
+
maxColumnWidth: number,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
const blessed = await loadBlessed();
|
|
47
|
+
if (!blessed) {
|
|
48
|
+
// Fallback to ASCII board
|
|
49
|
+
const boardStr = generateKanbanBoard(tasks, statuses, layout, maxColumnWidth);
|
|
50
|
+
console.log(boardStr);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Group tasks by status
|
|
55
|
+
const tasksByStatus = new Map<string, Task[]>();
|
|
56
|
+
for (const status of statuses) {
|
|
57
|
+
tasksByStatus.set(status, []);
|
|
58
|
+
}
|
|
59
|
+
for (const task of tasks) {
|
|
60
|
+
const status = task.status || "";
|
|
61
|
+
if (!tasksByStatus.has(status)) {
|
|
62
|
+
tasksByStatus.set(status, []);
|
|
63
|
+
}
|
|
64
|
+
const statusTasks = tasksByStatus.get(status);
|
|
65
|
+
if (statusTasks) {
|
|
66
|
+
statusTasks.push(task);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Filter out empty statuses
|
|
71
|
+
const nonEmptyStatuses = statuses.filter((status) => {
|
|
72
|
+
const tasks = tasksByStatus.get(status);
|
|
73
|
+
return tasks && tasks.length > 0;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// If no columns have tasks, show a message
|
|
77
|
+
if (nonEmptyStatuses.length === 0) {
|
|
78
|
+
console.log("No tasks found in any status.");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return new Promise<void>((resolve) => {
|
|
83
|
+
const screen = blessed.screen({
|
|
84
|
+
smartCSR: true,
|
|
85
|
+
title: "Backlog Board View",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Create container for the board
|
|
89
|
+
const container = blessed.box({
|
|
90
|
+
parent: screen,
|
|
91
|
+
width: "100%",
|
|
92
|
+
height: "100%",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Calculate column dimensions
|
|
96
|
+
const columnCount = nonEmptyStatuses.length || 1;
|
|
97
|
+
const columnWidth = Math.floor(100 / columnCount);
|
|
98
|
+
|
|
99
|
+
// Create columns
|
|
100
|
+
// biome-ignore lint/suspicious/noExplicitAny: blessed column structure
|
|
101
|
+
const columns: any[] = [];
|
|
102
|
+
|
|
103
|
+
nonEmptyStatuses.forEach((status, index) => {
|
|
104
|
+
// Calculate exact position for each column
|
|
105
|
+
const left = index * columnWidth;
|
|
106
|
+
const isLast = index === nonEmptyStatuses.length - 1;
|
|
107
|
+
|
|
108
|
+
// For the last column, extend to the right edge to avoid gaps
|
|
109
|
+
const width = isLast ? `${100 - left}%` : `${columnWidth}%`;
|
|
110
|
+
|
|
111
|
+
// Column container
|
|
112
|
+
const column = blessed.box({
|
|
113
|
+
parent: container,
|
|
114
|
+
left: `${left}%`,
|
|
115
|
+
top: 0,
|
|
116
|
+
width,
|
|
117
|
+
height: "100%-1", // Leave space for footer
|
|
118
|
+
border: {
|
|
119
|
+
type: "line",
|
|
120
|
+
},
|
|
121
|
+
style: {
|
|
122
|
+
border: {
|
|
123
|
+
fg: "gray",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
label: ` ${getStatusIcon(status)} ${status || "No Status"} (${tasksByStatus.get(status)?.length || 0}) `,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Task list for this column
|
|
130
|
+
const taskList = blessed.list({
|
|
131
|
+
parent: column,
|
|
132
|
+
top: 1, // Start below the border
|
|
133
|
+
left: 1, // Start after the border
|
|
134
|
+
width: "100%-4", // Account for borders and padding
|
|
135
|
+
height: "100%-3", // Account for borders
|
|
136
|
+
items: [],
|
|
137
|
+
keys: false, // Disable built-in key handling
|
|
138
|
+
vi: false,
|
|
139
|
+
mouse: true,
|
|
140
|
+
scrollable: true,
|
|
141
|
+
alwaysScroll: false,
|
|
142
|
+
selectedBg: undefined, // Don't show selection by default
|
|
143
|
+
style: {
|
|
144
|
+
selected: {
|
|
145
|
+
bg: undefined, // Initially no background
|
|
146
|
+
fg: "white",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
tags: true, // Enable tag parsing for colors
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Populate tasks with enhanced styling
|
|
153
|
+
const tasksInStatus = (tasksByStatus.get(status) || []).sort(compareIds);
|
|
154
|
+
const items = tasksInStatus.map((task) => {
|
|
155
|
+
const assigneeText = task.assignee?.length
|
|
156
|
+
? ` {cyan-fg}${task.assignee[0].startsWith("@") ? task.assignee[0] : `@${task.assignee[0]}`}{/}`
|
|
157
|
+
: "";
|
|
158
|
+
const labelsText = task.labels?.length ? ` {yellow-fg}[${task.labels.join(", ")}]{/}` : "";
|
|
159
|
+
|
|
160
|
+
return `{bold}${task.id}{/bold} - ${task.title}${assigneeText}${labelsText}`;
|
|
161
|
+
});
|
|
162
|
+
taskList.setItems(items);
|
|
163
|
+
|
|
164
|
+
// Store reference for navigation
|
|
165
|
+
columns.push({ box: column, list: taskList, status, tasks: tasksInStatus });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Current column index and popup state
|
|
169
|
+
let currentColumn = 0;
|
|
170
|
+
let isPopupOpen = false;
|
|
171
|
+
|
|
172
|
+
if (columns.length > 0) {
|
|
173
|
+
columns[currentColumn].list.focus();
|
|
174
|
+
columns[currentColumn].list.select(0);
|
|
175
|
+
// Only show selection on the active column
|
|
176
|
+
columns[currentColumn].list.style.selected.bg = "blue";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Helper function to switch columns
|
|
180
|
+
const switchToColumn = (newColumn: number) => {
|
|
181
|
+
// Don't allow navigation while popup is open
|
|
182
|
+
if (isPopupOpen) return;
|
|
183
|
+
|
|
184
|
+
if (newColumn >= 0 && newColumn < columns.length && newColumn !== currentColumn) {
|
|
185
|
+
// Remember current selection index
|
|
186
|
+
const currentIndex = columns[currentColumn].list.selected || 0;
|
|
187
|
+
|
|
188
|
+
// Unfocus current column
|
|
189
|
+
columns[currentColumn].list.style.selected.bg = undefined;
|
|
190
|
+
columns[currentColumn].list.screen.render();
|
|
191
|
+
|
|
192
|
+
// Focus new column
|
|
193
|
+
currentColumn = newColumn;
|
|
194
|
+
const newList = columns[currentColumn].list;
|
|
195
|
+
newList.focus();
|
|
196
|
+
newList.style.selected.bg = "blue";
|
|
197
|
+
|
|
198
|
+
// Set selection to same index or last item if fewer items
|
|
199
|
+
const newIndex = Math.min(currentIndex, newList.items.length - 1);
|
|
200
|
+
newList.select(Math.max(0, newIndex));
|
|
201
|
+
|
|
202
|
+
screen.render();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Navigation between columns
|
|
207
|
+
screen.key(["left", "h"], () => {
|
|
208
|
+
switchToColumn(currentColumn - 1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
screen.key(["right", "l"], () => {
|
|
212
|
+
switchToColumn(currentColumn + 1);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Up/down navigation within column
|
|
216
|
+
screen.key(["up", "k"], () => {
|
|
217
|
+
// Don't allow navigation while popup is open
|
|
218
|
+
if (isPopupOpen) return;
|
|
219
|
+
|
|
220
|
+
const list = columns[currentColumn].list;
|
|
221
|
+
const selected = list.selected || 0;
|
|
222
|
+
if (selected > 0) {
|
|
223
|
+
list.select(selected - 1);
|
|
224
|
+
screen.render();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
screen.key(["down", "j"], () => {
|
|
229
|
+
// Don't allow navigation while popup is open
|
|
230
|
+
if (isPopupOpen) return;
|
|
231
|
+
|
|
232
|
+
const list = columns[currentColumn].list;
|
|
233
|
+
const selected = list.selected || 0;
|
|
234
|
+
if (selected < list.items.length - 1) {
|
|
235
|
+
list.select(selected + 1);
|
|
236
|
+
screen.render();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Show task details on enter
|
|
241
|
+
screen.key(["enter"], async () => {
|
|
242
|
+
// Don't allow opening multiple popups
|
|
243
|
+
if (isPopupOpen) return;
|
|
244
|
+
|
|
245
|
+
const column = columns[currentColumn];
|
|
246
|
+
const selected = column.list.selected;
|
|
247
|
+
if (selected >= 0 && selected < column.tasks.length) {
|
|
248
|
+
const task = column.tasks[selected];
|
|
249
|
+
isPopupOpen = true;
|
|
250
|
+
|
|
251
|
+
// Load task content
|
|
252
|
+
let content = "";
|
|
253
|
+
try {
|
|
254
|
+
const core = new Core(process.cwd());
|
|
255
|
+
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.tasksDir }));
|
|
256
|
+
const taskFile = files.find((f) => f.startsWith(`${task.id} -`));
|
|
257
|
+
if (taskFile) {
|
|
258
|
+
const filePath = join(core.filesystem.tasksDir, taskFile);
|
|
259
|
+
content = await Bun.file(filePath).text();
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
// Use task data if file cannot be loaded
|
|
263
|
+
content = "";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Create enhanced popup
|
|
267
|
+
const popupData = await createTaskPopup(screen, task, content);
|
|
268
|
+
if (!popupData) {
|
|
269
|
+
isPopupOpen = false;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { popup, contentArea, close } = popupData;
|
|
274
|
+
|
|
275
|
+
// Add escape handler directly to the focused content area
|
|
276
|
+
contentArea.key(["escape", "q"], () => {
|
|
277
|
+
isPopupOpen = false;
|
|
278
|
+
close();
|
|
279
|
+
columns[currentColumn].list.focus();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
screen.render();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Footer hint line
|
|
287
|
+
const helpText = blessed.box({
|
|
288
|
+
parent: screen,
|
|
289
|
+
bottom: 0,
|
|
290
|
+
left: 0,
|
|
291
|
+
width: "100%",
|
|
292
|
+
height: 1,
|
|
293
|
+
border: "line",
|
|
294
|
+
content: " ←/→ columns · ↑/↓ tasks · Enter view details · q/Esc quit ",
|
|
295
|
+
style: {
|
|
296
|
+
fg: "gray",
|
|
297
|
+
border: { fg: "gray" },
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Exit keys - only when no popup is active
|
|
302
|
+
screen.key(["q", "C-c"], () => {
|
|
303
|
+
screen.destroy();
|
|
304
|
+
resolve();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Global escape handler - only exit if no popup is active
|
|
308
|
+
screen.key(["escape"], () => {
|
|
309
|
+
// Check if any popup is currently displayed
|
|
310
|
+
const hasPopup = screen.children.some(
|
|
311
|
+
// biome-ignore lint/suspicious/noExplicitAny: blessed types are complex
|
|
312
|
+
(child: any) => child !== container && child !== helpText && child.visible !== false,
|
|
313
|
+
);
|
|
314
|
+
if (!hasPopup) {
|
|
315
|
+
screen.destroy();
|
|
316
|
+
resolve();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
screen.render();
|
|
321
|
+
});
|
|
322
|
+
}
|