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/src/cli.ts DELETED
@@ -1,806 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
- import { stdin as input, stdout as output } from "node:process";
5
- import { createInterface } from "node:readline/promises";
6
- import { viewTaskEnhanced } from "./ui/task-viewer.ts";
7
- // Interactive TUI helpers (bblessed based)
8
- import { multiSelect, promptText, scrollableViewer, selectList } from "./ui/tui.ts";
9
-
10
- // Kanban TUI renderer
11
- import { renderBoardTui } from "./ui/board.ts";
12
-
13
- // Loading screen utilities
14
- import { createLoadingScreen, withLoadingScreen } from "./ui/loading.ts";
15
-
16
- // Remote task loading utilities
17
- import { type TaskWithMetadata, loadRemoteTasks, resolveTaskConflict } from "./core/remote-tasks.ts";
18
-
19
- import { Command } from "commander";
20
- import { DEFAULT_STATUSES, FALLBACK_STATUS } from "./constants/index.ts";
21
- import {
22
- type AgentInstructionFile,
23
- Core,
24
- addAgentInstructions,
25
- exportKanbanBoardToFile,
26
- initializeGitRepository,
27
- isGitRepository,
28
- parseTask,
29
- } from "./index.ts";
30
- import type { DecisionLog, Document as DocType, Task } from "./types/index.ts";
31
-
32
- const program = new Command();
33
- program.name("backlog").description("Backlog project management CLI");
34
-
35
- program
36
- .command("init [projectName]")
37
- .description("initialize backlog project in the current repository")
38
- .action(async (projectName?: string) => {
39
- try {
40
- const cwd = process.cwd();
41
- const isRepo = await isGitRepository(cwd);
42
-
43
- if (!isRepo) {
44
- const rl = createInterface({ input, output });
45
- const answer = (await rl.question("No git repository found. Initialize one here? [y/N] ")).trim().toLowerCase();
46
- rl.close();
47
-
48
- if (answer.startsWith("y")) {
49
- await initializeGitRepository(cwd);
50
- } else {
51
- console.log("Aborting initialization.");
52
- process.exit(1);
53
- }
54
- }
55
-
56
- let name = projectName;
57
- if (!name) {
58
- name = await promptText("Project name:");
59
- if (!name) {
60
- console.log("Aborting initialization.");
61
- process.exit(1);
62
- }
63
- }
64
-
65
- const reporter = (await promptText("Default reporter name (leave blank to skip):")) || "";
66
- let storeGlobal = false;
67
- if (reporter) {
68
- const store = (await promptText("Store reporter name globally? [y/N]", "N")).toLowerCase();
69
- storeGlobal = store.startsWith("y");
70
- }
71
-
72
- const options = [".cursorrules", "CLAUDE.md", "AGENTS.md", "readme.md"] as const;
73
- const selected = await multiSelect("Select agent instruction files to update", options as unknown as string[]);
74
- const files: AgentInstructionFile[] = (selected ?? []) as AgentInstructionFile[];
75
-
76
- const core = new Core(cwd);
77
- await core.initializeProject(name);
78
- console.log(`Initialized backlog project: ${name}`);
79
-
80
- if (files.length > 0) {
81
- await addAgentInstructions(cwd, core.gitOps, files);
82
- }
83
-
84
- if (reporter) {
85
- if (storeGlobal) {
86
- const globalPath = join(homedir(), ".backlog", "user");
87
- await mkdir(dirname(globalPath), { recursive: true });
88
- await Bun.write(globalPath, `default_reporter: "${reporter}"\n`);
89
- } else {
90
- const userPath = join(cwd, ".user");
91
- await Bun.write(userPath, `default_reporter: "${reporter}"\n`);
92
- const gitignorePath = join(cwd, ".gitignore");
93
- let gitignore = "";
94
- try {
95
- gitignore = await Bun.file(gitignorePath).text();
96
- } catch {}
97
- if (!gitignore.split(/\r?\n/).includes(".user")) {
98
- gitignore += `${gitignore.endsWith("\n") ? "" : "\n"}.user\n`;
99
- await Bun.write(gitignorePath, gitignore);
100
- }
101
- }
102
- }
103
- } catch (err) {
104
- console.error("Failed to initialize project", err);
105
- process.exitCode = 1;
106
- }
107
- });
108
-
109
- async function generateNextId(core: Core, parent?: string): Promise<string> {
110
- // Load local tasks and drafts in parallel
111
- const [tasks, drafts] = await Promise.all([core.filesystem.listTasks(), core.filesystem.listDrafts()]);
112
- const all = [...tasks, ...drafts];
113
-
114
- const remoteIds: string[] = [];
115
- try {
116
- await core.gitOps.fetch();
117
- const branches = await core.gitOps.listRemoteBranches();
118
-
119
- // Load files from all branches in parallel
120
- const branchFilePromises = branches.map(async (branch) => {
121
- const files = await core.gitOps.listFilesInRemoteBranch(branch, ".backlog/tasks");
122
- return files
123
- .map((file) => {
124
- const match = file.match(/task-([\d.]+)/);
125
- return match ? `task-${match[1]}` : null;
126
- })
127
- .filter((id): id is string => id !== null);
128
- });
129
-
130
- const branchResults = await Promise.all(branchFilePromises);
131
- for (const branchIds of branchResults) {
132
- remoteIds.push(...branchIds);
133
- }
134
- } catch {}
135
-
136
- if (parent) {
137
- const prefix = parent.startsWith("task-") ? parent : `task-${parent}`;
138
- let max = 0;
139
- for (const t of tasks) {
140
- if (t.id.startsWith(`${prefix}.`)) {
141
- const rest = t.id.slice(prefix.length + 1);
142
- const num = Number.parseInt(rest.split(".")[0] || "0", 10);
143
- if (num > max) max = num;
144
- }
145
- }
146
- for (const id of remoteIds) {
147
- if (id.startsWith(`${prefix}.`)) {
148
- const rest = id.slice(prefix.length + 1);
149
- const num = Number.parseInt(rest.split(".")[0] || "0", 10);
150
- if (num > max) max = num;
151
- }
152
- }
153
- return `${prefix}.${max + 1}`;
154
- }
155
-
156
- let max = -1;
157
- for (const t of all) {
158
- const match = t.id.match(/^task-(\d+)/);
159
- if (match) {
160
- const num = Number.parseInt(match[1], 10);
161
- if (num > max) max = num;
162
- }
163
- }
164
- for (const id of remoteIds) {
165
- const match = id.match(/^task-(\d+)/);
166
- if (match) {
167
- const num = Number.parseInt(match[1], 10);
168
- if (num > max) max = num;
169
- }
170
- }
171
- return `task-${max + 1}`;
172
- }
173
-
174
- async function generateNextDecisionId(core: Core): Promise<string> {
175
- const files = await Array.fromAsync(new Bun.Glob("decision-*.md").scan({ cwd: core.filesystem.decisionsDir }));
176
- let max = 0;
177
- for (const file of files) {
178
- const match = file.match(/^decision-(\d+)/);
179
- if (match) {
180
- const num = Number.parseInt(match[1], 10);
181
- if (num > max) max = num;
182
- }
183
- }
184
- return `decision-${max + 1}`;
185
- }
186
-
187
- async function generateNextDocId(core: Core): Promise<string> {
188
- const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.docsDir }));
189
- let max = 0;
190
- for (const file of files) {
191
- const match = file.match(/^doc-(\d+)/);
192
- if (match) {
193
- const num = Number.parseInt(match[1], 10);
194
- if (num > max) max = num;
195
- }
196
- }
197
- return `doc-${max + 1}`;
198
- }
199
-
200
- function buildTaskFromOptions(id: string, title: string, options: Record<string, unknown>): Task {
201
- const parentInput = options.parent ? String(options.parent) : undefined;
202
- const normalizedParent = parentInput
203
- ? parentInput.startsWith("task-")
204
- ? parentInput
205
- : `task-${parentInput}`
206
- : undefined;
207
-
208
- return {
209
- id,
210
- title,
211
- status: options.status || "",
212
- assignee: options.assignee ? [String(options.assignee)] : [],
213
- createdDate: new Date().toISOString().split("T")[0],
214
- labels: options.labels
215
- ? String(options.labels)
216
- .split(",")
217
- .map((l: string) => l.trim())
218
- .filter(Boolean)
219
- : [],
220
- dependencies: [],
221
- description: options.description || "",
222
- ...(normalizedParent && { parentTaskId: normalizedParent }),
223
- };
224
- }
225
-
226
- const taskCmd = program.command("task").aliases(["tasks"]);
227
-
228
- taskCmd
229
- .command("create <title>")
230
- .option("-d, --description <text>")
231
- .option("-a, --assignee <assignee>")
232
- .option("-s, --status <status>")
233
- .option("-l, --labels <labels>")
234
- .option("--draft")
235
- .option("--parent <taskId>")
236
- .action(async (title: string, options) => {
237
- const cwd = process.cwd();
238
- const core = new Core(cwd);
239
- const id = await generateNextId(core, options.parent);
240
- const task = buildTaskFromOptions(id, title, options);
241
- if (options.draft) {
242
- await core.createDraft(task, true);
243
- console.log(`Created draft ${id}`);
244
- } else {
245
- await core.createTask(task, true);
246
- console.log(`Created task ${id}`);
247
- }
248
- });
249
-
250
- taskCmd
251
- .command("list")
252
- .description("list tasks grouped by status")
253
- .option("-s, --status <status>", "filter tasks by status")
254
- .option("-a, --assignee <assignee>", "filter tasks by assignee")
255
- .option("--plain", "use plain text output instead of interactive UI")
256
- .action(async (options) => {
257
- const cwd = process.cwd();
258
- const core = new Core(cwd);
259
- const tasks = await core.filesystem.listTasks();
260
- const config = await core.filesystem.loadConfig();
261
-
262
- let filtered = tasks;
263
- if (options.status) {
264
- filtered = filtered.filter((t) => t.status === options.status);
265
- }
266
- if (options.assignee) {
267
- filtered = filtered.filter((t) => t.assignee.includes(options.assignee));
268
- }
269
-
270
- if (filtered.length === 0) {
271
- console.log("No tasks found.");
272
- return;
273
- }
274
-
275
- // Plain text output
276
- if (options.plain) {
277
- const groups = new Map<string, Task[]>();
278
- for (const task of filtered) {
279
- const status = task.status || "";
280
- const list = groups.get(status) || [];
281
- list.push(task);
282
- groups.set(status, list);
283
- }
284
-
285
- const statuses = config?.statuses || [];
286
- const ordered = [
287
- ...statuses.filter((s) => groups.has(s)),
288
- ...Array.from(groups.keys()).filter((s) => !statuses.includes(s)),
289
- ];
290
-
291
- for (const status of ordered) {
292
- const list = groups.get(status);
293
- if (!list) continue;
294
- console.log(`${status || "No Status"}:`);
295
- for (const t of list) {
296
- console.log(` ${t.id} - ${t.title}`);
297
- }
298
- console.log();
299
- }
300
- return;
301
- }
302
-
303
- // Interactive UI
304
- const selected = await selectList("Select a task", filtered, (task) => task.status || "No Status");
305
- if (selected) {
306
- // Show task details
307
- const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.tasksDir }));
308
- const taskFile = files.find((f) => f.startsWith(`${selected.id} -`));
309
- if (taskFile) {
310
- const filePath = join(core.filesystem.tasksDir, taskFile);
311
- const content = await Bun.file(filePath).text();
312
- await viewTaskEnhanced(selected, content);
313
- }
314
- }
315
- });
316
-
317
- taskCmd
318
- .command("edit <taskId>")
319
- .description("edit an existing task")
320
- .option("-t, --title <title>")
321
- .option("-d, --description <text>")
322
- .option("-a, --assignee <assignee>")
323
- .option("-s, --status <status>")
324
- .option("-l, --label <labels>")
325
- .option("--add-label <label>")
326
- .option("--remove-label <label>")
327
- .action(async (taskId: string, options) => {
328
- const cwd = process.cwd();
329
- const core = new Core(cwd);
330
- const task = await core.filesystem.loadTask(taskId);
331
-
332
- if (!task) {
333
- console.error(`Task ${taskId} not found.`);
334
- return;
335
- }
336
-
337
- if (options.title) {
338
- task.title = String(options.title);
339
- }
340
- if (options.description) {
341
- task.description = String(options.description);
342
- }
343
- if (typeof options.assignee !== "undefined") {
344
- task.assignee = [String(options.assignee)];
345
- }
346
- if (options.status) {
347
- task.status = String(options.status);
348
- }
349
-
350
- const labels = [...task.labels];
351
- if (options.label) {
352
- const newLabels = String(options.label)
353
- .split(",")
354
- .map((l: string) => l.trim())
355
- .filter(Boolean);
356
- labels.splice(0, labels.length, ...newLabels);
357
- }
358
- if (options.addLabel) {
359
- const adds = Array.isArray(options.addLabel) ? options.addLabel : [options.addLabel];
360
- for (const l of adds) {
361
- const trimmed = String(l).trim();
362
- if (trimmed && !labels.includes(trimmed)) labels.push(trimmed);
363
- }
364
- }
365
- if (options.removeLabel) {
366
- const removes = Array.isArray(options.removeLabel) ? options.removeLabel : [options.removeLabel];
367
- for (const l of removes) {
368
- const trimmed = String(l).trim();
369
- const idx = labels.indexOf(trimmed);
370
- if (idx !== -1) labels.splice(idx, 1);
371
- }
372
- }
373
- task.labels = labels;
374
- task.updatedDate = new Date().toISOString().split("T")[0];
375
-
376
- await core.updateTask(task, true);
377
- console.log(`Updated task ${task.id}`);
378
- });
379
-
380
- taskCmd
381
- .command("view <taskId>")
382
- .description("display task details")
383
- .action(async (taskId: string) => {
384
- const cwd = process.cwd();
385
- const core = new Core(cwd);
386
- const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.tasksDir }));
387
- const normalizedId = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
388
- const taskFile = files.find((f) => f.startsWith(`${normalizedId} -`));
389
-
390
- if (!taskFile) {
391
- console.error(`Task ${taskId} not found.`);
392
- return;
393
- }
394
-
395
- const filePath = join(core.filesystem.tasksDir, taskFile);
396
- const content = await Bun.file(filePath).text();
397
- const task = await core.filesystem.loadTask(taskId);
398
-
399
- if (!task) {
400
- console.error(`Task ${taskId} not found.`);
401
- return;
402
- }
403
-
404
- // Use enhanced task viewer
405
- await viewTaskEnhanced(task, content);
406
- });
407
-
408
- taskCmd
409
- .command("archive <taskId>")
410
- .description("archive a task")
411
- .action(async (taskId: string) => {
412
- const cwd = process.cwd();
413
- const core = new Core(cwd);
414
- const success = await core.archiveTask(taskId, true);
415
- if (success) {
416
- console.log(`Archived task ${taskId}`);
417
- } else {
418
- console.error(`Task ${taskId} not found.`);
419
- }
420
- });
421
-
422
- taskCmd
423
- .command("demote <taskId>")
424
- .description("move task back to drafts")
425
- .action(async (taskId: string) => {
426
- const cwd = process.cwd();
427
- const core = new Core(cwd);
428
- const success = await core.demoteTask(taskId, true);
429
- if (success) {
430
- console.log(`Demoted task ${taskId}`);
431
- } else {
432
- console.error(`Task ${taskId} not found.`);
433
- }
434
- });
435
-
436
- taskCmd.argument("[taskId]").action(async (taskId: string | undefined) => {
437
- if (!taskId) {
438
- taskCmd.help();
439
- return;
440
- }
441
- // Use the view command directly
442
- const cwd = process.cwd();
443
- const core = new Core(cwd);
444
- const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.tasksDir }));
445
- const normalizedId = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
446
- const taskFile = files.find((f) => f.startsWith(`${normalizedId} -`));
447
-
448
- if (!taskFile) {
449
- console.error(`Task ${taskId} not found.`);
450
- return;
451
- }
452
-
453
- const filePath = join(core.filesystem.tasksDir, taskFile);
454
- const content = await Bun.file(filePath).text();
455
- await scrollableViewer(content);
456
- });
457
-
458
- const draftCmd = program.command("draft");
459
-
460
- draftCmd
461
- .command("create <title>")
462
- .option("-d, --description <text>")
463
- .option("-a, --assignee <assignee>")
464
- .option("-s, --status <status>")
465
- .option("-l, --labels <labels>")
466
- .action(async (title: string, options) => {
467
- const cwd = process.cwd();
468
- const core = new Core(cwd);
469
- const id = await generateNextId(core);
470
- const task = buildTaskFromOptions(id, title, options);
471
- await core.createDraft(task, true);
472
- console.log(`Created draft ${id}`);
473
- });
474
-
475
- draftCmd
476
- .command("archive <taskId>")
477
- .description("archive a draft")
478
- .action(async (taskId: string) => {
479
- const cwd = process.cwd();
480
- const core = new Core(cwd);
481
- const success = await core.archiveDraft(taskId, true);
482
- if (success) {
483
- console.log(`Archived draft ${taskId}`);
484
- } else {
485
- console.error(`Draft ${taskId} not found.`);
486
- }
487
- });
488
-
489
- draftCmd
490
- .command("promote <taskId>")
491
- .description("promote draft to task")
492
- .action(async (taskId: string) => {
493
- const cwd = process.cwd();
494
- const core = new Core(cwd);
495
- const success = await core.promoteDraft(taskId, true);
496
- if (success) {
497
- console.log(`Promoted draft ${taskId}`);
498
- } else {
499
- console.error(`Draft ${taskId} not found.`);
500
- }
501
- });
502
-
503
- const boardCmd = program.command("board");
504
-
505
- function addBoardOptions(cmd: Command) {
506
- return cmd
507
- .option("-l, --layout <layout>", "board layout (horizontal|vertical)", "horizontal")
508
- .option("--vertical", "use vertical layout (shortcut for --layout vertical)");
509
- }
510
-
511
- // TaskWithMetadata and resolveTaskConflict are now imported from remote-tasks.ts
512
-
513
- async function handleBoardView(options: { layout?: string; vertical?: boolean }) {
514
- const cwd = process.cwd();
515
- const core = new Core(cwd);
516
- const config = await core.filesystem.loadConfig();
517
- const statuses = config?.statuses || [];
518
- const resolutionStrategy = config?.taskResolutionStrategy || "most_progressed";
519
-
520
- // Load tasks with loading screen
521
- const allTasks = await withLoadingScreen("Loading tasks from local and remote branches", async () => {
522
- // Load local tasks
523
- const localTasks = await core.filesystem.listTasksWithMetadata();
524
- const tasksById = new Map<string, TaskWithMetadata>(
525
- localTasks.map((t) => [t.id, { ...t, source: "local" } as TaskWithMetadata]),
526
- );
527
-
528
- // Load remote tasks in parallel
529
- const remoteTasks = await loadRemoteTasks(core.gitOps);
530
-
531
- // Merge remote tasks with local tasks
532
- for (const remoteTask of remoteTasks) {
533
- const existing = tasksById.get(remoteTask.id);
534
- if (!existing) {
535
- tasksById.set(remoteTask.id, remoteTask);
536
- } else {
537
- const resolved = resolveTaskConflict(existing, remoteTask, statuses, resolutionStrategy);
538
- tasksById.set(remoteTask.id, resolved);
539
- }
540
- }
541
-
542
- return Array.from(tasksById.values());
543
- });
544
-
545
- if (allTasks.length === 0) {
546
- console.log("No tasks found.");
547
- return;
548
- }
549
-
550
- const layout = options.vertical ? "vertical" : (options.layout as "horizontal" | "vertical") || "horizontal";
551
- const maxColumnWidth = config?.maxColumnWidth || 20; // Default for terminal display
552
- // Always use renderBoardTui which falls back to plain text if blessed is not available
553
- await renderBoardTui(allTasks, statuses, layout, maxColumnWidth);
554
- }
555
-
556
- addBoardOptions(boardCmd).description("display tasks in a Kanban board").action(handleBoardView);
557
-
558
- addBoardOptions(boardCmd.command("view").description("display tasks in a Kanban board")).action(handleBoardView);
559
-
560
- boardCmd
561
- .command("export [filename]")
562
- .description("append kanban board to readme or output file")
563
- .option("-o, --output <path>", "output file (deprecated, use filename argument instead)")
564
- .action(async (filename, options) => {
565
- const cwd = process.cwd();
566
- const core = new Core(cwd);
567
- const config = await core.filesystem.loadConfig();
568
- const statuses = config?.statuses || [];
569
- const resolutionStrategy = config?.taskResolutionStrategy || "most_progressed";
570
-
571
- // Load tasks with progress tracking
572
- const loadingScreen = await createLoadingScreen("Loading tasks for export");
573
-
574
- try {
575
- // Load local tasks
576
- loadingScreen?.update("Loading local tasks...");
577
- const localTasks = await core.filesystem.listTasksWithMetadata();
578
- const tasksById = new Map<string, TaskWithMetadata>(
579
- localTasks.map((t) => [t.id, { ...t, source: "local" } as TaskWithMetadata]),
580
- );
581
- loadingScreen?.update(`Found ${localTasks.length} local tasks`);
582
-
583
- // Load remote tasks in parallel
584
- loadingScreen?.update("Loading remote tasks...");
585
- const remoteTasks = await loadRemoteTasks(core.gitOps, (msg) => loadingScreen?.update(msg));
586
-
587
- // Merge remote tasks with local tasks
588
- loadingScreen?.update("Merging tasks...");
589
- for (const remoteTask of remoteTasks) {
590
- const existing = tasksById.get(remoteTask.id);
591
- if (!existing) {
592
- tasksById.set(remoteTask.id, remoteTask);
593
- } else {
594
- const resolved = resolveTaskConflict(existing, remoteTask, statuses, resolutionStrategy);
595
- tasksById.set(remoteTask.id, resolved);
596
- }
597
- }
598
-
599
- const allTasks = Array.from(tasksById.values());
600
- loadingScreen?.update(`Total tasks: ${allTasks.length}`);
601
-
602
- // Close loading screen before export
603
- loadingScreen?.close();
604
-
605
- // Priority: filename argument > --output option > default readme.md
606
- const outputFile = filename || options.output || "readme.md";
607
- const outputPath = join(cwd, outputFile as string);
608
- const maxColumnWidth = config?.maxColumnWidth || 30; // Default for export
609
- const addTitle = !filename && !options.output; // Add title only for default readme export
610
- await exportKanbanBoardToFile(allTasks, statuses, outputPath, maxColumnWidth, addTitle);
611
- console.log(`Exported board to ${outputPath}`);
612
- } catch (error) {
613
- loadingScreen?.close();
614
- throw error;
615
- }
616
- });
617
-
618
- const docCmd = program.command("doc");
619
-
620
- docCmd
621
- .command("create <title>")
622
- .option("-p, --path <path>")
623
- .option("-t, --type <type>")
624
- .action(async (title: string, options) => {
625
- const cwd = process.cwd();
626
- const core = new Core(cwd);
627
- const id = await generateNextDocId(core);
628
- const document: DocType = {
629
- id,
630
- title,
631
- type: (options.type || "other") as DocType["type"],
632
- createdDate: new Date().toISOString().split("T")[0],
633
- content: "",
634
- };
635
- await core.createDocument(document, true, options.path || "");
636
- console.log(`Created document ${id}`);
637
- });
638
-
639
- docCmd
640
- .command("list")
641
- .option("--plain", "use plain text output instead of interactive UI")
642
- .action(async (options) => {
643
- const cwd = process.cwd();
644
- const core = new Core(cwd);
645
- const docs = await core.filesystem.listDocuments();
646
- if (docs.length === 0) {
647
- console.log("No docs found.");
648
- return;
649
- }
650
-
651
- // Plain text output
652
- if (options.plain) {
653
- for (const d of docs) {
654
- console.log(`${d.id} - ${d.title}`);
655
- }
656
- return;
657
- }
658
-
659
- // Interactive UI
660
- const selected = await selectList("Select a document", docs);
661
- if (selected) {
662
- // Show document details
663
- const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.docsDir }));
664
- const docFile = files.find((f) => f.startsWith(`${selected.id} -`) || f === `${selected.id}.md`);
665
- if (docFile) {
666
- const filePath = join(core.filesystem.docsDir, docFile);
667
- const content = await Bun.file(filePath).text();
668
- await scrollableViewer(content);
669
- }
670
- }
671
- });
672
-
673
- // Document view command
674
- docCmd
675
- .command("view <docId>")
676
- .description("view a document")
677
- .action(async (docId: string) => {
678
- const cwd = process.cwd();
679
- const core = new Core(cwd);
680
- const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.docsDir }));
681
- const normalizedId = docId.startsWith("doc-") ? docId : `doc-${docId}`;
682
- const docFile = files.find((f) => f.startsWith(`${normalizedId} -`) || f === `${normalizedId}.md`);
683
-
684
- if (!docFile) {
685
- console.error(`Document ${docId} not found.`);
686
- return;
687
- }
688
-
689
- const filePath = join(core.filesystem.docsDir, docFile);
690
- const content = await Bun.file(filePath).text();
691
-
692
- // Use scrollableViewer which falls back to console.log if blessed is not available
693
- await scrollableViewer(content);
694
- });
695
-
696
- const decisionCmd = program.command("decision");
697
-
698
- decisionCmd
699
- .command("create <title>")
700
- .option("-s, --status <status>")
701
- .action(async (title: string, options) => {
702
- const cwd = process.cwd();
703
- const core = new Core(cwd);
704
- const id = await generateNextDecisionId(core);
705
- const decision: DecisionLog = {
706
- id,
707
- title,
708
- date: new Date().toISOString().split("T")[0],
709
- status: (options.status || "proposed") as DecisionLog["status"],
710
- context: "",
711
- decision: "",
712
- consequences: "",
713
- };
714
- await core.createDecisionLog(decision, true);
715
- console.log(`Created decision ${id}`);
716
- });
717
-
718
- decisionCmd
719
- .command("list")
720
- .option("--plain", "use plain text output instead of interactive UI")
721
- .action(async (options) => {
722
- const cwd = process.cwd();
723
- const core = new Core(cwd);
724
- const decisions = await core.filesystem.listDecisionLogs();
725
- if (decisions.length === 0) {
726
- console.log("No decisions found.");
727
- return;
728
- }
729
-
730
- // Plain text output
731
- if (options.plain) {
732
- for (const d of decisions) {
733
- console.log(`${d.id} - ${d.title}`);
734
- }
735
- return;
736
- }
737
-
738
- // Interactive UI
739
- const selected = await selectList("Select a decision", decisions);
740
- if (selected) {
741
- // Show decision details
742
- const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.decisionsDir }));
743
- const decisionFile = files.find((f) => f.startsWith(`${selected.id} -`) || f === `${selected.id}.md`);
744
- if (decisionFile) {
745
- const filePath = join(core.filesystem.decisionsDir, decisionFile);
746
- const content = await Bun.file(filePath).text();
747
- await scrollableViewer(content);
748
- }
749
- }
750
- });
751
-
752
- const configCmd = program.command("config");
753
-
754
- configCmd
755
- .command("get <key>")
756
- .description("get configuration value")
757
- .action(async (key: string) => {
758
- const cwd = process.cwd();
759
- const core = new Core(cwd);
760
- const localCfg = await core.filesystem.loadConfig();
761
- const localVal = localCfg ? (localCfg as unknown as Record<string, unknown>)[key] : undefined;
762
- if (localVal !== undefined) {
763
- console.log(localVal);
764
- return;
765
- }
766
- const globalVal = await core.filesystem.getUserSetting(key, true);
767
- if (globalVal !== undefined) {
768
- console.log(globalVal);
769
- return;
770
- }
771
- const defaults: Record<string, unknown> = {
772
- statuses: DEFAULT_STATUSES,
773
- defaultStatus: FALLBACK_STATUS,
774
- };
775
- if (key in defaults) {
776
- console.log(defaults[key]);
777
- }
778
- });
779
-
780
- configCmd
781
- .command("set <key> <value>")
782
- .description("set configuration value")
783
- .option("--global", "save to global user config")
784
- .option("--local", "save to local project config")
785
- .action(async (key: string, value: string, options) => {
786
- const cwd = process.cwd();
787
- const core = new Core(cwd);
788
- if (options.global) {
789
- await core.filesystem.setUserSetting(key, value, true);
790
- console.log(`Set ${key} in global config`);
791
- } else {
792
- const cfg = (await core.filesystem.loadConfig()) || {
793
- projectName: "",
794
- statuses: [...DEFAULT_STATUSES],
795
- labels: [],
796
- milestones: [],
797
- defaultStatus: FALLBACK_STATUS,
798
- dateFormat: "YYYY-MM-DD",
799
- };
800
- (cfg as unknown as Record<string, unknown>)[key] = value;
801
- await core.filesystem.saveConfig(cfg);
802
- console.log(`Set ${key} in local config`);
803
- }
804
- });
805
-
806
- program.parseAsync(process.argv);