@compilr-dev/cli 0.4.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 (152) hide show
  1. package/README.md +110 -0
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agent.js +317 -0
  4. package/dist/agents/registry.d.ts +66 -0
  5. package/dist/agents/registry.js +238 -0
  6. package/dist/agents/types.d.ts +40 -0
  7. package/dist/agents/types.js +94 -0
  8. package/dist/commands/custom-registry.d.ts +69 -0
  9. package/dist/commands/custom-registry.js +246 -0
  10. package/dist/commands/index.d.ts +7 -0
  11. package/dist/commands/index.js +7 -0
  12. package/dist/commands/types.d.ts +31 -0
  13. package/dist/commands/types.js +26 -0
  14. package/dist/commands.d.ts +63 -0
  15. package/dist/commands.js +324 -0
  16. package/dist/db/index.d.ts +42 -0
  17. package/dist/db/index.js +146 -0
  18. package/dist/db/repositories/document-repository.d.ts +63 -0
  19. package/dist/db/repositories/document-repository.js +184 -0
  20. package/dist/db/repositories/index.d.ts +9 -0
  21. package/dist/db/repositories/index.js +6 -0
  22. package/dist/db/repositories/project-repository.d.ts +132 -0
  23. package/dist/db/repositories/project-repository.js +337 -0
  24. package/dist/db/repositories/work-item-repository.d.ts +115 -0
  25. package/dist/db/repositories/work-item-repository.js +389 -0
  26. package/dist/db/schema.d.ts +83 -0
  27. package/dist/db/schema.js +143 -0
  28. package/dist/debug.d.ts +8 -0
  29. package/dist/debug.js +48 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +348 -0
  32. package/dist/index.old.d.ts +7 -0
  33. package/dist/index.old.js +1014 -0
  34. package/dist/repl.d.ts +121 -0
  35. package/dist/repl.js +1878 -0
  36. package/dist/settings/index.d.ts +80 -0
  37. package/dist/settings/index.js +195 -0
  38. package/dist/shared-handlers.d.ts +63 -0
  39. package/dist/shared-handlers.js +57 -0
  40. package/dist/slash-autocomplete.d.ts +41 -0
  41. package/dist/slash-autocomplete.js +638 -0
  42. package/dist/state.d.ts +75 -0
  43. package/dist/state.js +130 -0
  44. package/dist/tabbed-menu.d.ts +11 -0
  45. package/dist/tabbed-menu.js +328 -0
  46. package/dist/templates/backlog-md.d.ts +7 -0
  47. package/dist/templates/backlog-md.js +94 -0
  48. package/dist/templates/claude-md.d.ts +7 -0
  49. package/dist/templates/claude-md.js +189 -0
  50. package/dist/templates/coding-standards.d.ts +7 -0
  51. package/dist/templates/coding-standards.js +299 -0
  52. package/dist/templates/compilr-md.d.ts +7 -0
  53. package/dist/templates/compilr-md.js +189 -0
  54. package/dist/templates/config-json.d.ts +38 -0
  55. package/dist/templates/config-json.js +39 -0
  56. package/dist/templates/gitignore.d.ts +7 -0
  57. package/dist/templates/gitignore.js +85 -0
  58. package/dist/templates/index.d.ts +19 -0
  59. package/dist/templates/index.js +302 -0
  60. package/dist/templates/package-json.d.ts +7 -0
  61. package/dist/templates/package-json.js +111 -0
  62. package/dist/templates/readme-md.d.ts +7 -0
  63. package/dist/templates/readme-md.js +161 -0
  64. package/dist/templates/tsconfig.d.ts +7 -0
  65. package/dist/templates/tsconfig.js +61 -0
  66. package/dist/templates/types.d.ts +33 -0
  67. package/dist/templates/types.js +24 -0
  68. package/dist/test-autocomplete.d.ts +7 -0
  69. package/dist/test-autocomplete.js +85 -0
  70. package/dist/test-tabbed-menu.d.ts +7 -0
  71. package/dist/test-tabbed-menu.js +25 -0
  72. package/dist/themes/colors.d.ts +49 -0
  73. package/dist/themes/colors.js +135 -0
  74. package/dist/themes/index.d.ts +23 -0
  75. package/dist/themes/index.js +24 -0
  76. package/dist/themes/registry.d.ts +60 -0
  77. package/dist/themes/registry.js +195 -0
  78. package/dist/themes/types.d.ts +82 -0
  79. package/dist/themes/types.js +7 -0
  80. package/dist/tool-selector.d.ts +71 -0
  81. package/dist/tool-selector.js +184 -0
  82. package/dist/tools/ask-user-simple.d.ts +19 -0
  83. package/dist/tools/ask-user-simple.js +86 -0
  84. package/dist/tools/ask-user.d.ts +32 -0
  85. package/dist/tools/ask-user.js +113 -0
  86. package/dist/tools/backlog.d.ts +53 -0
  87. package/dist/tools/backlog.js +709 -0
  88. package/dist/tools.d.ts +15 -0
  89. package/dist/tools.js +121 -0
  90. package/dist/ui/agents-overlay.d.ts +12 -0
  91. package/dist/ui/agents-overlay.js +501 -0
  92. package/dist/ui/arch-type-overlay.d.ts +20 -0
  93. package/dist/ui/arch-type-overlay.js +229 -0
  94. package/dist/ui/ask-user-overlay.d.ts +26 -0
  95. package/dist/ui/ask-user-overlay.js +647 -0
  96. package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
  97. package/dist/ui/ask-user-simple-overlay.js +242 -0
  98. package/dist/ui/backlog-overlay.d.ts +17 -0
  99. package/dist/ui/backlog-overlay.js +786 -0
  100. package/dist/ui/commands-overlay.d.ts +11 -0
  101. package/dist/ui/commands-overlay.js +410 -0
  102. package/dist/ui/config-overlay.d.ts +34 -0
  103. package/dist/ui/config-overlay.js +977 -0
  104. package/dist/ui/conversation.d.ts +82 -0
  105. package/dist/ui/conversation.js +508 -0
  106. package/dist/ui/diff.d.ts +38 -0
  107. package/dist/ui/diff.js +182 -0
  108. package/dist/ui/ephemeral.d.ts +111 -0
  109. package/dist/ui/ephemeral.js +413 -0
  110. package/dist/ui/file-autocomplete.d.ts +45 -0
  111. package/dist/ui/file-autocomplete.js +237 -0
  112. package/dist/ui/footer.d.ts +153 -0
  113. package/dist/ui/footer.js +422 -0
  114. package/dist/ui/index.d.ts +12 -0
  115. package/dist/ui/index.js +15 -0
  116. package/dist/ui/init-overlay.d.ts +24 -0
  117. package/dist/ui/init-overlay.js +525 -0
  118. package/dist/ui/input-prompt-v2.d.ts +179 -0
  119. package/dist/ui/input-prompt-v2.js +991 -0
  120. package/dist/ui/input-prompt.d.ts +97 -0
  121. package/dist/ui/input-prompt.js +800 -0
  122. package/dist/ui/iteration-limit-overlay.d.ts +21 -0
  123. package/dist/ui/iteration-limit-overlay.js +150 -0
  124. package/dist/ui/keys-overlay.d.ts +14 -0
  125. package/dist/ui/keys-overlay.js +181 -0
  126. package/dist/ui/model-warning-overlay.d.ts +30 -0
  127. package/dist/ui/model-warning-overlay.js +171 -0
  128. package/dist/ui/overlay-controller.d.ts +25 -0
  129. package/dist/ui/overlay-controller.js +35 -0
  130. package/dist/ui/overlays.d.ts +47 -0
  131. package/dist/ui/overlays.js +627 -0
  132. package/dist/ui/permission-overlay.d.ts +16 -0
  133. package/dist/ui/permission-overlay.js +494 -0
  134. package/dist/ui/terminal.d.ts +117 -0
  135. package/dist/ui/terminal.js +237 -0
  136. package/dist/ui/todo-zone.d.ts +112 -0
  137. package/dist/ui/todo-zone.js +353 -0
  138. package/dist/ui/tools-overlay.d.ts +26 -0
  139. package/dist/ui/tools-overlay.js +278 -0
  140. package/dist/ui/tutorial-overlay.d.ts +10 -0
  141. package/dist/ui/tutorial-overlay.js +936 -0
  142. package/dist/ui/types.d.ts +103 -0
  143. package/dist/ui/types.js +33 -0
  144. package/dist/utils/credentials.d.ts +55 -0
  145. package/dist/utils/credentials.js +268 -0
  146. package/dist/utils/model-tiers.d.ts +37 -0
  147. package/dist/utils/model-tiers.js +118 -0
  148. package/dist/utils/project-memory.d.ts +47 -0
  149. package/dist/utils/project-memory.js +117 -0
  150. package/dist/utils/project-status.d.ts +56 -0
  151. package/dist/utils/project-status.js +237 -0
  152. package/package.json +66 -0
