backlog.md 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/.backlog/archive/drafts/readme.md +3 -0
  2. package/.backlog/archive/drafts/task-41 - temporary-test-task.md +13 -0
  3. package/.backlog/archive/readme.md +6 -0
  4. package/.backlog/archive/tasks/readme.md +3 -0
  5. package/.backlog/archive/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +14 -0
  6. package/.backlog/config.yml +7 -0
  7. package/.backlog/decisions/readme.md +7 -0
  8. package/.backlog/docs/readme.md +20 -0
  9. package/.backlog/drafts/readme.md +3 -0
  10. package/.backlog/drafts/task-26 - docs-add-board-export-step-to-agent-dod.md +21 -0
  11. package/.backlog/drafts/task-28 - add-code-of-conduct.md +20 -0
  12. package/.backlog/drafts/task-30 - create-changelog.md +19 -0
  13. package/.backlog/milestones/m-0 - project-setup.md +8 -0
  14. package/.backlog/milestones/m-1 - cli.md +8 -0
  15. package/.backlog/milestones/m-2 - cli-kanban.md +8 -0
  16. package/.backlog/milestones/m-3 - gui.md +8 -0
  17. package/.backlog/milestones/m-4 - gui-kanban.md +8 -0
  18. package/.backlog/milestones/m-5 - gui-advanced.md +12 -0
  19. package/.backlog/milestones/readme.md +3 -0
  20. package/.backlog/readme.md +5 -0
  21. package/.backlog/tasks/readme.md +37 -0
  22. package/.backlog/tasks/task-1 - cli-setup-core-project.md +23 -0
  23. package/.backlog/tasks/task-10 - gui-init-packaging.md +23 -0
  24. package/.backlog/tasks/task-11 - gui-kanban-board.md +26 -0
  25. package/.backlog/tasks/task-12 - gui-advanced.md +25 -0
  26. package/.backlog/tasks/task-13 - cli-add-agent-instruction-prompt.md +53 -0
  27. package/.backlog/tasks/task-13.1 - cli-agent-instruction-file-selection.md +40 -0
  28. package/.backlog/tasks/task-14 - gui-introduction-screens.md +21 -0
  29. package/.backlog/tasks/task-15 - improve-tasks-readme-with-generic-example-and-cli-reference.md +20 -0
  30. package/.backlog/tasks/task-16 - improve-docs-readme-with-generic-example-and-cli-reference.md +20 -0
  31. package/.backlog/tasks/task-17 - improve-drafts-readme-with-generic-example-and-cli-reference.md +20 -0
  32. package/.backlog/tasks/task-18 - improve-decisions-readme-with-generic-example-and-cli-reference.md +20 -0
  33. package/.backlog/tasks/task-19 - cli-fix-default-task-status-and-remove-draft-from-statuses.md +55 -0
  34. package/.backlog/tasks/task-2 - cli-core-logic-library.md +28 -0
  35. package/.backlog/tasks/task-20 - add-agent-guideline-to-mark-tasks-in-progress-on-start.md +32 -0
  36. package/.backlog/tasks/task-21 - kanban-board-vertical-layout.md +31 -0
  37. package/.backlog/tasks/task-22 - cli-prevent-double-dash-in-task-filenames.md +24 -0
  38. package/.backlog/tasks/task-23 - cli-kanban-board-order-tasks-by-id-asc.md +30 -0
  39. package/.backlog/tasks/task-24 - handle-subtasks-in-the-kanban-view.md +38 -0
  40. package/.backlog/tasks/task-24.1 - cli-kanban-board-milestone-view.md +19 -0
  41. package/.backlog/tasks/task-25 - cli-export-kanban-board-to-readme.md +28 -0
  42. package/.backlog/tasks/task-27 - add-contributing-guidelines.md +27 -0
  43. package/.backlog/tasks/task-29 - add-github-templates.md +28 -0
  44. package/.backlog/tasks/task-3 - cli-implement-backlog-init.md +63 -0
  45. package/.backlog/tasks/task-31 - update-readme-for-open-source.md +26 -0
  46. package/.backlog/tasks/task-32 - cli-hide-empty-'no-status'-column.md +31 -0
  47. package/.backlog/tasks/task-33 - cli-export-milestones-board-as-roadmap.md +20 -0
  48. package/.backlog/tasks/task-34 - split-readme.md-for-users-and-contributors.md +26 -0
  49. package/.backlog/tasks/task-35 - finalize-package.json-metadata-for-publishing.md +24 -0
  50. package/.backlog/tasks/task-36 - cli-prompt-for-project-name-in-init.md +24 -0
  51. package/.backlog/tasks/task-37 - cli-board-view-open-tasks-in-ide.md +19 -0
  52. package/.backlog/tasks/task-38 - cli-improved-agent-selection-for-init.md +25 -0
  53. package/.backlog/tasks/task-39 - cli-fix-empty-agent-instruction-files-on-init.md +31 -0
  54. package/.backlog/tasks/task-4 - cli-task-management-commands.md +28 -0
  55. package/.backlog/tasks/task-4.1 - cli-task-create.md +27 -0
  56. package/.backlog/tasks/task-4.10 - use-cli-to-mark-tasks-done.md +51 -0
  57. package/.backlog/tasks/task-4.11 - docs-add-definition-of-done-to-agent-guidelines.md +23 -0
  58. package/.backlog/tasks/task-4.12 - cli-handle-task-id-conflicts-across-branches.md +53 -0
  59. package/.backlog/tasks/task-4.13 - cli-fix-config-command-local-global-logic.md +58 -0
  60. package/.backlog/tasks/task-4.2 - cli-task-list-view.md +25 -0
  61. package/.backlog/tasks/task-4.3 - cli-task-edit.md +24 -0
  62. package/.backlog/tasks/task-4.4 - cli-task-archive-transition.md +27 -0
  63. package/.backlog/tasks/task-4.5 - cli-init-prompts-for-reporter-name-and-global-local-config.md +28 -0
  64. package/.backlog/tasks/task-4.6 - cli-add-empty-assignee-array-field-for-new-tasks.md +35 -0
  65. package/.backlog/tasks/task-4.7 - cli-parse-unquoted-created_date.md +40 -0
  66. package/.backlog/tasks/task-4.8 - cli-enforce-description-header.md +48 -0
  67. package/.backlog/tasks/task-4.9 - cli-normalize-task-id-inputs.md +66 -0
  68. package/.backlog/tasks/task-40 - cli-board-command-defaults-to-view.md +38 -0
  69. package/.backlog/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +93 -0
  70. package/.backlog/tasks/task-41.1 - cli-bblessed-init-wizard.md +42 -0
  71. package/.backlog/tasks/task-41.2 - cli-bblessed-task-view.md +44 -0
  72. package/.backlog/tasks/task-41.3 - cli-bblessed-doc-view.md +45 -0
  73. package/.backlog/tasks/task-41.4 - cli-bblessed-board-view.md +49 -0
  74. package/.backlog/tasks/task-41.5 - cli-audit-remaining-ui-for-bblessed.md +55 -0
  75. package/.backlog/tasks/task-42 - visual-hierarchy.md +54 -0
  76. package/.backlog/tasks/task-43 - remove-duplicate-acceptance-criteria-and-style-metadata.md +56 -0
  77. package/.backlog/tasks/task-44 - checklist-alignment.md +24 -0
  78. package/.backlog/tasks/task-45 - safe-line-wrapping.md +23 -0
  79. package/.backlog/tasks/task-46 - split-pane-layout.md +24 -0
  80. package/.backlog/tasks/task-47 - sticky-header-in-detail-view.md +43 -0
  81. package/.backlog/tasks/task-48 - footer-hint-line.md +21 -0
  82. package/.backlog/tasks/task-49 - status-styling.md +53 -0
  83. package/.backlog/tasks/task-5 - cli-docs-decisions.md +57 -0
  84. package/.backlog/tasks/task-50 - borders-&-padding.md +22 -0
  85. package/.backlog/tasks/task-51 - code-path-styling.md +23 -0
  86. package/.backlog/tasks/task-52 - cli-filter-tasks-list-by-status-or-assignee.md +29 -0
  87. package/.backlog/tasks/task-6 - cli-packaging.md +65 -0
  88. package/.backlog/tasks/task-6.1 - cli-local-installation-support-for-bunx-npx.md +49 -0
  89. package/.backlog/tasks/task-6.2 - cli-github-actions-for-build-&-publish.md +64 -0
  90. package/.backlog/tasks/task-7 - cli-kanban-view.md +60 -0
  91. package/.backlog/tasks/task-7.1 - cli-kanban-board-detect-remote-task-status.md +62 -0
  92. package/.backlog/tasks/task-8 - gui-project-setup.md +21 -0
  93. package/.backlog/tasks/task-9 - gui-task-crud.md +24 -0
  94. package/.cursorrules +223 -0
  95. package/.gitattributes +2 -0
  96. package/.github/ISSUE_TEMPLATE/bug_report.md +25 -0
  97. package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
  98. package/.github/PULL_REQUEST_TEMPLATE.md +8 -0
  99. package/.github/workflows/ci.yml +36 -0
  100. package/.husky/pre-commit +1 -0
  101. package/AGENTS.md +65 -0
  102. package/CLAUDE.md +87 -0
  103. package/CONTRIBUTING.md +19 -0
  104. package/DEVELOPMENT.md +37 -0
  105. package/LICENSE +21 -0
  106. package/biome.json +31 -0
  107. package/bun.lock +152 -0
  108. package/cli/.cursorrules-xh86jabm.md +82 -0
  109. package/cli/AGENTS-xh86jabm.md +82 -0
  110. package/cli/CLAUDE-xh86jabm.md +82 -0
  111. package/cli/backlog +0 -0
  112. package/cli/cli.js +19622 -0
  113. package/cli/index.js +2 -0
  114. package/docs/npm-publishing.md +69 -0
  115. package/package.json +47 -0
  116. package/readme.md +97 -0
  117. package/scripts/build.js +73 -0
  118. package/src/agent-instructions.ts +54 -0
  119. package/src/board.ts +263 -0
  120. package/src/cli.ts +806 -0
  121. package/src/constants/index.ts +48 -0
  122. package/src/core/backlog.ts +183 -0
  123. package/src/core/remote-tasks.ts +168 -0
  124. package/src/file-system/operations.ts +515 -0
  125. package/src/git/operations.ts +189 -0
  126. package/src/guidelines/.cursorrules.md +82 -0
  127. package/src/guidelines/AGENTS.md +82 -0
  128. package/src/guidelines/CLAUDE.md +82 -0
  129. package/src/guidelines/index.ts +7 -0
  130. package/src/index.ts +30 -0
  131. package/src/markdown/parser.ts +145 -0
  132. package/src/markdown/serializer.ts +71 -0
  133. package/src/test/agent-instructions.test.ts +62 -0
  134. package/src/test/board.test.ts +291 -0
  135. package/src/test/build.test.ts +28 -0
  136. package/src/test/checklist.test.ts +273 -0
  137. package/src/test/cli.test.ts +1300 -0
  138. package/src/test/code-path.test.ts +204 -0
  139. package/src/test/core.test.ts +330 -0
  140. package/src/test/filesystem.test.ts +435 -0
  141. package/src/test/git.test.ts +26 -0
  142. package/src/test/heading.test.ts +102 -0
  143. package/src/test/line-wrapping.test.ts +252 -0
  144. package/src/test/local-install.test.ts +34 -0
  145. package/src/test/markdown.test.ts +526 -0
  146. package/src/test/parallel-loading.test.ts +160 -0
  147. package/src/test/parent-id-normalization.test.ts +48 -0
  148. package/src/test/remote-id-conflict.test.ts +60 -0
  149. package/src/test/status-icon.test.ts +93 -0
  150. package/src/types/blessed.d.ts +14 -0
  151. package/src/types/index.ts +55 -0
  152. package/src/types/raw.d.ts +4 -0
  153. package/src/ui/board.ts +322 -0
  154. package/src/ui/checklist.ts +103 -0
  155. package/src/ui/code-path.ts +113 -0
  156. package/src/ui/heading.ts +121 -0
  157. package/src/ui/loading.ts +216 -0
  158. package/src/ui/status-icon.ts +53 -0
  159. package/src/ui/task-list.ts +168 -0
  160. package/src/ui/task-viewer.ts +640 -0
  161. package/src/ui/tui.ts +301 -0
  162. package/tsconfig.json +26 -0
