vibecodingmachine-cli 2026.1.29-713 → 2026.2.20-423

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 (45) hide show
  1. package/bin/vibecodingmachine.js +124 -0
  2. package/package.json +3 -2
  3. package/src/commands/agents-check.js +69 -0
  4. package/src/commands/auto-direct.js +930 -145
  5. package/src/commands/auto.js +26 -4
  6. package/src/commands/ide.js +2 -1
  7. package/src/commands/requirements.js +23 -27
  8. package/src/utils/auto-mode.js +4 -1
  9. package/src/utils/cline-js-handler.js +218 -0
  10. package/src/utils/config.js +22 -0
  11. package/src/utils/display-formatters-complete.js +229 -0
  12. package/src/utils/display-formatters-extracted.js +219 -0
  13. package/src/utils/display-formatters.js +157 -0
  14. package/src/utils/feedback-handler.js +143 -0
  15. package/src/utils/ide-detection-complete.js +126 -0
  16. package/src/utils/ide-detection-extracted.js +116 -0
  17. package/src/utils/ide-detection.js +124 -0
  18. package/src/utils/interactive-backup.js +5664 -0
  19. package/src/utils/interactive-broken.js +280 -0
  20. package/src/utils/interactive.js +31 -5534
  21. package/src/utils/provider-checker.js +410 -0
  22. package/src/utils/provider-manager.js +251 -0
  23. package/src/utils/provider-registry.js +18 -9
  24. package/src/utils/requirement-actions.js +884 -0
  25. package/src/utils/requirements-navigator.js +585 -0
  26. package/src/utils/rui-trui-adapter.js +311 -0
  27. package/src/utils/simple-trui.js +204 -0
  28. package/src/utils/status-helpers-extracted.js +125 -0
  29. package/src/utils/status-helpers.js +107 -0
  30. package/src/utils/trui-debug.js +261 -0
  31. package/src/utils/trui-feedback.js +133 -0
  32. package/src/utils/trui-nav-agents.js +119 -0
  33. package/src/utils/trui-nav-requirements.js +268 -0
  34. package/src/utils/trui-nav-settings.js +157 -0
  35. package/src/utils/trui-nav-specifications.js +139 -0
  36. package/src/utils/trui-navigation.js +303 -0
  37. package/src/utils/trui-provider-manager.js +182 -0
  38. package/src/utils/trui-quick-menu.js +365 -0
  39. package/src/utils/trui-req-actions.js +372 -0
  40. package/src/utils/trui-req-tree.js +534 -0
  41. package/src/utils/trui-specifications.js +359 -0
  42. package/src/utils/trui-text-editor.js +350 -0
  43. package/src/utils/trui-windsurf.js +336 -0
  44. package/src/utils/welcome-screen-extracted.js +135 -0
  45. package/src/utils/welcome-screen.js +134 -0