@@ -0,0 +1,786 @@
1
+ /**
2
+ * Backlog Overlay
3
+ *
4
+ * Modal overlay for viewing and managing the project backlog.
5
+ * Provides a UI for manually adding, editing, and filtering backlog items.
6
+ *
7
+ * Features:
8
+ * - Search by title/description
9
+ * - Filter tabs by type (All/Feature/Bug/Tech-Debt/Chore)
10
+ * - Pagination for large backlogs
11
+ * - Quick status toggle with Space
12
+ * - Add new items with wizard
13
+ */
14
+ import chalk from 'chalk';
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import * as terminal from './terminal.js';
18
+ import { getStyles } from '../themes/index.js';
19
+ // =============================================================================
20
+ // Constants
21
+ // =============================================================================
22
+ const PAGE_SIZE = 8;
23
+ const TYPE_OPTIONS = ['feature', 'bug', 'tech-debt', 'chore'];
24
+ const STATUS_OPTIONS = ['📋', '🚧', '✅'];
25
+ const PRIORITY_OPTIONS = ['critical', 'high', 'medium', 'low'];
26
+ const TYPE_LABELS = {
27
+ 'feature': 'Feature',
28
+ 'bug': 'Bug',
29
+ 'tech-debt': 'Tech Debt',
30
+ 'chore': 'Chore',
31
+ };
32
+ const PRIORITY_LABELS = {
33
+ 'critical': 'Critical',
34
+ 'high': 'High',
35
+ 'medium': 'Medium',
36
+ 'low': 'Low',
37
+ };
38
+ // =============================================================================
39
+ // Backlog File Operations
40
+ // =============================================================================
41
+ function findBacklogPath() {
42
+ const cwd = process.cwd();
43
+ // Single repo pattern: .compilr/backlog.md
44
+ const singleRepoPath = path.join(cwd, '.compilr', 'backlog.md');
45
+ if (fs.existsSync(singleRepoPath)) {
46
+ return singleRepoPath;
47
+ }
48
+ // Two repo pattern: look for -docs sibling folder
49
+ const parentDir = path.dirname(cwd);
50
+ const projectName = path.basename(cwd);
51
+ const docsRepoPath = path.join(parentDir, `${projectName}-docs`, '01-planning', 'backlog.md');
52
+ if (fs.existsSync(docsRepoPath)) {
53
+ return docsRepoPath;
54
+ }
55
+ // Check if we're in the docs repo itself
56
+ const inDocsPath = path.join(cwd, '01-planning', 'backlog.md');
57
+ if (fs.existsSync(inDocsPath)) {
58
+ return inDocsPath;
59
+ }
60
+ // Check for project subfolders with -docs pattern
61
+ try {
62
+ const entries = fs.readdirSync(cwd, { withFileTypes: true });
63
+ for (const entry of entries) {
64
+ if (entry.isDirectory() && entry.name.endsWith('-docs')) {
65
+ const docsBacklog = path.join(cwd, entry.name, '01-planning', 'backlog.md');
66
+ if (fs.existsSync(docsBacklog)) {
67
+ return docsBacklog;
68
+ }
69
+ }
70
+ }
71
+ }
72
+ catch {
73
+ // Ignore read errors
74
+ }
75
+ return null;
76
+ }
77
+ function parseBacklogItems(content) {
78
+ const items = [];
79
+ const lines = content.split('\n');
80
+ let inTable = false;
81
+ for (const line of lines) {
82
+ const trimmed = line.trim();
83
+ if (trimmed.startsWith('| ID |')) {
84
+ inTable = true;
85
+ continue;
86
+ }
87
+ if (trimmed.startsWith('|---')) {
88
+ continue;
89
+ }
90
+ if (inTable && trimmed.startsWith('|')) {
91
+ const cells = trimmed.split('|').map(c => c.trim()).filter(c => c !== '');
92
+ if (cells.length >= 6) {
93
+ const [id, type, status, priority, title, description, commit] = cells;
94
+ if (id && /^[A-Z]+-\d{3}$/.test(id)) {
95
+ items.push({
96
+ id,
97
+ type: type,
98
+ status: status,
99
+ priority: priority,
100
+ title,
101
+ description,
102
+ commit: commit || undefined,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ else if (inTable && !trimmed.startsWith('|')) {
108
+ inTable = false;
109
+ }
110
+ }
111
+ return items;
112
+ }
113
+ function generateBacklogMarkdown(items) {
114
+ const date = new Date().toISOString().split('T')[0];
115
+ let content = `# Backlog
116
+
117
+ | ID | Type | Status | Priority | Title | Description | Commit |
118
+ |----|------|--------|----------|-------|-------------|--------|
119
+ `;
120
+ for (const item of items) {
121
+ content += `| ${item.id} | ${item.type} | ${item.status} | ${item.priority} | ${item.title} | ${item.description} | ${item.commit ?? ''} |\n`;
122
+ }
123
+ content += `
124
+ ---
125
+
126
+ *Last updated: ${date} by user*
127
+ `;
128
+ return content;
129
+ }
130
+ function generateId(type, existingItems) {
131
+ const prefixMap = {
132
+ 'feature': 'REQ',
133
+ 'bug': 'BUG',
134
+ 'tech-debt': 'TECH',
135
+ 'chore': 'CHORE',
136
+ };
137
+ const prefix = prefixMap[type];
138
+ let maxNum = 0;
139
+ for (const item of existingItems) {
140
+ if (item.id.startsWith(prefix + '-')) {
141
+ const num = parseInt(item.id.split('-')[1], 10);
142
+ if (!isNaN(num) && num > maxNum) {
143
+ maxNum = num;
144
+ }
145
+ }
146
+ }
147
+ return `${prefix}-${String(maxNum + 1).padStart(3, '0')}`;
148
+ }
149
+ // =============================================================================
150
+ // Filtering & Search
151
+ // =============================================================================
152
+ function updateFilteredItems(state) {
153
+ let results = [...state.items];
154
+ // Apply type filter
155
+ if (state.filterType === 'active') {
156
+ // Active = not completed (📋 or 🚧)
157
+ results = results.filter(item => item.status === '📋' || item.status === '🚧');
158
+ }
159
+ else if (state.filterType !== 'all') {
160
+ results = results.filter(item => item.type === state.filterType);
161
+ }
162
+ // Apply search query
163
+ if (state.searchQuery.trim()) {
164
+ const query = state.searchQuery.toLowerCase();
165
+ results = results.filter(item => item.title.toLowerCase().includes(query) ||
166
+ item.description.toLowerCase().includes(query) ||
167
+ item.id.toLowerCase().includes(query));
168
+ }
169
+ state.filteredItems = results;
170
+ state.selectedIndex = 0;
171
+ state.currentPage = 0;
172
+ }
173
+ // =============================================================================
174
+ // Rendering
175
+ // =============================================================================
176
+ function render(state, prevLineCount) {
177
+ const s = getStyles();
178
+ const lines = [];
179
+ const cols = terminal.getTerminalWidth();
180
+ const border = s.muted('─'.repeat(Math.max(1, cols - 1)));
181
+ // Clear previous content
182
+ terminal.clearLinesAbove(prevLineCount);
183
+ // Header
184
+ lines.push(border);
185
+ lines.push(' ' + s.primaryBold('BACKLOG') + s.muted(` (${String(state.items.length)} total)`));
186
+ lines.push('');
187
+ if (state.mode === 'view') {
188
+ // Search box - show cursor only when in search mode
189
+ const searchCursor = state.isSearching ? '█' : '';
190
+ const searchPrefix = state.isSearching ? s.primary(' Search: ') : s.muted(' Search: ');
191
+ lines.push(searchPrefix + state.searchQuery + searchCursor);
192
+ // Filter tabs
193
+ const tabAll = state.filterType === 'all' ? s.selected(' All ') : s.muted(' All ');
194
+ const tabActive = state.filterType === 'active' ? s.selected(' Active ') : s.muted(' Active ');
195
+ const tabFeature = state.filterType === 'feature' ? s.selected(' Feature ') : s.muted(' Feature ');
196
+ const tabBug = state.filterType === 'bug' ? s.selected(' Bug ') : s.muted(' Bug ');
197
+ const tabTech = state.filterType === 'tech-debt' ? s.selected(' Tech ') : s.muted(' Tech ');
198
+ const tabChore = state.filterType === 'chore' ? s.selected(' Chore ') : s.muted(' Chore ');
199
+ lines.push(` Filter: ${tabAll}${tabActive}${tabFeature}${tabBug}${tabTech}${tabChore} ${s.muted('(Tab)')}`);
200
+ lines.push('');
201
+ // Results info
202
+ const totalResults = state.filteredItems.length;
203
+ const totalPages = Math.ceil(totalResults / PAGE_SIZE);
204
+ const startIndex = state.currentPage * PAGE_SIZE;
205
+ const endIndex = Math.min(startIndex + PAGE_SIZE, totalResults);
206
+ const pageItems = state.filteredItems.slice(startIndex, endIndex);
207
+ if (totalResults === 0) {
208
+ lines.push(s.muted(' No items found'));
209
+ if (state.searchQuery || state.filterType !== 'all') {
210
+ lines.push(s.muted(' Try clearing search or changing filter'));
211
+ }
212
+ else {
213
+ lines.push(s.muted(' Use [a] to add a new item or /design to create initial backlog'));
214
+ }
215
+ }
216
+ else {
217
+ lines.push(s.muted(` Showing ${String(startIndex + 1)}-${String(endIndex)} of ${String(totalResults)}:`));
218
+ lines.push('');
219
+ // Table header
220
+ const idW = 10;
221
+ const typeW = 10;
222
+ const statusW = 4;
223
+ const priW = 8;
224
+ const titleW = Math.max(20, cols - idW - typeW - statusW - priW - 10);
225
+ lines.push(s.muted(` ${'ID'.padEnd(idW)}${'Type'.padEnd(typeW)}${'St'.padEnd(statusW)}${'Pri'.padEnd(priW)}${'Title'.slice(0, titleW)}`));
226
+ // Items
227
+ for (let i = 0; i < pageItems.length; i++) {
228
+ const item = pageItems[i];
229
+ const isSelected = i === state.selectedIndex;
230
+ const prefix = isSelected ? '❯' : ' ';
231
+ const row = `${prefix}${item.id.padEnd(idW)}${item.type.padEnd(typeW)}${item.status.padEnd(statusW)}${item.priority.padEnd(priW)}${item.title.slice(0, titleW - 2)}`;
232
+ if (isSelected) {
233
+ lines.push(s.primary(' ' + row));
234
+ }
235
+ else {
236
+ lines.push(s.muted(' ' + row));
237
+ }
238
+ }
239
+ // Selected item details (truncate to prevent line wrapping)
240
+ if (pageItems.length > 0 && state.selectedIndex < pageItems.length) {
241
+ lines.push('');
242
+ const selected = pageItems[state.selectedIndex];
243
+ const descPrefix = ' Desc: ';
244
+ const maxDescLen = cols - descPrefix.length - 2;
245
+ const truncatedDesc = selected.description.length > maxDescLen
246
+ ? selected.description.slice(0, maxDescLen - 3) + '...'
247
+ : selected.description;
248
+ lines.push(s.muted(descPrefix) + truncatedDesc);
249
+ if (selected.commit) {
250
+ lines.push(s.muted(' Commit: ') + selected.commit);
251
+ }
252
+ }
253
+ }
254
+ lines.push('');
255
+ // Pagination
256
+ if (totalPages > 1) {
257
+ const pageInfo = `Page ${String(state.currentPage + 1)} of ${String(totalPages)}`;
258
+ const prevHint = state.currentPage > 0 ? '← ' : ' ';
259
+ const nextHint = state.currentPage < totalPages - 1 ? ' →' : '';
260
+ lines.push(s.muted(` ${prevHint}${pageInfo}${nextHint}`));
261
+ }
262
+ lines.push(border);
263
+ if (state.isSearching) {
264
+ lines.push(s.muted(' Type to search · Enter/Esc Exit search · Backspace Delete'));
265
+ }
266
+ else {
267
+ lines.push(s.muted(' / Search · ↑↓ Navigate · ←→ Pages · Tab Filter · Enter Detail · Space Toggle · a Add · Esc Close'));
268
+ }
269
+ }
270
+ else if (state.mode === 'detail') {
271
+ // Detail view
272
+ const item = state.detailItem;
273
+ if (item) {
274
+ lines.push(chalk.bold(` ${item.id}: ${item.title}`));
275
+ lines.push('');
276
+ // Editable fields with selection indicator
277
+ const typePrefix = state.detailField === 0 ? s.primary('❯ Type: ') : s.muted(' Type: ');
278
+ const typeValue = state.detailField === 0 ? s.primary(TYPE_LABELS[item.type]) : TYPE_LABELS[item.type];
279
+ lines.push(typePrefix + typeValue);
280
+ const statusPrefix = state.detailField === 1 ? s.primary('❯ Status: ') : s.muted(' Status: ');
281
+ const statusValue = item.status + ' ' + getStatusLabel(item.status);
282
+ lines.push(statusPrefix + (state.detailField === 1 ? s.primary(statusValue) : statusValue));
283
+ const priPrefix = state.detailField === 2 ? s.primary('❯ Priority: ') : s.muted(' Priority: ');
284
+ const priValue = state.detailField === 2 ? s.primary(PRIORITY_LABELS[item.priority]) : PRIORITY_LABELS[item.priority];
285
+ lines.push(priPrefix + priValue);
286
+ lines.push('');
287
+ lines.push(s.muted(' Description:'));
288
+ // Word-wrap description to fit terminal width
289
+ const descLines = wrapText(item.description, cols - 4);
290
+ for (const line of descLines) {
291
+ lines.push(' ' + line);
292
+ }
293
+ if (item.commit) {
294
+ lines.push('');
295
+ lines.push(s.muted(' Commit: ') + item.commit);
296
+ }
297
+ }
298
+ lines.push('');
299
+ lines.push(border);
300
+ lines.push(s.muted(' ↑↓ Select field · ←→ Change value · Esc Back'));
301
+ }
302
+ else {
303
+ // Add mode rendering
304
+ const stepLabels = ['Type', 'Priority', 'Title', 'Description', 'Confirm'];
305
+ lines.push(s.muted(` Step ${String(state.addStep + 1)}/5: ${stepLabels[state.addStep]}`));
306
+ lines.push('');
307
+ switch (state.addStep) {
308
+ case 0: // Type
309
+ lines.push(chalk.bold(' What type of item?'));
310
+ lines.push('');
311
+ for (let i = 0; i < TYPE_OPTIONS.length; i++) {
312
+ const isSelected = state.selectedIndex === i;
313
+ const prefix = isSelected ? ' ❯ ' : ' ';
314
+ const label = `${String(i + 1)}. ${TYPE_LABELS[TYPE_OPTIONS[i]]}`;
315
+ lines.push(isSelected ? s.primary(prefix + label) : s.muted(prefix + label));
316
+ }
317
+ break;
318
+ case 1: // Priority
319
+ lines.push(chalk.bold(' Priority level?'));
320
+ lines.push('');
321
+ for (let i = 0; i < PRIORITY_OPTIONS.length; i++) {
322
+ const isSelected = state.selectedIndex === i;
323
+ const prefix = isSelected ? ' ❯ ' : ' ';
324
+ const label = `${String(i + 1)}. ${PRIORITY_LABELS[PRIORITY_OPTIONS[i]]}`;
325
+ lines.push(isSelected ? s.primary(prefix + label) : s.muted(prefix + label));
326
+ }
327
+ break;
328
+ case 2: // Title
329
+ lines.push(chalk.bold(' Title (short description)'));
330
+ lines.push('');
331
+ lines.push(` > ${state.inputBuffer}█`);
332
+ lines.push('');
333
+ if (state.error) {
334
+ lines.push(s.error(` ${state.error}`));
335
+ }
336
+ else {
337
+ lines.push(s.muted(' Keep it concise (2-100 characters)'));
338
+ }
339
+ break;
340
+ case 3: // Description
341
+ lines.push(chalk.bold(' Description (details)'));
342
+ lines.push('');
343
+ lines.push(` > ${state.inputBuffer}█`);
344
+ lines.push('');
345
+ if (state.error) {
346
+ lines.push(s.error(` ${state.error}`));
347
+ }
348
+ else {
349
+ lines.push(s.muted(' Explain the requirement or issue'));
350
+ }
351
+ break;
352
+ case 4: // Confirm
353
+ lines.push(chalk.bold(' Confirm new item:'));
354
+ lines.push('');
355
+ lines.push(s.muted(' Type: ') + (state.newItem.type ? TYPE_LABELS[state.newItem.type] : 'N/A'));
356
+ lines.push(s.muted(' Priority: ') + (state.newItem.priority ? PRIORITY_LABELS[state.newItem.priority] : 'N/A'));
357
+ lines.push(s.muted(' Title: ') + state.newItem.title);
358
+ lines.push(s.muted(' Description: ') + state.newItem.description);
359
+ lines.push('');
360
+ for (let i = 0; i < 2; i++) {
361
+ const isSelected = state.selectedIndex === i;
362
+ const prefix = isSelected ? ' ❯ ' : ' ';
363
+ const label = i === 0 ? '1. Create item' : '2. Cancel';
364
+ lines.push(isSelected ? s.primary(prefix + label) : s.muted(prefix + label));
365
+ }
366
+ break;
367
+ }
368
+ lines.push('');
369
+ lines.push(border);
370
+ if (state.addStep === 2 || state.addStep === 3) {
371
+ lines.push(s.muted(' Enter Confirm · Esc Go back'));
372
+ }
373
+ else {
374
+ lines.push(s.muted(' ↑↓ Navigate · Enter Select · Esc Go back'));
375
+ }
376
+ }
377
+ // Write new content
378
+ terminal.write(lines.join('\n'));
379
+ return lines.length;
380
+ }
381
+ // =============================================================================
382
+ // Helper Functions
383
+ // =============================================================================
384
+ function getStatusLabel(status) {
385
+ switch (status) {
386
+ case '📋': return 'Backlog';
387
+ case '🚧': return 'In Progress';
388
+ case '✅': return 'Completed';
389
+ default: return '';
390
+ }
391
+ }
392
+ function wrapText(text, maxWidth) {
393
+ if (text.length <= maxWidth)
394
+ return [text];
395
+ const words = text.split(' ');
396
+ const lines = [];
397
+ let currentLine = '';
398
+ for (const word of words) {
399
+ if (currentLine.length === 0) {
400
+ currentLine = word;
401
+ }
402
+ else if (currentLine.length + 1 + word.length <= maxWidth) {
403
+ currentLine += ' ' + word;
404
+ }
405
+ else {
406
+ lines.push(currentLine);
407
+ currentLine = word;
408
+ }
409
+ }
410
+ if (currentLine.length > 0) {
411
+ lines.push(currentLine);
412
+ }
413
+ return lines;
414
+ }
415
+ function getMaxIndexForAddStep(step) {
416
+ switch (step) {
417
+ case 0: return TYPE_OPTIONS.length - 1;
418
+ case 1: return PRIORITY_OPTIONS.length - 1;
419
+ case 4: return 1;
420
+ default: return 0;
421
+ }
422
+ }
423
+ // =============================================================================
424
+ // Main Export
425
+ // =============================================================================
426
+ export async function showBacklogOverlay() {
427
+ const backlogPath = findBacklogPath();
428
+ let items = [];
429
+ if (backlogPath && fs.existsSync(backlogPath)) {
430
+ const content = fs.readFileSync(backlogPath, 'utf-8');
431
+ items = parseBacklogItems(content);
432
+ }
433
+ const state = {
434
+ mode: 'view',
435
+ items,
436
+ filteredItems: [...items],
437
+ selectedIndex: 0,
438
+ backlogPath,
439
+ searchQuery: '',
440
+ filterType: 'all',
441
+ isSearching: false,
442
+ currentPage: 0,
443
+ detailItem: null,
444
+ detailField: 0,
445
+ addStep: 0,
446
+ newItem: {
447
+ type: null,
448
+ priority: null,
449
+ title: '',
450
+ description: '',
451
+ },
452
+ inputBuffer: '',
453
+ error: null,
454
+ };
455
+ let lineCount = 0;
456
+ let modified = false;
457
+ terminal.writeLine('');
458
+ terminal.hideCursor();
459
+ const wasRawMode = process.stdin.isRaw;
460
+ terminal.enableRawMode();
461
+ lineCount = render(state, 0);
462
+ // Save changes to file
463
+ const saveChanges = () => {
464
+ if (state.backlogPath) {
465
+ const content = generateBacklogMarkdown(state.items);
466
+ const dir = path.dirname(state.backlogPath);
467
+ if (!fs.existsSync(dir)) {
468
+ fs.mkdirSync(dir, { recursive: true });
469
+ }
470
+ fs.writeFileSync(state.backlogPath, content, 'utf-8');
471
+ modified = true;
472
+ // Update filtered items after save
473
+ updateFilteredItems(state);
474
+ }
475
+ };
476
+ return new Promise((resolve) => {
477
+ const cleanup = () => {
478
+ terminal.clearLinesAbove(lineCount);
479
+ terminal.writeLine('');
480
+ terminal.showCursor();
481
+ if (!wasRawMode) {
482
+ terminal.disableRawMode();
483
+ }
484
+ process.stdin.removeListener('data', handleData);
485
+ };
486
+ const handleData = (data) => {
487
+ const isEscape = data.length === 1 && data[0] === 0x1b;
488
+ const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
489
+ const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
490
+ const isLeftArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x44;
491
+ const isRightArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x43;
492
+ const isCtrlC = data.length === 1 && data[0] === 0x03;
493
+ const isEnter = data.length === 1 && (data[0] === 0x0d || data[0] === 0x0a);
494
+ const isBackspace = data.length === 1 && (data[0] === 0x7f || data[0] === 0x08);
495
+ const isSpace = data.length === 1 && data[0] === 0x20;
496
+ const isTab = data.length === 1 && data[0] === 0x09;
497
+ const char = data.length === 1 && data[0] >= 0x20 && data[0] < 0x7f ? String.fromCharCode(data[0]) : null;
498
+ if (isCtrlC) {
499
+ cleanup();
500
+ resolve({ modified });
501
+ return;
502
+ }
503
+ // ===== VIEW MODE =====
504
+ if (state.mode === 'view') {
505
+ const totalResults = state.filteredItems.length;
506
+ const totalPages = Math.ceil(totalResults / PAGE_SIZE);
507
+ const pageItems = state.filteredItems.slice(state.currentPage * PAGE_SIZE, (state.currentPage + 1) * PAGE_SIZE);
508
+ // ----- SEARCH MODE -----
509
+ if (state.isSearching) {
510
+ // Escape or Enter exits search mode
511
+ if (isEscape || isEnter) {
512
+ state.isSearching = false;
513
+ lineCount = render(state, lineCount);
514
+ return;
515
+ }
516
+ // Backspace deletes search char
517
+ if (isBackspace) {
518
+ state.searchQuery = state.searchQuery.slice(0, -1);
519
+ updateFilteredItems(state);
520
+ lineCount = render(state, lineCount);
521
+ return;
522
+ }
523
+ // All printable chars go to search
524
+ if (char) {
525
+ state.searchQuery += char;
526
+ updateFilteredItems(state);
527
+ lineCount = render(state, lineCount);
528
+ return;
529
+ }
530
+ return;
531
+ }
532
+ // ----- NORMAL MODE (not searching) -----
533
+ // Escape closes overlay
534
+ if (isEscape) {
535
+ cleanup();
536
+ resolve({ modified });
537
+ return;
538
+ }
539
+ // '/' enters search mode (vim-style)
540
+ if (char === '/') {
541
+ state.isSearching = true;
542
+ lineCount = render(state, lineCount);
543
+ return;
544
+ }
545
+ // Tab cycles filter type
546
+ if (isTab) {
547
+ const filters = ['all', 'active', 'feature', 'bug', 'tech-debt', 'chore'];
548
+ const currentIdx = filters.indexOf(state.filterType);
549
+ state.filterType = filters[(currentIdx + 1) % filters.length];
550
+ updateFilteredItems(state);
551
+ lineCount = render(state, lineCount);
552
+ return;
553
+ }
554
+ // Navigation
555
+ if (isUpArrow && state.selectedIndex > 0) {
556
+ state.selectedIndex--;
557
+ lineCount = render(state, lineCount);
558
+ return;
559
+ }
560
+ if (isDownArrow && state.selectedIndex < pageItems.length - 1) {
561
+ state.selectedIndex++;
562
+ lineCount = render(state, lineCount);
563
+ return;
564
+ }
565
+ // Page navigation
566
+ if (isLeftArrow && state.currentPage > 0) {
567
+ state.currentPage--;
568
+ state.selectedIndex = 0;
569
+ lineCount = render(state, lineCount);
570
+ return;
571
+ }
572
+ if (isRightArrow && state.currentPage < totalPages - 1) {
573
+ state.currentPage++;
574
+ state.selectedIndex = 0;
575
+ lineCount = render(state, lineCount);
576
+ return;
577
+ }
578
+ // Space - toggle status
579
+ if (isSpace && pageItems.length > 0) {
580
+ const item = pageItems[state.selectedIndex];
581
+ // Find actual item in state.items
582
+ const actualItem = state.items.find(i => i.id === item.id);
583
+ if (actualItem) {
584
+ const statusIdx = STATUS_OPTIONS.indexOf(actualItem.status);
585
+ actualItem.status = STATUS_OPTIONS[(statusIdx + 1) % STATUS_OPTIONS.length];
586
+ saveChanges();
587
+ }
588
+ lineCount = render(state, lineCount);
589
+ return;
590
+ }
591
+ // Enter - open detail view
592
+ if (isEnter && pageItems.length > 0) {
593
+ state.detailItem = pageItems[state.selectedIndex];
594
+ state.mode = 'detail';
595
+ lineCount = render(state, lineCount);
596
+ return;
597
+ }
598
+ // 'a' - add mode
599
+ if (char === 'a' || char === 'A') {
600
+ state.mode = 'add';
601
+ state.addStep = 0;
602
+ state.selectedIndex = 0;
603
+ state.newItem = { type: null, priority: null, title: '', description: '' };
604
+ state.inputBuffer = '';
605
+ state.error = null;
606
+ lineCount = render(state, lineCount);
607
+ return;
608
+ }
609
+ return;
610
+ }
611
+ // ===== DETAIL MODE =====
612
+ if (state.mode === 'detail') {
613
+ // Escape - back to view
614
+ if (isEscape) {
615
+ state.mode = 'view';
616
+ state.detailItem = null;
617
+ state.detailField = 0;
618
+ lineCount = render(state, lineCount);
619
+ return;
620
+ }
621
+ // Up/Down - navigate fields
622
+ if (isUpArrow && state.detailField > 0) {
623
+ state.detailField = (state.detailField - 1);
624
+ lineCount = render(state, lineCount);
625
+ return;
626
+ }
627
+ if (isDownArrow && state.detailField < 2) {
628
+ state.detailField = (state.detailField + 1);
629
+ lineCount = render(state, lineCount);
630
+ return;
631
+ }
632
+ // Left/Right - change value of selected field
633
+ if ((isLeftArrow || isRightArrow) && state.detailItem) {
634
+ const detailId = state.detailItem.id;
635
+ const actualItem = state.items.find(i => i.id === detailId);
636
+ if (actualItem) {
637
+ const direction = isRightArrow ? 1 : -1;
638
+ if (state.detailField === 0) {
639
+ // Type
640
+ const currentIdx = TYPE_OPTIONS.indexOf(actualItem.type);
641
+ const newIdx = (currentIdx + direction + TYPE_OPTIONS.length) % TYPE_OPTIONS.length;
642
+ actualItem.type = TYPE_OPTIONS[newIdx];
643
+ }
644
+ else if (state.detailField === 1) {
645
+ // Status
646
+ const currentIdx = STATUS_OPTIONS.indexOf(actualItem.status);
647
+ const newIdx = (currentIdx + direction + STATUS_OPTIONS.length) % STATUS_OPTIONS.length;
648
+ actualItem.status = STATUS_OPTIONS[newIdx];
649
+ }
650
+ else {
651
+ // Priority
652
+ const currentIdx = PRIORITY_OPTIONS.indexOf(actualItem.priority);
653
+ const newIdx = (currentIdx + direction + PRIORITY_OPTIONS.length) % PRIORITY_OPTIONS.length;
654
+ actualItem.priority = PRIORITY_OPTIONS[newIdx];
655
+ }
656
+ state.detailItem = actualItem;
657
+ saveChanges();
658
+ }
659
+ lineCount = render(state, lineCount);
660
+ return;
661
+ }
662
+ return;
663
+ }
664
+ // ===== ADD MODE =====
665
+ {
666
+ if (state.addStep === 2 || state.addStep === 3) {
667
+ // Text input steps
668
+ if (isEscape) {
669
+ if (state.addStep === 2) {
670
+ state.addStep = 1;
671
+ state.selectedIndex = 0;
672
+ }
673
+ else {
674
+ state.addStep = 2;
675
+ state.inputBuffer = state.newItem.title;
676
+ }
677
+ state.error = null;
678
+ }
679
+ else if (isEnter) {
680
+ const text = state.inputBuffer.trim();
681
+ if (state.addStep === 2) {
682
+ if (text.length < 2) {
683
+ state.error = 'Title too short (min 2 characters)';
684
+ }
685
+ else if (text.length > 100) {
686
+ state.error = 'Title too long (max 100 characters)';
687
+ }
688
+ else {
689
+ state.newItem.title = text;
690
+ state.addStep = 3;
691
+ state.inputBuffer = '';
692
+ state.error = null;
693
+ }
694
+ }
695
+ else {
696
+ if (text.length < 5) {
697
+ state.error = 'Description too short (min 5 characters)';
698
+ }
699
+ else {
700
+ state.newItem.description = text;
701
+ state.addStep = 4;
702
+ state.selectedIndex = 0;
703
+ state.error = null;
704
+ }
705
+ }
706
+ }
707
+ else if (isBackspace) {
708
+ state.inputBuffer = state.inputBuffer.slice(0, -1);
709
+ }
710
+ else if (char) {
711
+ state.inputBuffer += char;
712
+ }
713
+ }
714
+ else {
715
+ // Selection steps
716
+ if (isEscape) {
717
+ if (state.addStep === 0) {
718
+ state.mode = 'view';
719
+ state.selectedIndex = 0;
720
+ }
721
+ else if (state.addStep === 4) {
722
+ state.addStep = 3;
723
+ state.inputBuffer = state.newItem.description;
724
+ }
725
+ else {
726
+ state.addStep = (state.addStep - 1);
727
+ state.selectedIndex = 0;
728
+ }
729
+ }
730
+ else if (isUpArrow) {
731
+ state.selectedIndex = Math.max(0, state.selectedIndex - 1);
732
+ }
733
+ else if (isDownArrow) {
734
+ const maxIdx = getMaxIndexForAddStep(state.addStep);
735
+ state.selectedIndex = Math.min(maxIdx, state.selectedIndex + 1);
736
+ }
737
+ else if (isEnter) {
738
+ switch (state.addStep) {
739
+ case 0:
740
+ state.newItem.type = TYPE_OPTIONS[state.selectedIndex];
741
+ state.addStep = 1;
742
+ state.selectedIndex = 0;
743
+ break;
744
+ case 1:
745
+ state.newItem.priority = PRIORITY_OPTIONS[state.selectedIndex];
746
+ state.addStep = 2;
747
+ state.inputBuffer = '';
748
+ break;
749
+ case 4:
750
+ if (state.newItem.type && state.newItem.priority) {
751
+ if (state.selectedIndex === 0) {
752
+ // Create item
753
+ const id = generateId(state.newItem.type, state.items);
754
+ const newItem = {
755
+ id,
756
+ type: state.newItem.type,
757
+ status: '📋',
758
+ priority: state.newItem.priority,
759
+ title: state.newItem.title,
760
+ description: state.newItem.description,
761
+ };
762
+ state.items.push(newItem);
763
+ updateFilteredItems(state);
764
+ saveChanges();
765
+ }
766
+ state.mode = 'view';
767
+ state.selectedIndex = Math.min(state.selectedIndex, Math.max(0, state.filteredItems.length - 1));
768
+ }
769
+ break;
770
+ }
771
+ }
772
+ else if (char && char >= '1' && char <= '9') {
773
+ const numIdx = parseInt(char, 10) - 1;
774
+ const maxIdx = getMaxIndexForAddStep(state.addStep);
775
+ if (numIdx <= maxIdx) {
776
+ state.selectedIndex = numIdx;
777
+ }
778
+ }
779
+ }
780
+ lineCount = render(state, lineCount);
781
+ return;
782
+ }
783
+ };
784
+ process.stdin.on('data', handleData);
785
+ });
786
+ }