backlog.md 0.1.0 → 0.1.3

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
package/cli/index.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import("./cli.js");
@@ -1,69 +0,0 @@
1
- # NPM Publishing Setup
2
-
3
- This guide explains how to set up npm publishing for the Backlog.md project.
4
-
5
- ## Prerequisites
6
-
7
- 1. An npm account with publish permissions for the `@backlog.md` package
8
- 2. Repository admin access to configure GitHub secrets
9
-
10
- ## Setting up the NPM Token
11
-
12
- ### 1. Generate an npm Access Token
13
-
14
- 1. Log in to [npmjs.com](https://www.npmjs.com)
15
- 2. Click on your profile picture → Access Tokens
16
- 3. Click "Generate New Token"
17
- 4. Choose "Automation" token type (recommended for CI/CD)
18
- 5. Name it something like "backlog-md-github-actions"
19
- 6. Copy the generated token (it starts with `npm_`)
20
-
21
- ### 2. Add the Token to GitHub Secrets
22
-
23
- 1. Go to the GitHub repository settings
24
- 2. Navigate to Settings → Secrets and variables → Actions
25
- 3. Click "New repository secret"
26
- 4. Name: `NPM_TOKEN`
27
- 5. Value: Paste your npm token
28
- 6. Click "Add secret"
29
-
30
- ## How Publishing Works
31
-
32
- The GitHub Actions workflow (`/.github/workflows/ci.yml`) automatically publishes to npm when:
33
-
34
- 1. A new git tag is pushed that matches the pattern `v*.*.*` (e.g., `v1.0.0`)
35
- 2. The build and tests pass successfully
36
-
37
- The workflow:
38
- - Uses `actions/setup-node@v4` to configure npm authentication
39
- - Sets the registry URL to `https://registry.npmjs.org`
40
- - Uses the `NPM_TOKEN` secret for authentication
41
- - Runs `npm publish --access public` to publish the package
42
-
43
- ## Troubleshooting
44
-
45
- ### Error: ENEEDAUTH
46
-
47
- If you see this error, it means the `NPM_TOKEN` secret is either:
48
- - Not configured in GitHub
49
- - Invalid or expired
50
- - Missing the required publish permissions
51
-
52
- ### Error: 403 Forbidden
53
-
54
- This usually means:
55
- - The token doesn't have permission to publish to the package
56
- - The package name is already taken by another user
57
- - You're not listed as a maintainer of the package
58
-
59
- ## Manual Publishing (Not Recommended)
60
-
61
- If you need to publish manually:
62
-
63
- ```bash
64
- npm login
65
- bun run build
66
- npm publish --access public
67
- ```
68
-
69
- Always prefer using the automated workflow to ensure consistency.
package/scripts/build.js DELETED
@@ -1,73 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawn } from "node:child_process";
4
- import { chmod, mkdir, writeFile } from "node:fs/promises";
5
- import { platform } from "node:os";
6
- import { dirname, join } from "node:path";
7
-
8
- const isWindows = platform() === "win32";
9
- const outDir = "cli";
10
- const indexFile = join(outDir, "index.js");
11
- const executableName = isWindows ? "backlog.exe" : "backlog";
12
- const executablePath = join(outDir, executableName);
13
-
14
- async function runCommand(command, args) {
15
- return new Promise((resolve, reject) => {
16
- const child = spawn(command, args, {
17
- stdio: "inherit",
18
- shell: isWindows,
19
- });
20
-
21
- child.on("close", (code) => {
22
- if (code === 0) {
23
- resolve();
24
- } else {
25
- reject(new Error(`Command failed with exit code ${code}`));
26
- }
27
- });
28
-
29
- child.on("error", reject);
30
- });
31
- }
32
-
33
- async function build() {
34
- try {
35
- console.log("Building CLI...");
36
-
37
- // Ensure output directory exists
38
- await mkdir(dirname(indexFile), { recursive: true });
39
-
40
- // Build JavaScript bundle
41
- console.log("Building JavaScript bundle...");
42
- await runCommand("bun", ["build", "src/cli.ts", "--outdir", outDir, "--target", "node"]);
43
-
44
- // Create index.js wrapper
45
- console.log("Creating index.js wrapper...");
46
- const indexContent = isWindows ? 'import("./cli.js");' : '#!/usr/bin/env node\nimport("./cli.js");';
47
-
48
- await writeFile(indexFile, indexContent);
49
-
50
- // Make executable on Unix systems
51
- if (!isWindows) {
52
- await chmod(indexFile, "755");
53
- }
54
-
55
- // Build compiled executable
56
- console.log("Building compiled executable...");
57
- await runCommand("bun", ["build", "src/cli.ts", "--compile", "--outfile", executablePath]);
58
-
59
- // Make executable on Unix systems
60
- if (!isWindows) {
61
- await chmod(executablePath, "755");
62
- }
63
-
64
- console.log("Build completed successfully!");
65
- console.log(`Executable: ${executablePath}`);
66
- console.log(`JS Bundle: ${indexFile}`);
67
- } catch (error) {
68
- console.error("Build failed:", error.message);
69
- process.exit(1);
70
- }
71
- }
72
-
73
- build();
@@ -1,54 +0,0 @@
1
- import { dirname, isAbsolute, join } from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { AGENT_GUIDELINES, CLAUDE_GUIDELINES, CURSOR_GUIDELINES, README_GUIDELINES } from "./constants/index.ts";
4
- import type { GitOperations } from "./git/operations.ts";
5
-
6
- export type AgentInstructionFile = "AGENTS.md" | "CLAUDE.md" | ".cursorrules" | "readme.md";
7
-
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
-
10
- async function loadContent(textOrPath: string): Promise<string> {
11
- if (textOrPath.includes("\n")) return textOrPath;
12
- try {
13
- const path = isAbsolute(textOrPath) ? textOrPath : join(__dirname, textOrPath);
14
- return await Bun.file(path).text();
15
- } catch {
16
- return textOrPath;
17
- }
18
- }
19
-
20
- export async function addAgentInstructions(
21
- projectRoot: string,
22
- git?: GitOperations,
23
- files: AgentInstructionFile[] = ["AGENTS.md", "CLAUDE.md", ".cursorrules"],
24
- ): Promise<void> {
25
- const mapping: Record<AgentInstructionFile, string> = {
26
- "AGENTS.md": AGENT_GUIDELINES,
27
- "CLAUDE.md": CLAUDE_GUIDELINES,
28
- ".cursorrules": CURSOR_GUIDELINES,
29
- "readme.md": README_GUIDELINES,
30
- };
31
-
32
- const paths: string[] = [];
33
- for (const name of files) {
34
- const content = await loadContent(mapping[name]);
35
- const filePath = join(projectRoot, name);
36
- let existing = "";
37
- try {
38
- existing = await Bun.file(filePath).text();
39
- if (!existing.endsWith("\n")) existing += "\n";
40
- existing += content;
41
- } catch {
42
- existing = content;
43
- }
44
- await Bun.write(filePath, existing);
45
- paths.push(filePath);
46
- }
47
-
48
- if (git && paths.length > 0) {
49
- await git.addFiles(paths);
50
- await git.commitChanges("Add AI agent instructions");
51
- }
52
- }
53
-
54
- export { loadContent as _loadAgentGuideline };
package/src/board.ts DELETED
@@ -1,263 +0,0 @@
1
- export interface BoardOptions {
2
- statuses?: string[];
3
- }
4
-
5
- import { mkdir } from "node:fs/promises";
6
- import { dirname } from "node:path";
7
- import type { Task } from "./types/index.ts";
8
-
9
- export type BoardLayout = "horizontal" | "vertical";
10
- export type BoardFormat = "terminal" | "markdown";
11
-
12
- interface DisplayTask {
13
- id: string;
14
- title: string;
15
- }
16
-
17
- export function idSegments(id: string): number[] {
18
- const normalized = id.startsWith("task-") ? id.slice(5) : id;
19
- return normalized.split(".").map((part) => Number.parseInt(part, 10));
20
- }
21
-
22
- export function compareIds(a: Task, b: Task): number {
23
- const segA = idSegments(a.id);
24
- const segB = idSegments(b.id);
25
- const len = Math.max(segA.length, segB.length);
26
- for (let i = 0; i < len; i++) {
27
- const diff = (segA[i] ?? 0) - (segB[i] ?? 0);
28
- if (diff !== 0) return diff;
29
- }
30
- return 0;
31
- }
32
-
33
- function wrapText(text: string, maxWidth: number): string[] {
34
- if (text.length <= maxWidth) return [text];
35
-
36
- const words = text.split(" ");
37
- const lines: string[] = [];
38
- let currentLine = "";
39
-
40
- for (const word of words) {
41
- if (currentLine.length + word.length + 1 <= maxWidth) {
42
- currentLine += (currentLine ? " " : "") + word;
43
- } else {
44
- if (currentLine) lines.push(currentLine);
45
- currentLine = word;
46
- }
47
- }
48
-
49
- if (currentLine) lines.push(currentLine);
50
- return lines;
51
- }
52
-
53
- export function generateKanbanBoard(
54
- tasks: Task[],
55
- statuses: string[] = [],
56
- layout: BoardLayout = "horizontal",
57
- maxColumnWidth = 20,
58
- format: BoardFormat = "terminal",
59
- ): string {
60
- const groups = new Map<string, Task[]>();
61
- for (const task of tasks) {
62
- const status = task.status || "";
63
- const list = groups.get(status) || [];
64
- list.push(task);
65
- groups.set(status, list);
66
- }
67
-
68
- // Map for quick lookup by id
69
- const byId = new Map<string, Task>(tasks.map((t) => [t.id, t]));
70
-
71
- // Only show statuses that have tasks (filter out empty groups)
72
- const ordered =
73
- tasks.length > 0
74
- ? [
75
- ...statuses.filter((s) => groups.has(s) && (groups.get(s)?.length ?? 0) > 0),
76
- ...Array.from(groups.keys()).filter((s) => !statuses.includes(s) && (groups.get(s)?.length ?? 0) > 0),
77
- ]
78
- : [];
79
-
80
- const columns: DisplayTask[][] = ordered.map((status) => {
81
- const items = groups.get(status) || [];
82
- const top: Task[] = [];
83
- const children = new Map<string, Task[]>();
84
-
85
- // Use compareIds for sorting instead of localeCompare
86
- for (const t of items.sort(compareIds)) {
87
- const parent = t.parentTaskId ? byId.get(t.parentTaskId) : undefined;
88
- if (parent && parent.status === t.status) {
89
- const list = children.get(parent.id) || [];
90
- list.push(t);
91
- children.set(parent.id, list);
92
- } else {
93
- top.push(t);
94
- }
95
- }
96
-
97
- const result: DisplayTask[] = [];
98
- for (const t of top) {
99
- result.push({ id: t.id, title: t.title });
100
- const subs = children.get(t.id) || [];
101
- subs.sort(compareIds);
102
-
103
- for (const [subIdx, s] of subs.entries()) {
104
- const isLastSub = subIdx === subs.length - 1;
105
- const prefix = isLastSub ? " └─" : " |—";
106
- result.push({ id: `${prefix} ${s.id}`, title: ` ${s.title}` });
107
- }
108
- }
109
-
110
- return result;
111
- });
112
-
113
- if (layout === "vertical") {
114
- const rows: string[] = [];
115
- for (const [idx, status] of ordered.entries()) {
116
- const header = status || "No Status";
117
- rows.push(header);
118
- rows.push("-".repeat(header.length));
119
- const tasksInStatus = columns[idx];
120
- for (const task of tasksInStatus) {
121
- rows.push(task.id);
122
- rows.push(task.title);
123
- rows.push("");
124
- }
125
- if (tasksInStatus.length === 0) {
126
- rows.push("");
127
- }
128
- }
129
- return rows.join("\n").trimEnd();
130
- }
131
-
132
- // Return empty string if no columns to show
133
- if (ordered.length === 0) {
134
- return "";
135
- }
136
-
137
- const colWidths = ordered.map((status, idx) => {
138
- const header = status || "No Status";
139
- let width = Math.min(Math.max(header.length, 8), maxColumnWidth); // Minimum 8, max maxColumnWidth
140
- for (const t of columns[idx]) {
141
- // Check both task ID and title lengths separately
142
- const idLength = t.id.length;
143
- const titleLength = t.title.length;
144
- const maxTaskWidth = Math.max(idLength, titleLength);
145
- if (maxTaskWidth > width && width < maxColumnWidth) {
146
- width = Math.min(maxTaskWidth, maxColumnWidth);
147
- }
148
- }
149
- return width;
150
- });
151
-
152
- // For markdown format, we need simpler output without text wrapping
153
- if (format === "markdown") {
154
- const headerRow = `| ${ordered.map((status) => status || "No Status").join(" | ")} |`;
155
- const separatorRow = `| ${ordered.map(() => "---").join(" | ")} |`;
156
-
157
- const maxTasks = Math.max(...columns.map((c) => c.length), 0);
158
- const rows = [headerRow, separatorRow];
159
-
160
- for (let taskIdx = 0; taskIdx < maxTasks; taskIdx++) {
161
- const row = ordered.map((_, cIdx) => {
162
- const task = columns[cIdx][taskIdx];
163
- if (!task) return "";
164
- // For markdown, combine ID and title in one cell
165
- return `${task.id}: ${task.title}`;
166
- });
167
- rows.push(`| ${row.join(" | ")} |`);
168
- }
169
-
170
- return rows.join("\n");
171
- }
172
-
173
- // Terminal format with text wrapping
174
- const pad = (text: string, width: number): string => text.padEnd(width, " ");
175
-
176
- const headerRow = ordered.map((status, i) => pad(status || "No Status", colWidths[i])).join(" | ");
177
- const separatorRow = ordered.map((_, i) => "-".repeat(colWidths[i])).join("-|-");
178
-
179
- // Prepare wrapped tasks for each column
180
- const wrappedTasks = ordered.map((_, cIdx) => {
181
- return columns[cIdx].map((task) => ({
182
- idLines: wrapText(task.id, colWidths[cIdx]),
183
- titleLines: wrapText(task.title, colWidths[cIdx]),
184
- }));
185
- });
186
-
187
- const maxTasks = Math.max(...columns.map((c) => c.length), 0);
188
- const rows = [headerRow, separatorRow];
189
-
190
- for (let taskIdx = 0; taskIdx < maxTasks; taskIdx++) {
191
- // Get the maximum number of lines needed for this task across all columns
192
- let maxTaskLines = 0;
193
- for (let cIdx = 0; cIdx < ordered.length; cIdx++) {
194
- if (wrappedTasks[cIdx][taskIdx]) {
195
- const taskLines = wrappedTasks[cIdx][taskIdx].idLines.length + wrappedTasks[cIdx][taskIdx].titleLines.length;
196
- maxTaskLines = Math.max(maxTaskLines, taskLines);
197
- }
198
- }
199
-
200
- // Render each line for this task
201
- for (let lineIdx = 0; lineIdx < maxTaskLines; lineIdx++) {
202
- const lineRow = ordered
203
- .map((_, cIdx) => {
204
- const wrappedTask = wrappedTasks[cIdx][taskIdx];
205
- if (!wrappedTask) return pad("", colWidths[cIdx]);
206
-
207
- const idLineCount = wrappedTask.idLines.length;
208
- let text = "";
209
-
210
- if (lineIdx < idLineCount) {
211
- // Show ID lines first
212
- text = wrappedTask.idLines[lineIdx];
213
- } else {
214
- // Then show title lines
215
- const titleLineIdx = lineIdx - idLineCount;
216
- if (titleLineIdx < wrappedTask.titleLines.length) {
217
- text = wrappedTask.titleLines[titleLineIdx];
218
- }
219
- }
220
-
221
- return pad(text, colWidths[cIdx]);
222
- })
223
- .join(" | ");
224
- rows.push(lineRow);
225
- }
226
-
227
- // Add empty row between tasks for better separation (except after last task)
228
- // Skip empty row if next task is a subtask (to keep parent and child together)
229
- if (taskIdx < maxTasks - 1) {
230
- const emptyRow = ordered.map((_, cIdx) => pad("", colWidths[cIdx])).join(" | ");
231
- rows.push(emptyRow);
232
- }
233
- }
234
-
235
- return rows.join("\n");
236
- }
237
-
238
- export async function exportKanbanBoardToFile(
239
- tasks: Task[],
240
- statuses: string[],
241
- filePath: string,
242
- maxColumnWidth = 20,
243
- addTitle = false,
244
- ): Promise<void> {
245
- const board = generateKanbanBoard(tasks, statuses, "horizontal", maxColumnWidth, "markdown");
246
-
247
- let existing = "";
248
- try {
249
- existing = await Bun.file(filePath).text();
250
- } catch {
251
- await mkdir(dirname(filePath), { recursive: true });
252
- }
253
-
254
- // Add proper spacing and title for readme export
255
- let boardContent = board;
256
- if (addTitle && filePath.toLowerCase().includes("readme")) {
257
- boardContent = `\n\n## Project Board\n\n${board}`;
258
- }
259
-
260
- const needsNewline = existing && !existing.endsWith("\n");
261
- const content = `${existing}${needsNewline ? "\n" : ""}${boardContent}\n`;
262
- await Bun.write(filePath, content);
263
- }