@@ -0,0 +1,534 @@
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
+
13
+ // ─── Data loading (ported from old interactive.js) ───────────────────────────
14
+
15
+ /**
16
+ * Load all requirement sections using parseRequirementsFile from core.
17
+ *
18
+ * Section mapping (matches old interactive.js behaviour):
19
+ * verified (🎉) ← parsed.verified (## ✅ Verified by AI screenshot)
20
+ * verify (✅) ← parsed.completed (## 📝 VERIFIED — fully human-verified)
21
+ * todo (⏳) ← parsed.requirements (## ⏳ Requirements not yet completed)
22
+ *
23
+ * Each item is a string; we normalise to { title } objects for the tree.
24
+ */
25
+ async function loadAllSections() {
26
+ try {
27
+ const { getRequirementsPath, parseRequirementsFile } = require('vibecodingmachine-core');
28
+ const { getRepoPath } = require('./config');
29
+ const repoPath = await getRepoPath();
30
+ const reqPath = await getRequirementsPath(repoPath);
31
+ if (!reqPath || !await fs.pathExists(reqPath)) {
32
+ return { verified: [], verify: [], todo: [], recycled: [] };
33
+ }
34
+
35
+ const content = await fs.readFile(reqPath, 'utf8');
36
+ const parsed = parseRequirementsFile(content);
37
+
38
+ const toItems = arr =>
39
+ (arr || [])
40
+ .filter(r => r && (typeof r === 'string' ? r.trim() : r.title))
41
+ .map(r => typeof r === 'string' ? { title: r.trim() } : r);
42
+
43
+ return {
44
+ verified: toItems(parsed.verified), // AI-screenshot verified items
45
+ verify: toItems(parsed.completed), // fully human-verified items
46
+ todo: toItems(parsed.requirements), // not yet completed
47
+ recycled: toItems(parsed.recycled),
48
+ };
49
+ } catch (_) {
50
+ return { verified: [], verify: [], todo: [], recycled: [] };
51
+ }
52
+ }
53
+
54
+ // Keep loadSection exported for backwards compat with other modules
55
+ async function loadSection(sectionKey) {
56
+ const sections = await loadAllSections();
57
+ return sections[sectionKey] || [];
58
+ }
59
+
60
+ // ─── Menu building ────────────────────────────────────────────────────────────
61
+
62
+ function getStatusIcon(status) {
63
+ switch (status) {
64
+ case 'completed': return '✅';
65
+ case 'in-progress': return '🔄';
66
+ case 'todo': return '📋';
67
+ default: return '❓';
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Build showQuickMenu items for the requirements tree
73
+ * @param {Object} sections - requirements sections
74
+ * @param {Object} expanded - which sections are expanded
75
+ * @param {Set} [expandedSpecs] - set of expanded spec indices
76
+ * @param {Map} [specUserStories] - cache of specIdx -> story title strings
77
+ */
78
+ async function buildTreeItems(sections, expanded, expandedSpecs, specUserStories) {
79
+ const items = [];
80
+ const total = sections.verified.length + sections.verify.length + sections.todo.length;
81
+ const pct = n => total > 0 ? Math.round((n / total) * 100) : 0;
82
+
83
+ const addSection = (key, icon, label, reqs) => {
84
+ const count = reqs.length;
85
+ const isOpen = expanded[key];
86
+ const arrow = isOpen ? '▾' : '▸';
87
+ items.push({
88
+ type: isOpen ? 'header' : 'action',
89
+ name: `${arrow} ${icon} ${label} (${count}${total > 0 ? ' — ' + pct(count) + '%' : ''})`,
90
+ value: `section:${key}`,
91
+ });
92
+ if (expanded[key]) {
93
+ if (count === 0) {
94
+ items.push({ type: 'info', name: ' (empty)', value: `empty:${key}` });
95
+ } else {
96
+ reqs.forEach((req, idx) => {
97
+ const title = req.title || req;
98
+ const isDisabled = typeof title === 'string' && title.startsWith('DISABLED: ');
99
+ const displayTitle = isDisabled
100
+ ? chalk.gray(`⊘ ${title.slice('DISABLED: '.length)}`)
101
+ : title;
102
+ items.push({
103
+ type: 'action',
104
+ name: ` ${displayTitle}`,
105
+ value: `req:${key}:${idx}`,
106
+ });
107
+ });
108
+ }
109
+ }
110
+ };
111
+
112
+ // TODO SPECIFICATIONS first
113
+ try {
114
+ const { getSpecsList } = require('./trui-specifications');
115
+ const specs = await getSpecsList();
116
+ const specsCount = specs.length;
117
+ const specsOpen = expanded.specifications;
118
+ const specsArrow = specsOpen ? '▾' : '▸';
119
+ items.push({
120
+ type: specsOpen ? 'header' : 'action',
121
+ name: `${specsArrow} 📋 TODO SPECIFICATIONS (${specsCount})`,
122
+ value: 'section:specifications',
123
+ });
124
+ if (specsOpen) {
125
+ if (specsCount === 0) {
126
+ items.push({ type: 'info', name: ' (no specifications found)', value: 'empty:specifications' });
127
+ } else {
128
+ specs.forEach((spec, idx) => {
129
+ let progressStr = '';
130
+ if (spec.taskTotal > 0) {
131
+ progressStr = chalk.gray(` [${spec.pct}%, ${spec.taskDone}/${spec.taskTotal} tasks complete]`);
132
+ }
133
+ const isSpecExpanded = expandedSpecs && expandedSpecs.has(idx);
134
+ const specArrow = isSpecExpanded ? '▾' : '▸';
135
+ const isDisabled = spec.disabled || spec.id.startsWith('DISABLED-');
136
+ const displayId = isDisabled ? spec.id.slice('DISABLED-'.length) : spec.id;
137
+ const specLabel = isDisabled ? chalk.gray(`⊘ ${displayId}`) : displayId;
138
+ // Spec items always keep their letter so the user can navigate to and collapse them
139
+ items.push({
140
+ type: 'action',
141
+ name: ` ${specArrow} ${specLabel}${progressStr}`,
142
+ value: `spec:${idx}`,
143
+ });
144
+ if (isSpecExpanded) {
145
+ const phases = (specUserStories && specUserStories.get(idx)) || [];
146
+ if (phases.length === 0) {
147
+ items.push({ type: 'info', name: ' (no phases found)', value: `spec-phase-empty:${idx}` });
148
+ } else {
149
+ phases.forEach((phase, pIdx) => {
150
+ const phaseTitle = typeof phase === 'string' ? phase : phase.title;
151
+ let phaseProgress = '';
152
+ if (phase && typeof phase === 'object' && phase.total > 0) {
153
+ phaseProgress = chalk.gray(` [${phase.pct}%, ${phase.done}/${phase.total}]`);
154
+ }
155
+ items.push({
156
+ type: 'info',
157
+ name: ` ${chalk.gray(phaseTitle)}${phaseProgress}`,
158
+ value: `spec-phase:${idx}:${pIdx}`,
159
+ });
160
+ });
161
+ }
162
+ }
163
+ });
164
+ }
165
+ }
166
+ } catch (_) {
167
+ items.push({ type: 'header', name: '▸ 📋 TODO SPECIFICATIONS', value: 'section:specifications' });
168
+ }
169
+
170
+ addSection('verified', '🎉', 'VERIFIED', sections.verified);
171
+ addSection('verify', '✅', 'TO VERIFY', sections.verify);
172
+ addSection('todo', '⏳', 'TODO REQUIREMENTS', sections.todo);
173
+
174
+ if (sections.recycled && sections.recycled.length > 0) {
175
+ addSection('recycled', '♻️', 'RECYCLED', sections.recycled);
176
+ }
177
+
178
+ return items;
179
+ }
180
+
181
+ // ─── Main navigator ───────────────────────────────────────────────────────────
182
+
183
+ async function showRequirementsTree() {
184
+ const { showQuickMenu } = require('./trui-quick-menu');
185
+ const { showRequirementActions, addRequirementFlow, _moveRequirement, _deleteRequirement } = require('./trui-req-actions');
186
+ const { getOrCreateRequirementsFilePath, getRequirementsPath } = require('vibecodingmachine-core');
187
+ const { getRepoPath } = require('./config');
188
+
189
+ // Resolve requirements file path exactly like loadAllSections does (getRequirementsPath,
190
+ // NOT getOrCreate) so reads and writes always go to the same file regardless of
191
+ // whether it lives inside the repo or in a sibling directory.
192
+ const repoPath = await getRepoPath();
193
+ const getReqPath = async () => {
194
+ // Mirror loadAllSections: call getRequirementsPath with repoPath (even null) first
195
+ const p = await getRequirementsPath(repoPath);
196
+ if (p && await fs.pathExists(p)) return p;
197
+ // File doesn't exist yet — create it (first-time use).
198
+ // Use effectiveRepoPath (git root) so .vibecodingmachine is never created inside
199
+ // a sub-package directory when repoPath is null.
200
+ const { getEffectiveRepoPath } = require('./config');
201
+ const effectiveRepoPath = await getEffectiveRepoPath();
202
+ return getOrCreateRequirementsFilePath(effectiveRepoPath);
203
+ };
204
+
205
+ const expanded = { specifications: false, verified: false, verify: false, todo: true, recycled: false };
206
+ const expandedSpecs = new Set(); // indices of expanded spec items
207
+ const specPhases = new Map(); // specIdx -> string[]
208
+ let sections = await loadAllSections();
209
+ let lastIndex = 0;
210
+
211
+ const printHeader = () => {
212
+ console.clear();
213
+ process.stdout.write(chalk.bold.cyan('📋 Requirements\n\n'));
214
+ };
215
+
216
+ printHeader();
217
+
218
+ while (true) {
219
+ const items = await buildTreeItems(sections, expanded, expandedSpecs, specPhases);
220
+ const hintText = '[→ expand ← collapse/back < move item down > move item up - delete + add Space toggle, NO SHIFT needed]';
221
+
222
+ const extraKeys = (str, key, selectedIndex, { resolveWith }) => {
223
+ const keyName = key && key.name;
224
+
225
+ // = or + → add requirement (works anywhere)
226
+ if (str === '=' || str === '+') {
227
+ resolveWith('add-req');
228
+ return true;
229
+ }
230
+
231
+ // → right arrow: expand sections and specs (non-toggling — only opens, never closes)
232
+ if (keyName === 'right') {
233
+ const item = items[selectedIndex];
234
+ if (item && item.value) {
235
+ if (item.value.startsWith('section:')) {
236
+ const sectionKey = item.value.slice('section:'.length);
237
+ if (!expanded[sectionKey]) {
238
+ resolveWith(`expand-section:${sectionKey}`);
239
+ }
240
+ return true; // consume even if already expanded
241
+ }
242
+ if (item.value.startsWith('spec:')) {
243
+ const specIdx = parseInt(item.value.split(':')[1], 10);
244
+ if (!expandedSpecs.has(specIdx)) {
245
+ resolveWith(`expand-spec:${specIdx}`);
246
+ }
247
+ return true; // consume even if already expanded
248
+ }
249
+ }
250
+ return false; // let right arrow act as Enter for other items
251
+ }
252
+
253
+ // ← left arrow: collapse expanded sections/specs; otherwise pass through (→ back)
254
+ if (keyName === 'left') {
255
+ const item = items[selectedIndex];
256
+ if (item && item.value) {
257
+ // On a collapsed section header → go back (fall through)
258
+ if (item.value.startsWith('section:')) {
259
+ const sectionKey = item.value.slice('section:'.length);
260
+ if (expanded[sectionKey]) {
261
+ resolveWith(`collapse-section:${sectionKey}`);
262
+ return true;
263
+ }
264
+ }
265
+ // On a spec item inside expanded SPECIFICATIONS section
266
+ if (item.value.startsWith('spec:')) {
267
+ const specIdx = parseInt(item.value.split(':')[1], 10);
268
+ if (expandedSpecs.has(specIdx)) {
269
+ // Spec is expanded (showing phases) → collapse phases first
270
+ resolveWith(`collapse-spec:${specIdx}`);
271
+ return true;
272
+ }
273
+ // Spec not expanded → collapse parent SPECIFICATIONS section
274
+ if (expanded.specifications) {
275
+ resolveWith('collapse-section:specifications');
276
+ return true;
277
+ }
278
+ }
279
+ // On a req item → collapse its parent section
280
+ if (item.value.startsWith('req:')) {
281
+ const sectionKey = item.value.split(':')[1];
282
+ if (expanded[sectionKey]) {
283
+ resolveWith(`collapse-section:${sectionKey}`);
284
+ return true;
285
+ }
286
+ }
287
+ }
288
+ return false; // not on a sub-item → default cancel (go back)
289
+ }
290
+
291
+ // Space → toggle req or spec enabled/disabled
292
+ if (str === ' ' || keyName === 'space') {
293
+ const item = items[selectedIndex];
294
+ if (item && item.value && item.value.startsWith('req:')) {
295
+ resolveWith(`toggle-disabled:${selectedIndex}`);
296
+ return true;
297
+ }
298
+ if (item && item.value && item.value.startsWith('spec:')) {
299
+ resolveWith(`toggle-disabled-spec:${selectedIndex}`);
300
+ return true;
301
+ }
302
+ return true; // consume silently on other items
303
+ }
304
+
305
+ // , or < → move requirement down
306
+ // . or > → move requirement up
307
+ // - or _ or Delete key → delete requirement
308
+ const isDown = str === ',' || str === '<' || keyName === ',' || keyName === '<';
309
+ const isUp = str === '.' || str === '>' || keyName === '.' || keyName === '>';
310
+ const isDelete = str === '-' || str === '_' || keyName === '-' || keyName === '_' ||
311
+ (key && (key.name === 'delete' || key.name === 'backspace'));
312
+ if (isDown || isUp || isDelete) {
313
+ const item = items[selectedIndex];
314
+ if (item && item.value && item.value.startsWith('req:')) {
315
+ const action = isDown ? ',' : isUp ? '.' : '-';
316
+ resolveWith(`jkd:${action}:${selectedIndex}`);
317
+ return true;
318
+ }
319
+ return true; // consume the key even when not on a req (no-op, avoids confusion)
320
+ }
321
+
322
+ return false;
323
+ };
324
+
325
+ const result = await showQuickMenu(items, lastIndex, { extraKeys, hintText });
326
+ lastIndex = result.selectedIndex;
327
+ const value = result.value;
328
+
329
+ if (value === '__cancel__' || value === 'exit') break;
330
+
331
+ if (value === 'add-req') {
332
+ try { await addRequirementFlow(); } catch (_) {}
333
+ sections = await loadAllSections();
334
+ printHeader();
335
+ continue;
336
+ }
337
+
338
+ // Handle Space: toggle requirement enabled/disabled
339
+ if (value.startsWith('toggle-disabled:')) {
340
+ const itemIdx = parseInt(value.split(':')[1], 10);
341
+ const item = items[itemIdx];
342
+ if (item && item.value && item.value.startsWith('req:')) {
343
+ const reqParts = item.value.split(':');
344
+ const sectionKey = reqParts[1];
345
+ const reqIdx = parseInt(reqParts[2], 10);
346
+ const req = sections[sectionKey] && sections[sectionKey][reqIdx];
347
+ if (req) {
348
+ try {
349
+ const reqPath = await getReqPath();
350
+ const content = await fs.readFile(reqPath, 'utf8');
351
+ const title = req.title;
352
+ let newContent;
353
+ if (title.startsWith('DISABLED: ')) {
354
+ // Enable: remove DISABLED: prefix
355
+ const cleanTitle = title.slice('DISABLED: '.length);
356
+ const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
357
+ newContent = content.replace(new RegExp(`^(###\\s*)${escaped}\\s*$`, 'm'), `$1${cleanTitle}`);
358
+ } else {
359
+ // Disable: add DISABLED: prefix
360
+ const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
361
+ newContent = content.replace(new RegExp(`^(###\\s*)${escaped}\\s*$`, 'm'), `$1DISABLED: ${title}`);
362
+ }
363
+ if (newContent !== content) {
364
+ await fs.writeFile(reqPath, newContent, 'utf8');
365
+ sections = await loadAllSections();
366
+ }
367
+ } catch (_) {}
368
+ }
369
+ }
370
+ printHeader();
371
+ continue;
372
+ }
373
+
374
+ // Handle Space: toggle spec enabled/disabled (rename directory DISABLED- prefix)
375
+ if (value.startsWith('toggle-disabled-spec:')) {
376
+ const itemIdx = parseInt(value.split(':')[1], 10);
377
+ const item = items[itemIdx];
378
+ if (item && item.value && item.value.startsWith('spec:')) {
379
+ const specIdx = parseInt(item.value.split(':')[1], 10);
380
+ try {
381
+ const { getSpecsList } = require('./trui-specifications');
382
+ const specs = await getSpecsList();
383
+ const spec = specs[specIdx];
384
+ if (spec && spec.path) {
385
+ const parentDir = path.dirname(spec.path);
386
+ const dirName = path.basename(spec.path);
387
+ const newDirName = dirName.startsWith('DISABLED-')
388
+ ? dirName.slice('DISABLED-'.length)
389
+ : 'DISABLED-' + dirName;
390
+ await fs.rename(spec.path, path.join(parentDir, newDirName));
391
+ // Clear spec caches — indices may shift after rename changes sort order
392
+ expandedSpecs.clear();
393
+ specPhases.clear();
394
+ }
395
+ } catch (_) {}
396
+ }
397
+ printHeader();
398
+ continue;
399
+ }
400
+
401
+ // Handle j/k/d inline shortcuts
402
+ if (value.startsWith('jkd:')) {
403
+ const parts = value.split(':');
404
+ const action = parts[1];
405
+ const itemIdx = parseInt(parts[2], 10);
406
+ const item = items[itemIdx];
407
+ if (item && item.value && item.value.startsWith('req:')) {
408
+ const reqParts = item.value.split(':');
409
+ const sectionKey = reqParts[1];
410
+ const reqIdx = parseInt(reqParts[2], 10);
411
+ const req = sections[sectionKey] && sections[sectionKey][reqIdx];
412
+ if (req) {
413
+ try {
414
+ const reqPath = await getReqPath();
415
+ if (action === ',') {
416
+ const moved = await _moveRequirement(reqPath, req.title, sectionKey, 'down');
417
+ if (moved) { sections = await loadAllSections(); lastIndex = Math.min(lastIndex + 1, items.length - 1); }
418
+ } else if (action === '.') {
419
+ const moved = await _moveRequirement(reqPath, req.title, sectionKey, 'up');
420
+ if (moved) { sections = await loadAllSections(); lastIndex = Math.max(lastIndex - 1, 0); }
421
+ } else if (action === '-') {
422
+ console.clear();
423
+ const deleted = await _deleteRequirement(reqPath, req.title);
424
+ if (deleted) { sections = await loadAllSections(); lastIndex = Math.max(lastIndex - 1, 0); }
425
+ }
426
+ } catch (_) {}
427
+ }
428
+ }
429
+ printHeader();
430
+ continue;
431
+ }
432
+
433
+ // Enter on section header → toggle; cursor auto-advances on open, returns to header on close
434
+ if (value.startsWith('section:')) {
435
+ const key = value.slice('section:'.length);
436
+ expanded[key] = !expanded[key];
437
+ if (!expanded[key]) {
438
+ // Closing: position cursor back on the header
439
+ const newItems = await buildTreeItems(sections, expanded, expandedSpecs, specPhases);
440
+ const headerIdx = newItems.findIndex(item => item.value === `section:${key}`);
441
+ if (headerIdx !== -1) lastIndex = headerIdx;
442
+ }
443
+ // Opening: lastIndex stays pointing at header pos; showQuickMenu auto-advances past info
444
+ printHeader();
445
+ continue;
446
+ }
447
+
448
+ if (value.startsWith('req:')) {
449
+ const parts = value.split(':');
450
+ const sectionKey = parts[1];
451
+ const idx = parseInt(parts[2], 10);
452
+ const req = sections[sectionKey] && sections[sectionKey][idx];
453
+ if (req) {
454
+ console.clear();
455
+ try {
456
+ const changed = await showRequirementActions(req, sectionKey);
457
+ if (changed) sections = await loadAllSections();
458
+ } catch (_) {}
459
+ printHeader();
460
+ }
461
+ continue;
462
+ }
463
+
464
+ // Expand section (right arrow)
465
+ if (value.startsWith('expand-section:')) {
466
+ const key = value.slice('expand-section:'.length);
467
+ expanded[key] = true;
468
+ printHeader();
469
+ continue;
470
+ }
471
+
472
+ // Collapse section — position cursor back on the section header
473
+ if (value.startsWith('collapse-section:')) {
474
+ const key = value.slice('collapse-section:'.length);
475
+ expanded[key] = false;
476
+ // Rebuild to find the now-selectable (action) section header position
477
+ const newItems = await buildTreeItems(sections, expanded, expandedSpecs, specPhases);
478
+ const headerIdx = newItems.findIndex(item => item.value === `section:${key}`);
479
+ if (headerIdx !== -1) lastIndex = headerIdx;
480
+ printHeader();
481
+ continue;
482
+ }
483
+
484
+ // Expand spec phases (right arrow on spec item) — specIdx is the spec array index
485
+ if (value.startsWith('expand-spec:')) {
486
+ const specIdx = parseInt(value.split(':')[1], 10);
487
+ expandedSpecs.add(specIdx);
488
+ if (!specPhases.has(specIdx)) {
489
+ try {
490
+ const { getSpecsList, extractPhases } = require('./trui-specifications');
491
+ const specs = await getSpecsList();
492
+ const spec = specs[specIdx];
493
+ if (spec && spec.path) specPhases.set(specIdx, extractPhases(spec.path));
494
+ } catch (_) {}
495
+ }
496
+ printHeader();
497
+ continue;
498
+ }
499
+
500
+ // Collapse spec phases — position cursor back on the spec item
501
+ if (value.startsWith('collapse-spec:')) {
502
+ const specIdx = parseInt(value.split(':')[1], 10);
503
+ expandedSpecs.delete(specIdx);
504
+ // Rebuild to find the now-selectable spec item position
505
+ const newItems = await buildTreeItems(sections, expanded, expandedSpecs, specPhases);
506
+ const specItemIdx = newItems.findIndex(item => item.value === `spec:${specIdx}`);
507
+ if (specItemIdx !== -1) lastIndex = specItemIdx;
508
+ printHeader();
509
+ continue;
510
+ }
511
+
512
+ // Enter on spec item → toggle expansion
513
+ if (value.startsWith('spec:')) {
514
+ const specIdx = parseInt(value.split(':')[1], 10);
515
+ if (expandedSpecs.has(specIdx)) {
516
+ expandedSpecs.delete(specIdx);
517
+ } else {
518
+ expandedSpecs.add(specIdx);
519
+ if (!specPhases.has(specIdx)) {
520
+ try {
521
+ const { getSpecsList, extractPhases } = require('./trui-specifications');
522
+ const specs = await getSpecsList();
523
+ const spec = specs[specIdx];
524
+ if (spec && spec.path) specPhases.set(specIdx, extractPhases(spec.path));
525
+ } catch (_) {}
526
+ }
527
+ }
528
+ printHeader();
529
+ continue;
530
+ }
531
+ }
532
+ }
533
+
534
+ module.exports = { showRequirementsTree, loadSection, loadAllSections, buildTreeItems };