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.
- package/bin/vibecodingmachine.js +124 -0
- package/package.json +3 -2
- package/src/commands/agents-check.js +69 -0
- package/src/commands/auto-direct.js +930 -145
- package/src/commands/auto.js +26 -4
- package/src/commands/ide.js +2 -1
- package/src/commands/requirements.js +23 -27
- package/src/utils/auto-mode.js +4 -1
- package/src/utils/cline-js-handler.js +218 -0
- package/src/utils/config.js +22 -0
- package/src/utils/display-formatters-complete.js +229 -0
- package/src/utils/display-formatters-extracted.js +219 -0
- package/src/utils/display-formatters.js +157 -0
- package/src/utils/feedback-handler.js +143 -0
- package/src/utils/ide-detection-complete.js +126 -0
- package/src/utils/ide-detection-extracted.js +116 -0
- package/src/utils/ide-detection.js +124 -0
- package/src/utils/interactive-backup.js +5664 -0
- package/src/utils/interactive-broken.js +280 -0
- package/src/utils/interactive.js +31 -5534
- package/src/utils/provider-checker.js +410 -0
- package/src/utils/provider-manager.js +251 -0
- package/src/utils/provider-registry.js +18 -9
- package/src/utils/requirement-actions.js +884 -0
- package/src/utils/requirements-navigator.js +585 -0
- package/src/utils/rui-trui-adapter.js +311 -0
- package/src/utils/simple-trui.js +204 -0
- package/src/utils/status-helpers-extracted.js +125 -0
- package/src/utils/status-helpers.js +107 -0
- package/src/utils/trui-debug.js +261 -0
- package/src/utils/trui-feedback.js +133 -0
- package/src/utils/trui-nav-agents.js +119 -0
- package/src/utils/trui-nav-requirements.js +268 -0
- package/src/utils/trui-nav-settings.js +157 -0
- package/src/utils/trui-nav-specifications.js +139 -0
- package/src/utils/trui-navigation.js +303 -0
- package/src/utils/trui-provider-manager.js +182 -0
- package/src/utils/trui-quick-menu.js +365 -0
- package/src/utils/trui-req-actions.js +372 -0
- package/src/utils/trui-req-tree.js +534 -0
- package/src/utils/trui-specifications.js +359 -0
- package/src/utils/trui-text-editor.js +350 -0
- package/src/utils/trui-windsurf.js +336 -0
- package/src/utils/welcome-screen-extracted.js +135 -0
- 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 };
|