vibecodingmachine-cli 2026.2.26-1739 → 2026.3.9-1621

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 (74) hide show
  1. package/bin/auth/auth-compliance.js +7 -1
  2. package/bin/commands/agent-commands.js +150 -228
  3. package/bin/commands/command-aliases.js +68 -0
  4. package/bin/vibecodingmachine.js +1 -2
  5. package/package.json +2 -2
  6. package/src/commands/agents/list.js +71 -115
  7. package/src/commands/agents-check.js +16 -4
  8. package/src/commands/analyze-file-sizes.js +1 -1
  9. package/src/commands/auto-direct/auto-provider-manager.js +290 -0
  10. package/src/commands/auto-direct/auto-status-display.js +331 -0
  11. package/src/commands/auto-direct/auto-utils.js +439 -0
  12. package/src/commands/auto-direct/file-operations.js +110 -0
  13. package/src/commands/auto-direct/provider-config.js +1 -1
  14. package/src/commands/auto-direct/provider-manager.js +1 -1
  15. package/src/commands/auto-direct/status-display.js +1 -1
  16. package/src/commands/auto-direct/utils.js +24 -18
  17. package/src/commands/auto-direct-refactored.js +413 -0
  18. package/src/commands/auto-direct.js +594 -188
  19. package/src/commands/requirements/commands.js +353 -0
  20. package/src/commands/requirements/default-handlers.js +272 -0
  21. package/src/commands/requirements/disable.js +97 -0
  22. package/src/commands/requirements/enable.js +97 -0
  23. package/src/commands/requirements/utils.js +194 -0
  24. package/src/commands/requirements-refactored.js +60 -0
  25. package/src/commands/requirements.js +38 -771
  26. package/src/commands/specs/disable.js +96 -0
  27. package/src/commands/specs/enable.js +96 -0
  28. package/src/trui/TruiInterface.js +5 -11
  29. package/src/trui/agents/AgentInterface.js +24 -396
  30. package/src/trui/agents/handlers/CommandHandler.js +93 -0
  31. package/src/trui/agents/handlers/ContextManager.js +117 -0
  32. package/src/trui/agents/handlers/DisplayHandler.js +243 -0
  33. package/src/trui/agents/handlers/HelpHandler.js +51 -0
  34. package/src/utils/auth.js +13 -111
  35. package/src/utils/config.js +5 -1
  36. package/src/utils/interactive/requirements-navigation.js +17 -15
  37. package/src/utils/interactive-broken.js +2 -2
  38. package/src/utils/provider-checker/agent-runner.js +15 -1
  39. package/src/utils/provider-checker/cli-installer.js +149 -7
  40. package/src/utils/provider-checker/opencode-checker.js +588 -0
  41. package/src/utils/provider-checker/provider-validator.js +88 -3
  42. package/src/utils/provider-checker/time-formatter.js +3 -2
  43. package/src/utils/provider-manager.js +28 -20
  44. package/src/utils/provider-registry.js +35 -3
  45. package/src/utils/requirements-navigator/index.js +94 -0
  46. package/src/utils/requirements-navigator/input-handler.js +217 -0
  47. package/src/utils/requirements-navigator/section-loader.js +188 -0
  48. package/src/utils/requirements-navigator/tree-builder.js +105 -0
  49. package/src/utils/requirements-navigator/tree-renderer.js +50 -0
  50. package/src/utils/requirements-navigator.js +2 -583
  51. package/src/utils/trui-clarifications.js +188 -0
  52. package/src/utils/trui-feedback.js +54 -1
  53. package/src/utils/trui-kiro-integration.js +398 -0
  54. package/src/utils/trui-main-handlers.js +194 -0
  55. package/src/utils/trui-main-menu.js +235 -0
  56. package/src/utils/trui-nav-agents.js +178 -25
  57. package/src/utils/trui-nav-requirements.js +203 -27
  58. package/src/utils/trui-nav-settings.js +114 -1
  59. package/src/utils/trui-nav-specifications.js +44 -3
  60. package/src/utils/trui-navigation-backup.js +603 -0
  61. package/src/utils/trui-navigation.js +70 -228
  62. package/src/utils/trui-provider-health.js +274 -0
  63. package/src/utils/trui-provider-manager.js +376 -0
  64. package/src/utils/trui-quick-menu.js +25 -1
  65. package/src/utils/trui-req-actions-backup.js +507 -0
  66. package/src/utils/trui-req-actions.js +148 -216
  67. package/src/utils/trui-req-editor.js +170 -0
  68. package/src/utils/trui-req-file-ops.js +278 -0
  69. package/src/utils/trui-req-tree-old.js +719 -0
  70. package/src/utils/trui-req-tree.js +348 -627
  71. package/src/utils/trui-specifications.js +25 -7
  72. package/src/utils/trui-windsurf.js +231 -10
  73. package/src/utils/welcome-screen-extracted.js +2 -2
  74. package/src/utils/welcome-screen.js +2 -2