@@ -0,0 +1,103 @@
1
+ /* Checklist alignment utilities for consistent checkbox display */
2
+
3
+ export interface ChecklistItem {
4
+ text: string;
5
+ checked: boolean;
6
+ }
7
+
8
+ /**
9
+ * Regex patterns for detecting checkbox markdown
10
+ */
11
+ export const CHECKBOX_PATTERNS = {
12
+ // Matches "- [ ] text" or "- [x] text" with optional leading whitespace
13
+ CHECKBOX_LINE: /^\s*-\s*\[([ x])\]\s*(.*)$/,
14
+ // Matches just the checkbox part
15
+ CHECKBOX_PREFIX: /^-\s*\[([ x])\]\s*/,
16
+ } as const;
17
+
18
+ /**
19
+ * Parse a line to extract checkbox state and text
20
+ */
21
+ export function parseCheckboxLine(line: string): ChecklistItem | null {
22
+ const match = line.match(CHECKBOX_PATTERNS.CHECKBOX_LINE);
23
+ if (!match) return null;
24
+
25
+ const [, checkState, text] = match;
26
+ return {
27
+ text: text.trim(),
28
+ checked: checkState === "x",
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Format a checklist item with aligned checkbox display
34
+ */
35
+ export function formatChecklistItem(
36
+ item: ChecklistItem,
37
+ options: {
38
+ padding?: string;
39
+ checkedSymbol?: string;
40
+ uncheckedSymbol?: string;
41
+ } = {},
42
+ ): string {
43
+ const { padding = " ", checkedSymbol = "[x]", uncheckedSymbol = "[ ]" } = options;
44
+
45
+ const checkbox = item.checked ? checkedSymbol : uncheckedSymbol;
46
+ return `${padding}${checkbox} ${item.text}`;
47
+ }
48
+
49
+ /**
50
+ * Process acceptance criteria section and align checkboxes
51
+ */
52
+ export function alignAcceptanceCriteria(criteriaSection: string): string[] {
53
+ if (!criteriaSection) return [];
54
+
55
+ return criteriaSection
56
+ .split("\n")
57
+ .map((line) => line.trim())
58
+ .filter((line) => line.length > 0)
59
+ .map((line) => {
60
+ const item = parseCheckboxLine(line);
61
+ if (item) {
62
+ return formatChecklistItem(item);
63
+ }
64
+ // Return non-checkbox lines as-is with minimal padding
65
+ return ` ${line}`;
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Extract and format acceptance criteria from markdown content
71
+ */
72
+ export function extractAndFormatAcceptanceCriteria(content: string): string[] {
73
+ const criteriaSection = extractSection(content, "Acceptance Criteria");
74
+ if (!criteriaSection) return [];
75
+
76
+ return alignAcceptanceCriteria(criteriaSection);
77
+ }
78
+
79
+ /**
80
+ * Extract a section from markdown content
81
+ */
82
+ function extractSection(content: string, sectionTitle: string): string | undefined {
83
+ const regex = new RegExp(`## ${sectionTitle}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`, "i");
84
+ const match = content.match(regex);
85
+ return match?.[1]?.trim();
86
+ }
87
+
88
+ /**
89
+ * Format multiple checklist items with consistent alignment
90
+ */
91
+ export function formatChecklist(items: ChecklistItem[]): string[] {
92
+ return items.map((item) => formatChecklistItem(item));
93
+ }
94
+
95
+ /**
96
+ * Parse multiple checkbox lines from text
97
+ */
98
+ export function parseCheckboxLines(text: string): ChecklistItem[] {
99
+ return text
100
+ .split("\n")
101
+ .map((line) => parseCheckboxLine(line))
102
+ .filter((item): item is ChecklistItem => item !== null);
103
+ }
@@ -0,0 +1,113 @@
1
+ /* Code path detection and styling utilities */
2
+
3
+ /**
4
+ * Regex patterns for detecting code paths in backticks
5
+ */
6
+ export const CODE_PATH_PATTERNS = {
7
+ // Matches `src/cli.ts`, `package.json`, `/full/path/file.ts`
8
+ BACKTICKED_PATH: /`([^`]+)`/g,
9
+ // Matches file extensions
10
+ FILE_EXTENSION: /\.[a-zA-Z0-9]+$/,
11
+ // Matches path separators
12
+ PATH_SEPARATOR: /[\/\\]/,
13
+ } as const;
14
+
15
+ /**
16
+ * Detect if a backticked string is likely a file path
17
+ */
18
+ export function isCodePath(content: string): boolean {
19
+ // Has file extension OR contains path separator
20
+ return CODE_PATH_PATTERNS.FILE_EXTENSION.test(content) || CODE_PATH_PATTERNS.PATH_SEPARATOR.test(content);
21
+ }
22
+
23
+ /**
24
+ * Extract all code paths from text
25
+ */
26
+ export function extractCodePaths(text: string): string[] {
27
+ const matches = text.match(CODE_PATH_PATTERNS.BACKTICKED_PATH);
28
+ if (!matches) return [];
29
+
30
+ return matches
31
+ .map((match) => match.slice(1, -1)) // Remove backticks
32
+ .filter(isCodePath);
33
+ }
34
+
35
+ /**
36
+ * Style a code path for blessed display
37
+ */
38
+ export function styleCodePath(path: string): string {
39
+ return `{gray-fg}\`${path}\`{/gray-fg}`;
40
+ }
41
+
42
+ /**
43
+ * Transform text to style code paths and place them on separate lines
44
+ */
45
+ export function transformCodePaths(text: string): string {
46
+ if (!text) return text;
47
+
48
+ // Split into lines to preserve existing line breaks
49
+ const lines = text.split("\n");
50
+ const result: string[] = [];
51
+
52
+ for (const line of lines) {
53
+ let transformedLine = line;
54
+ const codePaths = extractCodePaths(line);
55
+
56
+ if (codePaths.length === 0) {
57
+ // No code paths, add line as-is
58
+ result.push(transformedLine);
59
+ continue;
60
+ }
61
+
62
+ // Check if line contains only a code path (possibly with minimal surrounding text)
63
+ const lineWithoutPaths = line.replace(/`[^`]+`/g, "").trim();
64
+ const isIsolatedPath = codePaths.length === 1 && lineWithoutPaths.length < 10;
65
+
66
+ if (isIsolatedPath) {
67
+ // Style the code path in place
68
+ for (const path of codePaths) {
69
+ transformedLine = transformedLine.replace(`\`${path}\``, styleCodePath(path));
70
+ }
71
+ result.push(transformedLine);
72
+ } else {
73
+ // Extract code paths to separate lines
74
+ let workingLine = transformedLine;
75
+ const pathsToExtract: string[] = [];
76
+
77
+ for (const path of codePaths) {
78
+ const backticked = `\`${path}\``;
79
+ if (workingLine.includes(backticked)) {
80
+ // Remove from line and collect for separate placement, clean up extra spaces
81
+ workingLine = workingLine.replace(backticked, " ").replace(/\s+/g, " ").trim();
82
+ pathsToExtract.push(path);
83
+ }
84
+ }
85
+
86
+ // Add the line without code paths (if not empty)
87
+ if (workingLine.length > 0) {
88
+ result.push(workingLine);
89
+ }
90
+
91
+ // Add each code path on its own line
92
+ for (const path of pathsToExtract) {
93
+ result.push(styleCodePath(path));
94
+ }
95
+ }
96
+ }
97
+
98
+ return result.join("\n");
99
+ }
100
+
101
+ /**
102
+ * Simple styling for plain text (without blessed tags)
103
+ */
104
+ export function transformCodePathsPlain(text: string): string {
105
+ if (!text) return text;
106
+
107
+ return text.replace(CODE_PATH_PATTERNS.BACKTICKED_PATH, (match, path) => {
108
+ if (isCodePath(path)) {
109
+ return `\`${path}\``;
110
+ }
111
+ return match;
112
+ });
113
+ }
@@ -0,0 +1,121 @@
1
+ /* Heading helper component for consistent terminal UI styling */
2
+
3
+ import { createRequire } from "node:module";
4
+
5
+ // Load blessed dynamically
6
+ // biome-ignore lint/suspicious/noExplicitAny: blessed is dynamically loaded
7
+ async function loadBlessed(): Promise<any | null> {
8
+ try {
9
+ const require = createRequire(import.meta.url);
10
+ const blessed = require("blessed");
11
+ return blessed;
12
+ } catch {
13
+ try {
14
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic import
15
+ const mod = (await import("blessed")) as any;
16
+ return mod.default ?? mod;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ }
22
+
23
+ export type HeadingLevel = 1 | 2 | 3;
24
+
25
+ /**
26
+ * Get the styling for a heading level
27
+ */
28
+ export function getHeadingStyle(level: HeadingLevel): { color: string; bold: boolean } {
29
+ switch (level) {
30
+ case 1:
31
+ return { color: "bright-white", bold: true };
32
+ case 2:
33
+ return { color: "cyan", bold: false };
34
+ case 3:
35
+ return { color: "white", bold: false };
36
+ default:
37
+ return { color: "white", bold: false };
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Format heading text with appropriate blessed tags
43
+ */
44
+ export function formatHeading(text: string, level: HeadingLevel): string {
45
+ const style = getHeadingStyle(level);
46
+ const colorTag = style.color.replace("-", "");
47
+
48
+ if (style.bold) {
49
+ return `{bold}{${colorTag}-fg}${text}{/${colorTag}-fg}{/bold}`;
50
+ }
51
+ return `{${colorTag}-fg}${text}{/${colorTag}-fg}`;
52
+ }
53
+
54
+ /**
55
+ * Create a heading box with proper styling and spacing
56
+ */
57
+ export async function createHeading(
58
+ // biome-ignore lint/suspicious/noExplicitAny: blessed types
59
+ parent: any,
60
+ text: string,
61
+ level: HeadingLevel,
62
+ options: {
63
+ top?: number | string;
64
+ left?: number | string;
65
+ width?: number | string;
66
+ } = {},
67
+ // biome-ignore lint/suspicious/noExplicitAny: blessed element type
68
+ ): Promise<any | null> {
69
+ const blessed = await loadBlessed();
70
+ if (!blessed) return null;
71
+
72
+ const style = getHeadingStyle(level);
73
+
74
+ return blessed.box({
75
+ parent,
76
+ content: formatHeading(text, level),
77
+ top: options.top || 0,
78
+ left: options.left || 0,
79
+ width: options.width || "100%",
80
+ height: 1,
81
+ tags: true,
82
+ style: {
83
+ fg: style.color,
84
+ bold: style.bold,
85
+ },
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Add a heading with automatic spacing (blank line before)
91
+ */
92
+ export async function addHeadingWithSpacing(
93
+ // biome-ignore lint/suspicious/noExplicitAny: blessed types
94
+ parent: any,
95
+ text: string,
96
+ level: HeadingLevel,
97
+ currentTop: number,
98
+ options: {
99
+ left?: number | string;
100
+ width?: number | string;
101
+ } = {},
102
+ // biome-ignore lint/suspicious/noExplicitAny: blessed element type
103
+ ): Promise<{ element: any; nextTop: number }> {
104
+ const blessed = await loadBlessed();
105
+ if (!blessed) return { element: null, nextTop: currentTop };
106
+
107
+ // Add blank line before heading (except if it's the very first element)
108
+ const actualTop = currentTop === 0 ? 0 : currentTop + 1;
109
+
110
+ const heading = await createHeading(parent, text, level, {
111
+ top: actualTop,
112
+ left: options.left,
113
+ width: options.width,
114
+ });
115
+
116
+ // Return next available position (heading + 1 line for spacing after)
117
+ return {
118
+ element: heading,
119
+ nextTop: actualTop + 1,
120
+ };
121
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Loading screen utilities for blessed-based terminal UI
3
+ */
4
+
5
+ import { createRequire } from "node:module";
6
+
7
+ // biome-ignore lint/suspicious/noExplicitAny: blessed is dynamically loaded
8
+ async function loadBlessed(): Promise<any | null> {
9
+ try {
10
+ const require = createRequire(import.meta.url);
11
+ return require("blessed");
12
+ } catch {
13
+ try {
14
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic import
15
+ const mod = await import("blessed" as any);
16
+ return mod.default ?? mod;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ }
22
+
23
+ export interface LoadingScreen {
24
+ update: (message: string) => void;
25
+ close: () => void;
26
+ }
27
+
28
+ /**
29
+ * Show a loading screen while an async operation runs.
30
+ * Falls back to console.log if blessed is not available.
31
+ */
32
+ export async function withLoadingScreen<T>(message: string, operation: () => Promise<T>): Promise<T> {
33
+ const blessed = await loadBlessed();
34
+
35
+ if (!blessed) {
36
+ // Fallback: just log the message
37
+ console.log(`${message}...`);
38
+ return operation();
39
+ }
40
+
41
+ const screen = blessed.screen({
42
+ smartCSR: true,
43
+ title: "Loading...",
44
+ });
45
+
46
+ // Create loading box
47
+ const loadingBox = blessed.box({
48
+ parent: screen,
49
+ top: "center",
50
+ left: "center",
51
+ width: "50%",
52
+ height: 7,
53
+ border: "line",
54
+ label: " Loading ",
55
+ padding: 1,
56
+ style: {
57
+ border: { fg: "cyan" },
58
+ },
59
+ });
60
+
61
+ // Loading message
62
+ const messageText = blessed.text({
63
+ parent: loadingBox,
64
+ top: 0,
65
+ left: "center",
66
+ content: message,
67
+ style: { fg: "white" },
68
+ });
69
+
70
+ // Spinner
71
+ const spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
72
+ let spinnerIndex = 0;
73
+
74
+ const spinner = blessed.text({
75
+ parent: loadingBox,
76
+ top: 2,
77
+ left: "center",
78
+ content: spinnerChars[0],
79
+ style: { fg: "cyan" },
80
+ });
81
+
82
+ // Start spinner animation
83
+ const spinnerInterval = setInterval(() => {
84
+ spinnerIndex = (spinnerIndex + 1) % spinnerChars.length;
85
+ spinner.setContent(spinnerChars[spinnerIndex]);
86
+ screen.render();
87
+ }, 100);
88
+
89
+ // Allow escape to cancel (though operation continues)
90
+ let cancelled = false;
91
+ screen.key(["escape", "C-c"], () => {
92
+ cancelled = true;
93
+ clearInterval(spinnerInterval);
94
+ screen.destroy();
95
+ });
96
+
97
+ screen.render();
98
+
99
+ try {
100
+ const result = await operation();
101
+
102
+ // Clean up
103
+ clearInterval(spinnerInterval);
104
+ if (!cancelled) {
105
+ screen.destroy();
106
+ }
107
+
108
+ return result;
109
+ } catch (error) {
110
+ // Clean up on error
111
+ clearInterval(spinnerInterval);
112
+ if (!cancelled) {
113
+ screen.destroy();
114
+ }
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Create a loading screen that can be updated with progress messages.
121
+ * Useful for multi-step operations.
122
+ */
123
+ export async function createLoadingScreen(initialMessage: string): Promise<LoadingScreen | null> {
124
+ const blessed = await loadBlessed();
125
+
126
+ if (!blessed) {
127
+ // Fallback: return a simple console logger
128
+ console.log(`${initialMessage}...`);
129
+ return {
130
+ update: (msg) => console.log(` ${msg}...`),
131
+ close: () => {},
132
+ };
133
+ }
134
+
135
+ const screen = blessed.screen({
136
+ smartCSR: true,
137
+ title: "Loading...",
138
+ });
139
+
140
+ const loadingBox = blessed.box({
141
+ parent: screen,
142
+ top: "center",
143
+ left: "center",
144
+ width: "60%",
145
+ height: 10,
146
+ border: "line",
147
+ label: " Loading ",
148
+ padding: 1,
149
+ scrollable: true,
150
+ alwaysScroll: true,
151
+ style: {
152
+ border: { fg: "cyan" },
153
+ },
154
+ });
155
+
156
+ // Progress messages area
157
+ const messages = blessed.log({
158
+ parent: loadingBox,
159
+ top: 0,
160
+ left: 0,
161
+ width: "100%-2",
162
+ height: "100%-2",
163
+ tags: true,
164
+ style: { fg: "white" },
165
+ });
166
+
167
+ // Add initial message
168
+ messages.log(initialMessage);
169
+
170
+ // Spinner at bottom
171
+ const spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
172
+ let spinnerIndex = 0;
173
+
174
+ const spinner = blessed.text({
175
+ parent: screen,
176
+ bottom: 1,
177
+ left: "center",
178
+ content: spinnerChars[0],
179
+ style: { fg: "cyan" },
180
+ });
181
+
182
+ // Start spinner animation
183
+ const spinnerInterval = setInterval(() => {
184
+ spinnerIndex = (spinnerIndex + 1) % spinnerChars.length;
185
+ spinner.setContent(spinnerChars[spinnerIndex]);
186
+ screen.render();
187
+ }, 100);
188
+
189
+ // Allow escape to close
190
+ let closed = false;
191
+ screen.key(["escape", "C-c"], () => {
192
+ if (!closed) {
193
+ closed = true;
194
+ clearInterval(spinnerInterval);
195
+ screen.destroy();
196
+ }
197
+ });
198
+
199
+ screen.render();
200
+
201
+ return {
202
+ update: (message: string) => {
203
+ if (!closed) {
204
+ messages.log(message);
205
+ screen.render();
206
+ }
207
+ },
208
+ close: () => {
209
+ if (!closed) {
210
+ closed = true;
211
+ clearInterval(spinnerInterval);
212
+ screen.destroy();
213
+ }
214
+ },
215
+ };
216
+ }
@@ -0,0 +1,53 @@
1
+ /* Status icon and color mappings for consistent UI display */
2
+
3
+ export interface StatusStyle {
4
+ icon: string;
5
+ color: string;
6
+ }
7
+
8
+ /**
9
+ * Get the icon and color for a given status
10
+ * @param status - The task status
11
+ * @returns The icon and color for the status
12
+ */
13
+ export function getStatusStyle(status: string): StatusStyle {
14
+ const statusMap: Record<string, StatusStyle> = {
15
+ Done: { icon: "✔", color: "green" },
16
+ "In Progress": { icon: "◒", color: "yellow" },
17
+ Blocked: { icon: "●", color: "red" },
18
+ "To Do": { icon: "○", color: "white" },
19
+ Review: { icon: "◆", color: "blue" },
20
+ Testing: { icon: "▣", color: "cyan" },
21
+ };
22
+
23
+ // Return the mapped style or default for unknown statuses
24
+ return statusMap[status] || { icon: "○", color: "white" };
25
+ }
26
+
27
+ /**
28
+ * Get just the color for a status (for backward compatibility)
29
+ * @param status - The task status
30
+ * @returns The color for the status
31
+ */
32
+ export function getStatusColor(status: string): string {
33
+ return getStatusStyle(status).color;
34
+ }
35
+
36
+ /**
37
+ * Get just the icon for a status
38
+ * @param status - The task status
39
+ * @returns The icon for the status
40
+ */
41
+ export function getStatusIcon(status: string): string {
42
+ return getStatusStyle(status).icon;
43
+ }
44
+
45
+ /**
46
+ * Format a status with its icon
47
+ * @param status - The task status
48
+ * @returns The formatted status string with icon
49
+ */
50
+ export function formatStatusWithIcon(status: string): string {
51
+ const style = getStatusStyle(status);
52
+ return `${style.icon} ${status}`;
53
+ }