backlog.md 0.1.0 → 0.1.1

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.
Files changed (165) hide show
  1. package/bin/backlog-darwin-arm64/backlog +0 -0
  2. package/bin/backlog-darwin-x64/backlog +0 -0
  3. package/bin/backlog-linux-arm64/backlog +0 -0
  4. package/{cli → bin/backlog-linux-x64}/backlog +0 -0
  5. package/bin/backlog-win32-x64/backlog.exe +0 -0
  6. package/cli.js +62 -0
  7. package/package.json +57 -46
  8. package/.backlog/archive/drafts/readme.md +0 -3
  9. package/.backlog/archive/drafts/task-41 - temporary-test-task.md +0 -13
  10. package/.backlog/archive/readme.md +0 -6
  11. package/.backlog/archive/tasks/readme.md +0 -3
  12. package/.backlog/archive/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +0 -14
  13. package/.backlog/config.yml +0 -7
  14. package/.backlog/decisions/readme.md +0 -7
  15. package/.backlog/docs/readme.md +0 -20
  16. package/.backlog/drafts/readme.md +0 -3
  17. package/.backlog/drafts/task-26 - docs-add-board-export-step-to-agent-dod.md +0 -21
  18. package/.backlog/drafts/task-28 - add-code-of-conduct.md +0 -20
  19. package/.backlog/drafts/task-30 - create-changelog.md +0 -19
  20. package/.backlog/milestones/m-0 - project-setup.md +0 -8
  21. package/.backlog/milestones/m-1 - cli.md +0 -8
  22. package/.backlog/milestones/m-2 - cli-kanban.md +0 -8
  23. package/.backlog/milestones/m-3 - gui.md +0 -8
  24. package/.backlog/milestones/m-4 - gui-kanban.md +0 -8
  25. package/.backlog/milestones/m-5 - gui-advanced.md +0 -12
  26. package/.backlog/milestones/readme.md +0 -3
  27. package/.backlog/readme.md +0 -5
  28. package/.backlog/tasks/readme.md +0 -37
  29. package/.backlog/tasks/task-1 - cli-setup-core-project.md +0 -23
  30. package/.backlog/tasks/task-10 - gui-init-packaging.md +0 -23
  31. package/.backlog/tasks/task-11 - gui-kanban-board.md +0 -26
  32. package/.backlog/tasks/task-12 - gui-advanced.md +0 -25
  33. package/.backlog/tasks/task-13 - cli-add-agent-instruction-prompt.md +0 -53
  34. package/.backlog/tasks/task-13.1 - cli-agent-instruction-file-selection.md +0 -40
  35. package/.backlog/tasks/task-14 - gui-introduction-screens.md +0 -21
  36. package/.backlog/tasks/task-15 - improve-tasks-readme-with-generic-example-and-cli-reference.md +0 -20
  37. package/.backlog/tasks/task-16 - improve-docs-readme-with-generic-example-and-cli-reference.md +0 -20
  38. package/.backlog/tasks/task-17 - improve-drafts-readme-with-generic-example-and-cli-reference.md +0 -20
  39. package/.backlog/tasks/task-18 - improve-decisions-readme-with-generic-example-and-cli-reference.md +0 -20
  40. package/.backlog/tasks/task-19 - cli-fix-default-task-status-and-remove-draft-from-statuses.md +0 -55
  41. package/.backlog/tasks/task-2 - cli-core-logic-library.md +0 -28
  42. package/.backlog/tasks/task-20 - add-agent-guideline-to-mark-tasks-in-progress-on-start.md +0 -32
  43. package/.backlog/tasks/task-21 - kanban-board-vertical-layout.md +0 -31
  44. package/.backlog/tasks/task-22 - cli-prevent-double-dash-in-task-filenames.md +0 -24
  45. package/.backlog/tasks/task-23 - cli-kanban-board-order-tasks-by-id-asc.md +0 -30
  46. package/.backlog/tasks/task-24 - handle-subtasks-in-the-kanban-view.md +0 -38
  47. package/.backlog/tasks/task-24.1 - cli-kanban-board-milestone-view.md +0 -19
  48. package/.backlog/tasks/task-25 - cli-export-kanban-board-to-readme.md +0 -28
  49. package/.backlog/tasks/task-27 - add-contributing-guidelines.md +0 -27
  50. package/.backlog/tasks/task-29 - add-github-templates.md +0 -28
  51. package/.backlog/tasks/task-3 - cli-implement-backlog-init.md +0 -63
  52. package/.backlog/tasks/task-31 - update-readme-for-open-source.md +0 -26
  53. package/.backlog/tasks/task-32 - cli-hide-empty-'no-status'-column.md +0 -31
  54. package/.backlog/tasks/task-33 - cli-export-milestones-board-as-roadmap.md +0 -20
  55. package/.backlog/tasks/task-34 - split-readme.md-for-users-and-contributors.md +0 -26
  56. package/.backlog/tasks/task-35 - finalize-package.json-metadata-for-publishing.md +0 -24
  57. package/.backlog/tasks/task-36 - cli-prompt-for-project-name-in-init.md +0 -24
  58. package/.backlog/tasks/task-37 - cli-board-view-open-tasks-in-ide.md +0 -19
  59. package/.backlog/tasks/task-38 - cli-improved-agent-selection-for-init.md +0 -25
  60. package/.backlog/tasks/task-39 - cli-fix-empty-agent-instruction-files-on-init.md +0 -31
  61. package/.backlog/tasks/task-4 - cli-task-management-commands.md +0 -28
  62. package/.backlog/tasks/task-4.1 - cli-task-create.md +0 -27
  63. package/.backlog/tasks/task-4.10 - use-cli-to-mark-tasks-done.md +0 -51
  64. package/.backlog/tasks/task-4.11 - docs-add-definition-of-done-to-agent-guidelines.md +0 -23
  65. package/.backlog/tasks/task-4.12 - cli-handle-task-id-conflicts-across-branches.md +0 -53
  66. package/.backlog/tasks/task-4.13 - cli-fix-config-command-local-global-logic.md +0 -58
  67. package/.backlog/tasks/task-4.2 - cli-task-list-view.md +0 -25
  68. package/.backlog/tasks/task-4.3 - cli-task-edit.md +0 -24
  69. package/.backlog/tasks/task-4.4 - cli-task-archive-transition.md +0 -27
  70. package/.backlog/tasks/task-4.5 - cli-init-prompts-for-reporter-name-and-global-local-config.md +0 -28
  71. package/.backlog/tasks/task-4.6 - cli-add-empty-assignee-array-field-for-new-tasks.md +0 -35
  72. package/.backlog/tasks/task-4.7 - cli-parse-unquoted-created_date.md +0 -40
  73. package/.backlog/tasks/task-4.8 - cli-enforce-description-header.md +0 -48
  74. package/.backlog/tasks/task-4.9 - cli-normalize-task-id-inputs.md +0 -66
  75. package/.backlog/tasks/task-40 - cli-board-command-defaults-to-view.md +0 -38
  76. package/.backlog/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +0 -93
  77. package/.backlog/tasks/task-41.1 - cli-bblessed-init-wizard.md +0 -42
  78. package/.backlog/tasks/task-41.2 - cli-bblessed-task-view.md +0 -44
  79. package/.backlog/tasks/task-41.3 - cli-bblessed-doc-view.md +0 -45
  80. package/.backlog/tasks/task-41.4 - cli-bblessed-board-view.md +0 -49
  81. package/.backlog/tasks/task-41.5 - cli-audit-remaining-ui-for-bblessed.md +0 -55
  82. package/.backlog/tasks/task-42 - visual-hierarchy.md +0 -54
  83. package/.backlog/tasks/task-43 - remove-duplicate-acceptance-criteria-and-style-metadata.md +0 -56
  84. package/.backlog/tasks/task-44 - checklist-alignment.md +0 -24
  85. package/.backlog/tasks/task-45 - safe-line-wrapping.md +0 -23
  86. package/.backlog/tasks/task-46 - split-pane-layout.md +0 -24
  87. package/.backlog/tasks/task-47 - sticky-header-in-detail-view.md +0 -43
  88. package/.backlog/tasks/task-48 - footer-hint-line.md +0 -21
  89. package/.backlog/tasks/task-49 - status-styling.md +0 -53
  90. package/.backlog/tasks/task-5 - cli-docs-decisions.md +0 -57
  91. package/.backlog/tasks/task-50 - borders-&-padding.md +0 -22
  92. package/.backlog/tasks/task-51 - code-path-styling.md +0 -23
  93. package/.backlog/tasks/task-52 - cli-filter-tasks-list-by-status-or-assignee.md +0 -29
  94. package/.backlog/tasks/task-6 - cli-packaging.md +0 -65
  95. package/.backlog/tasks/task-6.1 - cli-local-installation-support-for-bunx-npx.md +0 -49
  96. package/.backlog/tasks/task-6.2 - cli-github-actions-for-build-&-publish.md +0 -64
  97. package/.backlog/tasks/task-7 - cli-kanban-view.md +0 -60
  98. package/.backlog/tasks/task-7.1 - cli-kanban-board-detect-remote-task-status.md +0 -62
  99. package/.backlog/tasks/task-8 - gui-project-setup.md +0 -21
  100. package/.backlog/tasks/task-9 - gui-task-crud.md +0 -24
  101. package/.cursorrules +0 -223
  102. package/.gitattributes +0 -2
  103. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -25
  104. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -15
  105. package/.github/PULL_REQUEST_TEMPLATE.md +0 -8
  106. package/.github/workflows/ci.yml +0 -36
  107. package/.husky/pre-commit +0 -1
  108. package/AGENTS.md +0 -65
  109. package/CLAUDE.md +0 -87
  110. package/CONTRIBUTING.md +0 -19
  111. package/DEVELOPMENT.md +0 -37
  112. package/biome.json +0 -31
  113. package/bun.lock +0 -152
  114. package/cli/.cursorrules-xh86jabm.md +0 -82
  115. package/cli/AGENTS-xh86jabm.md +0 -82
  116. package/cli/CLAUDE-xh86jabm.md +0 -82
  117. package/cli/cli.js +0 -19622
  118. package/cli/index.js +0 -2
  119. package/docs/npm-publishing.md +0 -69
  120. package/scripts/build.js +0 -73
  121. package/src/agent-instructions.ts +0 -54
  122. package/src/board.ts +0 -263
  123. package/src/cli.ts +0 -806
  124. package/src/constants/index.ts +0 -48
  125. package/src/core/backlog.ts +0 -183
  126. package/src/core/remote-tasks.ts +0 -168
  127. package/src/file-system/operations.ts +0 -515
  128. package/src/git/operations.ts +0 -189
  129. package/src/guidelines/.cursorrules.md +0 -82
  130. package/src/guidelines/AGENTS.md +0 -82
  131. package/src/guidelines/CLAUDE.md +0 -82
  132. package/src/guidelines/index.ts +0 -7
  133. package/src/index.ts +0 -30
  134. package/src/markdown/parser.ts +0 -145
  135. package/src/markdown/serializer.ts +0 -71
  136. package/src/test/agent-instructions.test.ts +0 -62
  137. package/src/test/board.test.ts +0 -291
  138. package/src/test/build.test.ts +0 -28
  139. package/src/test/checklist.test.ts +0 -273
  140. package/src/test/cli.test.ts +0 -1300
  141. package/src/test/code-path.test.ts +0 -204
  142. package/src/test/core.test.ts +0 -330
  143. package/src/test/filesystem.test.ts +0 -435
  144. package/src/test/git.test.ts +0 -26
  145. package/src/test/heading.test.ts +0 -102
  146. package/src/test/line-wrapping.test.ts +0 -252
  147. package/src/test/local-install.test.ts +0 -34
  148. package/src/test/markdown.test.ts +0 -526
  149. package/src/test/parallel-loading.test.ts +0 -160
  150. package/src/test/parent-id-normalization.test.ts +0 -48
  151. package/src/test/remote-id-conflict.test.ts +0 -60
  152. package/src/test/status-icon.test.ts +0 -93
  153. package/src/types/blessed.d.ts +0 -14
  154. package/src/types/index.ts +0 -55
  155. package/src/types/raw.d.ts +0 -4
  156. package/src/ui/board.ts +0 -322
  157. package/src/ui/checklist.ts +0 -103
  158. package/src/ui/code-path.ts +0 -113
  159. package/src/ui/heading.ts +0 -121
  160. package/src/ui/loading.ts +0 -216
  161. package/src/ui/status-icon.ts +0 -53
  162. package/src/ui/task-list.ts +0 -168
  163. package/src/ui/task-viewer.ts +0 -640
  164. package/src/ui/tui.ts +0 -301
  165. package/tsconfig.json +0 -26
@@ -1,48 +0,0 @@
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
- });
@@ -1,60 +0,0 @@
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
- });
@@ -1,93 +0,0 @@
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
- });
@@ -1,14 +0,0 @@
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
- }
@@ -1,55 +0,0 @@
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
- }
@@ -1,4 +0,0 @@
1
- declare module "*.md?raw" {
2
- const content: string;
3
- export default content;
4
- }
package/src/ui/board.ts DELETED
@@ -1,322 +0,0 @@
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
- }