@@ -0,0 +1,719 @@
1
+ /**
2
+ * TRUI Requirements Tree Navigator
3
+ *
4
+ * Shows expandable sections using showQuickMenu with letter shortcuts.
5
+ * Sections: TODO SPECIFICATIONS, Verified, To Verify, TODO REQUIREMENTS
6
+ * Each section is collapsible; requirements shown as sub-items when expanded.
7
+ */
8
+
9
+ const chalk = require('chalk');
10
+ const fs = require('fs-extra');
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ // CLI state management functions
15
+ const getStateFilePath = () => path.join(os.homedir(), '.vibecodingmachine-cli-states.json');
16
+
17
+ const saveCliStates = async (expanded, expandedSpecs) => {
18
+ try {
19
+ const states = {
20
+ expanded: expanded,
21
+ expandedSpecs: Array.from(expandedSpecs)
22
+ };
23
+ await fs.writeFile(getStateFilePath(), JSON.stringify(states, null, 2));
24
+ console.log('💾 Saved CLI list states');
25
+ } catch (error) {
26
+ console.error('❌ Error saving CLI states:', error.message);
27
+ }
28
+ };
29
+
30
+ const loadCliStates = async () => {
31
+ try {
32
+ const stateFile = getStateFilePath();
33
+ if (await fs.pathExists(stateFile)) {
34
+ const content = await fs.readFile(stateFile, 'utf8');
35
+ const states = JSON.parse(content);
36
+ console.log('📂 Loaded CLI list states');
37
+ return {
38
+ expanded: states.expanded || { specifications: false, verified: false, verify: false, todo: false, recycled: false },
39
+ expandedSpecs: new Set(states.expandedSpecs || [])
40
+ };
41
+ }
42
+ console.log('📂 No saved CLI states found, using defaults (all closed)');
43
+ return null;
44
+ } catch (error) {
45
+ console.error('❌ Error loading CLI states:', error.message);
46
+ return null;
47
+ }
48
+ };
49
+
50
+ /**
51
+
52
+ /**
53
+ * Create progress bar string with green/orange gradient based on completion percentage
54
+ * @param {number} percentage - Completion percentage (0-100)
55
+ * @param {number} done - Number of completed tasks
56
+ * @param {number} total - Total number of tasks
57
+ * @returns {string} Progress bar with colors
58
+ */
59
+ function createProgressBar(percentage, done, total) {
60
+ if (total === 0) return chalk.gray(' TODO');
61
+
62
+ const isComplete = percentage === 100;
63
+ if (isComplete) {
64
+ return chalk.bgGreen.black(` ${percentage}%, ${done}/${total} tasks complete`);
65
+ }
66
+
67
+ // For partial progress, split the text background based on actual percentage
68
+ const progressText = `${percentage}%, ${done}/${total} tasks complete`;
69
+ const textLength = progressText.length;
70
+ const splitPoint = Math.floor((percentage / 100) * textLength);
71
+
72
+ const firstHalf = progressText.substring(0, splitPoint);
73
+ const secondHalf = progressText.substring(splitPoint);
74
+
75
+ // Apply green background to percentage portion, orange to remaining
76
+ const greenHalf = chalk.bgGreen.black(firstHalf);
77
+ const orangeHalf = chalk.bgHex('#f59e0b').black(secondHalf);
78
+
79
+ return ` ${greenHalf}${orangeHalf}`;
80
+ }
81
+
82
+ // ─── Data loading (ported from old interactive.js) ───────────────────────────
83
+
84
+ /**
85
+ * Load all requirement sections using parseRequirementsFile from core.
86
+ *
87
+ * Section mapping (matches old interactive.js behaviour):
88
+ * verified (🎉) ← parsed.verified (## ✅ Verified by AI screenshot)
89
+ * verify (✅) ← parsed.completed (## 📝 VERIFIED — fully human-verified)
90
+ * todo (⏳) ← parsed.requirements (## ⏳ Requirements not yet completed)
91
+ *
92
+ * Each item is a string; we normalise to { title } objects for the tree.
93
+ */
94
+ async function loadAllSections() {
95
+ try {
96
+ const { getRequirementsPath, parseRequirementsFile } = require('vibecodingmachine-core');
97
+ const { getRepoPath } = require('./config');
98
+ const repoPath = await getRepoPath();
99
+ const reqPath = await getRequirementsPath(repoPath);
100
+ if (!reqPath || !await fs.pathExists(reqPath)) {
101
+ return { verified: [], verify: [], todo: [], recycled: [] };
102
+ }
103
+
104
+ const content = await fs.readFile(reqPath, 'utf8');
105
+ const parsed = parseRequirementsFile(content);
106
+
107
+ const toItems = arr =>
108
+ (arr || [])
109
+ .filter(r => r && (typeof r === 'string' ? r.trim() : r.title))
110
+ .map(r => typeof r === 'string' ? { title: r.trim() } : r);
111
+
112
+ return {
113
+ verified: toItems(parsed.verified), // AI-screenshot verified items
114
+ verify: toItems(parsed.completed), // fully human-verified items
115
+ todo: toItems(parsed.requirements), // not yet completed
116
+ recycled: toItems(parsed.recycled),
117
+ };
118
+ } catch (_) {
119
+ return { verified: [], verify: [], todo: [], recycled: [] };
120
+ }
121
+ }
122
+
123
+ // Keep loadSection exported for backwards compat with other modules
124
+ async function loadSection(sectionKey) {
125
+ const sections = await loadAllSections();
126
+ return sections[sectionKey] || [];
127
+ }
128
+
129
+ // ─── Menu building ────────────────────────────────────────────────────────────
130
+
131
+ function getStatusIcon(status) {
132
+ switch (status) {
133
+ case 'completed': return '✅';
134
+ case 'in-progress': return '🔄';
135
+ case 'todo': return '📋';
136
+ default: return '❓';
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Build showQuickMenu items for the requirements tree
142
+ * @param {Object} sections - requirements sections
143
+ * @param {Object} expanded - which sections are expanded
144
+ * @param {Set} [expandedSpecs] - set of expanded spec indices
145
+ * @param {Map} [specUserStories] - cache of specIdx -> story title strings
146
+ */
147
+ async function buildTreeItems(sections, expanded, expandedSpecs, specUserStories) {
148
+ const items = [];
149
+ const total = sections.verified.length + sections.verify.length + sections.todo.length;
150
+ const pct = n => total > 0 ? Math.round((n / total) * 100) : 0;
151
+
152
+ const addSection = (key, icon, label, reqs) => {
153
+ const count = reqs.length;
154
+ const isOpen = expanded[key];
155
+ const arrow = isOpen ? '▾' : '▸';
156
+ const percentage = total > 0 ? pct(count) : 0;
157
+
158
+ // Use createProgressBar for consistent colored progress bars
159
+ const progressStr = total > 0 ? ' ' + createProgressBar(percentage, count, total) : ` (${count})`;
160
+
161
+ items.push({
162
+ type: isOpen ? 'header' : 'action',
163
+ name: `${arrow} ${icon} ${label}${progressStr}`,
164
+ value: `section:${key}`,
165
+ });
166
+ if (expanded[key]) {
167
+ if (count === 0) {
168
+ items.push({ type: 'info', name: ' (empty)', value: `empty:${key}` });
169
+ } else {
170
+ reqs.forEach((req, idx) => {
171
+ const title = req.title || req;
172
+ const isDisabled = typeof title === 'string' && title.startsWith('DISABLED: ');
173
+ const displayTitle = isDisabled
174
+ ? chalk.gray(`⊘ ${title.slice('DISABLED: '.length)}`)
175
+ : title;
176
+ items.push({
177
+ type: 'action',
178
+ name: ` ${displayTitle}`,
179
+ value: `req:${key}:${idx}`,
180
+ });
181
+ });
182
+ }
183
+ }
184
+ };
185
+
186
+ // TODO SPECIFICATIONS first
187
+ try {
188
+ const { getSpecsList } = require('./trui-specifications');
189
+ const specs = await getSpecsList();
190
+ const specsCount = specs.length;
191
+
192
+ // Calculate overall specs progress
193
+ let specsTotalTasks = 0;
194
+ let specsDoneTasks = 0;
195
+ specs.forEach(spec => {
196
+ if (spec.taskTotal > 0) {
197
+ specsTotalTasks += spec.taskTotal;
198
+ specsDoneTasks += spec.taskDone;
199
+ }
200
+ });
201
+ const specsPct = specsTotalTasks > 0 ? Math.round((specsDoneTasks / specsTotalTasks) * 100) : 0;
202
+
203
+ const specsOpen = expanded.specifications;
204
+ const specsArrow = specsOpen ? '▾' : '▸';
205
+
206
+ // Use createProgressBar for consistent colored progress bars
207
+ const specsProgressStr = specsTotalTasks > 0 ? ' ' + createProgressBar(specsPct, specsDoneTasks, specsTotalTasks) : '';
208
+
209
+ items.push({
210
+ type: specsOpen ? 'header' : 'action',
211
+ name: `${specsArrow} 📋 TODO Specifications${specsProgressStr}`,
212
+ value: 'section:specifications',
213
+ });
214
+ if (specsOpen) {
215
+ if (specsCount === 0) {
216
+ items.push({ type: 'info', name: ' (no specifications found)', value: 'empty:specifications' });
217
+ } else {
218
+ specs.forEach((spec, idx) => {
219
+ let progressStr = '';
220
+ if (spec.taskTotal > 0) {
221
+ progressStr = ' ' + createProgressBar(spec.pct, spec.taskDone, spec.taskTotal);
222
+ }
223
+ const isSpecExpanded = expandedSpecs && expandedSpecs.has(idx);
224
+ const specArrow = isSpecExpanded ? '▾' : '▸';
225
+ const isDisabled = spec.disabled || spec.id.startsWith('DISABLED-');
226
+ const displayId = isDisabled ? spec.id.slice('DISABLED-'.length) : spec.id;
227
+ const specLabel = isDisabled ? chalk.gray(`⊘ ${displayId}`) : displayId;
228
+ // Spec items always keep their letter so the user can navigate to and collapse them
229
+ items.push({
230
+ type: 'action',
231
+ name: ` ${specArrow} ${specLabel}${progressStr}`,
232
+ value: `spec:${idx}`,
233
+ });
234
+ if (isSpecExpanded) {
235
+ const phases = (specUserStories && specUserStories.get(idx)) || [];
236
+ if (phases.length === 0) {
237
+ items.push({ type: 'info', name: ' (no phases found)', value: `spec-phase-empty:${idx}` });
238
+ } else {
239
+ phases.forEach((phase, pIdx) => {
240
+ const phaseTitle = typeof phase === 'string' ? phase : phase.title;
241
+ let phaseProgress = '';
242
+ if (phase && typeof phase === 'object' && phase.total > 0) {
243
+ phaseProgress = chalk.gray(` ${phase.pct}%, ${phase.done}/${phase.total}`);
244
+ }
245
+ items.push({
246
+ type: 'info',
247
+ name: ` ${chalk.gray(phaseTitle)}${phaseProgress}`,
248
+ value: `spec-phase:${idx}:${pIdx}`,
249
+ });
250
+ });
251
+ }
252
+ }
253
+ });
254
+ }
255
+ }
256
+ } catch (_) {
257
+ items.push({ type: 'header', name: '▸ 📋 TODO SPECIFICATIONS', value: 'section:specifications' });
258
+ }
259
+
260
+ addSection('verified', '🎉', 'Verified', sections.verified);
261
+ addSection('verify', '✅', 'To Verify', sections.verify);
262
+
263
+ // Calculate requirements progress
264
+ let reqsTotalTasks = 0;
265
+ let reqsDoneTasks = 0;
266
+ if (sections.todo && sections.todo.length > 0) {
267
+ sections.todo.forEach(req => {
268
+ if (req.total > 0) {
269
+ reqsTotalTasks += req.total;
270
+ reqsDoneTasks += req.done;
271
+ }
272
+ });
273
+ }
274
+ const reqsPct = reqsTotalTasks > 0 ? Math.round((reqsDoneTasks / reqsTotalTasks) * 100) : 0;
275
+
276
+ // Use createProgressBar for consistent colored progress bars
277
+ const reqsProgressStr = reqsTotalTasks > 0 ? ' ' + createProgressBar(reqsPct, reqsDoneTasks, reqsTotalTasks) : '';
278
+
279
+ const reqsTitle = reqsTotalTasks > 0
280
+ ? `TODO Requirements${reqsProgressStr}`
281
+ : 'TODO Requirements';
282
+
283
+ addSection('todo', '⏳', reqsTitle, sections.todo);
284
+
285
+ if (sections.recycled && sections.recycled.length > 0) {
286
+ addSection('recycled', '♻️', 'RECYCLED', sections.recycled);
287
+ }
288
+
289
+ return items;
290
+ }
291
+
292
+ // ─── Main navigator ───────────────────────────────────────────────────────────
293
+
294
+ async function showRequirementsTree() {
295
+ const { showQuickMenu } = require('./trui-quick-menu');
296
+ const { showRequirementActions, addRequirementFlow, _moveRequirement, _deleteRequirement } = require('./trui-req-actions');
297
+ const { getOrCreateRequirementsFilePath, getRequirementsPath } = require('vibecodingmachine-core');
298
+ const { getRepoPath } = require('./config');
299
+
300
+ // Toggle all items functionality
301
+ const extraKeys = (str, key, selectedIndex, context) => {
302
+ if (str === '*' || str === '8') {
303
+ context.resolveWith({ value: 'toggle-all', selectedIndex });
304
+ return true;
305
+ }
306
+ return false;
307
+ };
308
+
309
+ // Resolve requirements file path exactly like loadAllSections does (getRequirementsPath,
310
+ // NOT getOrCreate) so reads and writes always go to the same file regardless of
311
+ // whether it lives inside the repo or in a sibling directory.
312
+ const repoPath = await getRepoPath();
313
+ const getReqPath = async () => {
314
+ // Mirror loadAllSections: call getRequirementsPath with repoPath (even null) first
315
+ const p = await getRequirementsPath(repoPath);
316
+ if (p && await fs.pathExists(p)) return p;
317
+ // File doesn't exist yet — create it (first-time use).
318
+ // Use effectiveRepoPath (git root) so .vibecodingmachine is never created inside
319
+ // a sub-package directory when repoPath is null.
320
+ const { getEffectiveRepoPath } = require('./config');
321
+ const effectiveRepoPath = await getEffectiveRepoPath();
322
+ return getOrCreateRequirementsFilePath(effectiveRepoPath);
323
+ };
324
+
325
+ const expanded = { specifications: false, verified: false, verify: false, todo: false, recycled: false };
326
+ const expandedSpecs = new Set(); // indices of expanded spec items
327
+ const specPhases = new Map(); // specIdx -> string[]
328
+ let sections = await loadAllSections();
329
+ let lastIndex = 0;
330
+
331
+ // Load saved states from file
332
+ const savedStates = await loadCliStates();
333
+ if (savedStates) {
334
+ Object.assign(expanded, savedStates.expanded);
335
+ savedStates.expandedSpecs.forEach(idx => expandedSpecs.add(idx));
336
+ }
337
+
338
+ const printHeader = () => {
339
+ console.clear();
340
+ process.stdout.write(chalk.bold.cyan('📋 Requirements\n\n'));
341
+ };
342
+
343
+ printHeader();
344
+
345
+ while (true) {
346
+ const items = await buildTreeItems(sections, expanded, expandedSpecs, specPhases);
347
+ const hintText = '[→ expand ← collapse/back < move item down > move item up - delete + add Space toggle]';
348
+
349
+ const extraKeys = (str, key, selectedIndex, { resolveWith }) => {
350
+ const keyName = key && key.name;
351
+
352
+ // = or + → add requirement (works anywhere)
353
+ if (str === '=' || str === '+') {
354
+ resolveWith('add-req');
355
+ return true;
356
+ }
357
+
358
+ // → right arrow: expand sections and specs (non-toggling — only opens, never closes)
359
+ if (keyName === 'right') {
360
+ const item = items[selectedIndex];
361
+ if (item && item.value) {
362
+ if (item.value.startsWith('section:')) {
363
+ const sectionKey = item.value.slice('section:'.length);
364
+ if (!expanded[sectionKey]) {
365
+ resolveWith(`expand-section:${sectionKey}`);
366
+ }
367
+ return true; // consume even if already expanded
368
+ }
369
+ if (item.value.startsWith('spec:')) {
370
+ const specIdx = parseInt(item.value.split(':')[1], 10);
371
+ if (!expandedSpecs.has(specIdx)) {
372
+ resolveWith(`expand-spec:${specIdx}`);
373
+ }
374
+ return true; // consume even if already expanded
375
+ }
376
+ }
377
+ return false; // let right arrow act as Enter for other items
378
+ }
379
+
380
+ // ← left arrow: collapse expanded sections/specs; otherwise pass through (→ back)
381
+ if (keyName === 'left') {
382
+ const item = items[selectedIndex];
383
+ if (item && item.value) {
384
+ // On a collapsed section header → go back (fall through)
385
+ if (item.value.startsWith('section:')) {
386
+ const sectionKey = item.value.slice('section:'.length);
387
+ if (expanded[sectionKey]) {
388
+ resolveWith(`collapse-section:${sectionKey}`);
389
+ return true;
390
+ }
391
+ }
392
+ // On a spec item inside expanded SPECIFICATIONS section
393
+ if (item.value.startsWith('spec:')) {
394
+ const specIdx = parseInt(item.value.split(':')[1], 10);
395
+ if (expandedSpecs.has(specIdx)) {
396
+ // Spec is expanded (showing phases) → collapse phases first
397
+ resolveWith(`collapse-spec:${specIdx}`);
398
+ return true;
399
+ }
400
+ // Spec not expanded → collapse parent SPECIFICATIONS section
401
+ if (expanded.specifications) {
402
+ resolveWith('collapse-section:specifications');
403
+ return true;
404
+ }
405
+ }
406
+ // On a req item → collapse its parent section
407
+ if (item.value.startsWith('req:')) {
408
+ const sectionKey = item.value.split(':')[1];
409
+ if (expanded[sectionKey]) {
410
+ resolveWith(`collapse-section:${sectionKey}`);
411
+ return true;
412
+ }
413
+ }
414
+ }
415
+ return false; // not on a sub-item → default cancel (go back)
416
+ }
417
+
418
+ // Space → toggle req or spec enabled/disabled
419
+ if (str === ' ' || keyName === 'space') {
420
+ const item = items[selectedIndex];
421
+ if (item && item.value && item.value.startsWith('req:')) {
422
+ resolveWith(`toggle-disabled:${selectedIndex}`);
423
+ return true;
424
+ }
425
+ if (item && item.value && item.value.startsWith('spec:')) {
426
+ resolveWith(`toggle-disabled-spec:${selectedIndex}`);
427
+ return true;
428
+ }
429
+ return true; // consume silently on other items
430
+ }
431
+
432
+ // , or < → move requirement down
433
+ // . or > → move requirement up
434
+ // - or _ or Delete key → delete requirement
435
+ const isDown = str === ',' || str === '<' || keyName === ',' || keyName === '<';
436
+ const isUp = str === '.' || str === '>' || keyName === '.' || keyName === '>';
437
+ const isDelete = str === '-' || str === '_' || keyName === '-' || keyName === '_' ||
438
+ (key && (key.name === 'delete' || key.name === 'backspace'));
439
+ if (isDown || isUp || isDelete) {
440
+ const item = items[selectedIndex];
441
+ if (item && item.value && item.value.startsWith('req:')) {
442
+ const action = isDown ? ',' : isUp ? '.' : '-';
443
+ resolveWith(`jkd:${action}:${selectedIndex}`);
444
+ return true;
445
+ }
446
+ return true; // consume the key even when not on a req (no-op, avoids confusion)
447
+ }
448
+
449
+ return false;
450
+ };
451
+
452
+ const result = await showQuickMenu(items, lastIndex, { extraKeys, hintText });
453
+ lastIndex = result.selectedIndex;
454
+ const value = result.value;
455
+
456
+ if (value === '__cancel__' || value === 'exit') break;
457
+
458
+ if (value === 'add-req') {
459
+ try { await addRequirementFlow(); } catch (_) {}
460
+ sections = await loadAllSections();
461
+ printHeader();
462
+ continue;
463
+ }
464
+
465
+ // Handle Space: toggle requirement enabled/disabled
466
+ if (value.startsWith('toggle-disabled:')) {
467
+ const itemIdx = parseInt(value.split(':')[1], 10);
468
+ const item = items[itemIdx];
469
+ if (item && item.value && item.value.startsWith('req:')) {
470
+ const reqParts = item.value.split(':');
471
+ const sectionKey = reqParts[1];
472
+ const reqIdx = parseInt(reqParts[2], 10);
473
+ const req = sections[sectionKey] && sections[sectionKey][reqIdx];
474
+ if (req) {
475
+ try {
476
+ const reqPath = await getReqPath();
477
+ const content = await fs.readFile(reqPath, 'utf8');
478
+ const title = req.title;
479
+ let newContent;
480
+ if (title.startsWith('DISABLED: ')) {
481
+ // Enable: remove DISABLED: prefix
482
+ const cleanTitle = title.slice('DISABLED: '.length);
483
+ const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
484
+ newContent = content.replace(new RegExp(`^(###\\s*)${escaped}\\s*$`, 'm'), `$1${cleanTitle}`);
485
+ } else {
486
+ // Disable: add DISABLED: prefix
487
+ const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
488
+ newContent = content.replace(new RegExp(`^(###\\s*)${escaped}\\s*$`, 'm'), `$1DISABLED: ${title}`);
489
+ }
490
+ if (newContent !== content) {
491
+ await fs.writeFile(reqPath, newContent, 'utf8');
492
+ sections = await loadAllSections();
493
+ }
494
+ } catch (_) {}
495
+ }
496
+ }
497
+ printHeader();
498
+ continue;
499
+ }
500
+
501
+ // Handle */8: toggle all items
502
+ if (value === 'toggle-all') {
503
+ try {
504
+ const reqPath = await getReqPath();
505
+ let content = await fs.readFile(reqPath, 'utf8');
506
+ let hasChanges = false;
507
+
508
+ // Toggle all requirements
509
+ if (sections.todo && sections.todo.length > 0) {
510
+ sections.todo.forEach(req => {
511
+ const title = req.title;
512
+ if (!title.startsWith('DISABLED: ')) {
513
+ // Disable all
514
+ const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
515
+ content = content.replace(new RegExp(`^(###\\s*)${escaped}\\s*$`, 'm'), `$1DISABLED: ${title}`);
516
+ hasChanges = true;
517
+ }
518
+ });
519
+ }
520
+
521
+ // Toggle all specifications
522
+ const { getSpecsList } = require('./trui-specifications');
523
+ const specs = await getSpecsList();
524
+ for (const spec of specs) {
525
+ if (spec.taskTotal > 0) {
526
+ const specDir = spec.path || path.join(await getRepoPath(), 'specs', spec.directory);
527
+ const tasksMdPath = path.join(specDir, 'tasks.md');
528
+ if (await fs.pathExists(tasksMdPath)) {
529
+ let specContent = await fs.readFile(tasksMdPath, 'utf8');
530
+ const specTitle = spec.title || spec.directory;
531
+ if (!specContent.includes('DISABLED:')) {
532
+ // Disable spec
533
+ specContent = `DISABLED: ${specTitle}\n${specContent}`;
534
+ hasChanges = true;
535
+ } else {
536
+ // Enable spec
537
+ specContent = specContent.replace(/^DISABLED: .+\n/, '');
538
+ hasChanges = true;
539
+ }
540
+ if (hasChanges) {
541
+ await fs.writeFile(tasksMdPath, specContent, 'utf8');
542
+ }
543
+ }
544
+ }
545
+ }
546
+
547
+ if (hasChanges) {
548
+ sections = await loadAllSections();
549
+ }
550
+ } catch (_) {}
551
+ printHeader();
552
+ continue;
553
+ }
554
+
555
+ // Handle Space: toggle spec enabled/disabled (rename directory DISABLED- prefix)
556
+ if (value.startsWith('toggle-disabled-spec:')) {
557
+ const itemIdx = parseInt(value.split(':')[1], 10);
558
+ const item = items[itemIdx];
559
+ if (item && item.value && item.value.startsWith('spec:')) {
560
+ const specIdx = parseInt(item.value.split(':')[1], 10);
561
+ try {
562
+ const { getSpecsList } = require('./trui-specifications');
563
+ const specs = await getSpecsList();
564
+ const spec = specs[specIdx];
565
+ if (spec && spec.path) {
566
+ const parentDir = path.dirname(spec.path);
567
+ const dirName = path.basename(spec.path);
568
+ const newDirName = dirName.startsWith('DISABLED-')
569
+ ? dirName.slice('DISABLED-'.length)
570
+ : 'DISABLED-' + dirName;
571
+ await fs.rename(spec.path, path.join(parentDir, newDirName));
572
+ // Clear spec caches — indices may shift after rename changes sort order
573
+ expandedSpecs.clear();
574
+ specPhases.clear();
575
+ }
576
+ } catch (_) {}
577
+ }
578
+ printHeader();
579
+ continue;
580
+ }
581
+
582
+ // Handle j/k/d inline shortcuts
583
+ if (value.startsWith('jkd:')) {
584
+ const parts = value.split(':');
585
+ const action = parts[1];
586
+ const itemIdx = parseInt(parts[2], 10);
587
+ const item = items[itemIdx];
588
+ if (item && item.value && item.value.startsWith('req:')) {
589
+ const reqParts = item.value.split(':');
590
+ const sectionKey = reqParts[1];
591
+ const reqIdx = parseInt(reqParts[2], 10);
592
+ const req = sections[sectionKey] && sections[sectionKey][reqIdx];
593
+ if (req) {
594
+ try {
595
+ const reqPath = await getReqPath();
596
+ if (action === ',') {
597
+ const moved = await _moveRequirement(reqPath, req.title, sectionKey, 'down');
598
+ if (moved) { sections = await loadAllSections(); lastIndex = Math.min(lastIndex + 1, items.length - 1); }
599
+ } else if (action === '.') {
600
+ const moved = await _moveRequirement(reqPath, req.title, sectionKey, 'up');
601
+ if (moved) { sections = await loadAllSections(); lastIndex = Math.max(lastIndex - 1, 0); }
602
+ } else if (action === '-') {
603
+ console.clear();
604
+ const deleted = await _deleteRequirement(reqPath, req.title);
605
+ if (deleted) { sections = await loadAllSections(); lastIndex = Math.max(lastIndex - 1, 0); }
606
+ }
607
+ } catch (_) {}
608
+ }
609
+ }
610
+ printHeader();
611
+ continue;
612
+ }
613
+
614
+ // Enter on section header → toggle; cursor auto-advances on open, returns to header on close
615
+ if (value.startsWith('section:')) {
616
+ const key = value.slice('section:'.length);
617
+ expanded[key] = !expanded[key];
618
+ await saveCliStates(expanded, expandedSpecs);
619
+ if (!expanded[key]) {
620
+ // Closing: position cursor back on header
621
+ const newItems = await buildTreeItems(sections, expanded, expandedSpecs, specPhases);
622
+ const headerIdx = newItems.findIndex(item => item.value === `section:${key}`);
623
+ if (headerIdx !== -1) lastIndex = headerIdx;
624
+ }
625
+ printHeader();
626
+ continue;
627
+ }
628
+
629
+ if (value.startsWith('req:')) {
630
+ const parts = value.split(':');
631
+ const sectionKey = parts[1];
632
+ const idx = parseInt(parts[2], 10);
633
+ const req = sections[sectionKey] && sections[sectionKey][idx];
634
+ if (req) {
635
+ console.clear();
636
+ try {
637
+ const changed = await showRequirementActions(req, sectionKey);
638
+ if (changed) sections = await loadAllSections();
639
+ } catch (_) {}
640
+ printHeader();
641
+ }
642
+ continue;
643
+ }
644
+
645
+ // Expand section (right arrow)
646
+ if (value.startsWith('expand-section:')) {
647
+ const key = value.slice('expand-section:'.length);
648
+ expanded[key] = true;
649
+ await saveCliStates(expanded, expandedSpecs);
650
+ printHeader();
651
+ continue;
652
+ }
653
+
654
+ // Collapse section — position cursor back on the section header
655
+ if (value.startsWith('collapse-section:')) {
656
+ const key = value.slice('collapse-section:'.length);
657
+ expanded[key] = false;
658
+ await saveCliStates(expanded, expandedSpecs);
659
+ // Rebuild to find the now-selectable (action) section header position
660
+ const newItems = await buildTreeItems(sections, expanded, expandedSpecs, specPhases);
661
+ const headerIdx = newItems.findIndex(item => item.value === `section:${key}`);
662
+ if (headerIdx !== -1) lastIndex = headerIdx;
663
+ printHeader();
664
+ continue;
665
+ }
666
+
667
+ // Expand spec phases (right arrow on spec item) — specIdx is the spec array index
668
+ if (value.startsWith('expand-spec:')) {
669
+ const specIdx = parseInt(value.split(':')[1], 10);
670
+ expandedSpecs.add(specIdx);
671
+ await saveCliStates(expanded, expandedSpecs);
672
+ if (!specPhases.has(specIdx)) {
673
+ try {
674
+ const { getSpecsList, extractPhases } = require('./trui-specifications');
675
+ const specs = await getSpecsList();
676
+ const spec = specs[specIdx];
677
+ if (spec && spec.path) specPhases.set(specIdx, extractPhases(spec.path));
678
+ } catch (_) {}
679
+ }
680
+ printHeader();
681
+ continue;
682
+ }
683
+
684
+ // Collapse spec phases — position cursor back on the spec item
685
+ if (value.startsWith('collapse-spec:')) {
686
+ const specIdx = parseInt(value.split(':')[1], 10);
687
+ expandedSpecs.delete(specIdx);
688
+ await saveCliStates(expanded, expandedSpecs);
689
+ // Rebuild to find the now-selectable spec item position
690
+ const newItems = await buildTreeItems(sections, expanded, expandedSpecs, specPhases);
691
+ const specItemIdx = newItems.findIndex(item => item.value === `spec:${specIdx}`);
692
+ if (specItemIdx !== -1) lastIndex = specItemIdx;
693
+ printHeader();
694
+ continue;
695
+ }
696
+
697
+ // Enter on spec item → toggle expansion
698
+ if (value.startsWith('spec:')) {
699
+ const specIdx = parseInt(value.split(':')[1], 10);
700
+ if (expandedSpecs.has(specIdx)) {
701
+ expandedSpecs.delete(specIdx);
702
+ } else {
703
+ expandedSpecs.add(specIdx);
704
+ if (!specPhases.has(specIdx)) {
705
+ try {
706
+ const { getSpecsList, extractPhases } = require('./trui-specifications');
707
+ const specs = await getSpecsList();
708
+ const spec = specs[specIdx];
709
+ if (spec && spec.path) specPhases.set(specIdx, extractPhases(spec.path));
710
+ } catch (_) {}
711
+ }
712
+ }
713
+ printHeader();
714
+ continue;
715
+ }
716
+ }
717
+ }
718
+
719
+ module.exports = { showRequirementsTree, loadSection, loadAllSections, buildTreeItems };