vibecodingmachine-cli 2026.2.26-1752 → 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.
- package/bin/auth/auth-compliance.js +7 -1
- package/bin/commands/agent-commands.js +150 -228
- package/bin/commands/command-aliases.js +68 -0
- package/bin/vibecodingmachine.js +1 -2
- package/package.json +2 -2
- package/src/commands/agents/list.js +71 -115
- package/src/commands/agents-check.js +16 -4
- package/src/commands/analyze-file-sizes.js +1 -1
- package/src/commands/auto-direct/auto-provider-manager.js +290 -0
- package/src/commands/auto-direct/auto-status-display.js +331 -0
- package/src/commands/auto-direct/auto-utils.js +439 -0
- package/src/commands/auto-direct/file-operations.js +110 -0
- package/src/commands/auto-direct/provider-config.js +1 -1
- package/src/commands/auto-direct/provider-manager.js +1 -1
- package/src/commands/auto-direct/status-display.js +1 -1
- package/src/commands/auto-direct/utils.js +24 -18
- package/src/commands/auto-direct-refactored.js +413 -0
- package/src/commands/auto-direct.js +594 -188
- package/src/commands/requirements/commands.js +353 -0
- package/src/commands/requirements/default-handlers.js +272 -0
- package/src/commands/requirements/disable.js +97 -0
- package/src/commands/requirements/enable.js +97 -0
- package/src/commands/requirements/utils.js +194 -0
- package/src/commands/requirements-refactored.js +60 -0
- package/src/commands/requirements.js +38 -771
- package/src/commands/specs/disable.js +96 -0
- package/src/commands/specs/enable.js +96 -0
- package/src/trui/TruiInterface.js +5 -11
- package/src/trui/agents/AgentInterface.js +24 -396
- package/src/trui/agents/handlers/CommandHandler.js +93 -0
- package/src/trui/agents/handlers/ContextManager.js +117 -0
- package/src/trui/agents/handlers/DisplayHandler.js +243 -0
- package/src/trui/agents/handlers/HelpHandler.js +51 -0
- package/src/utils/auth.js +13 -111
- package/src/utils/config.js +4 -0
- package/src/utils/interactive/requirements-navigation.js +17 -15
- package/src/utils/interactive-broken.js +2 -2
- package/src/utils/provider-checker/agent-runner.js +15 -1
- package/src/utils/provider-checker/cli-installer.js +149 -7
- package/src/utils/provider-checker/opencode-checker.js +588 -0
- package/src/utils/provider-checker/provider-validator.js +88 -3
- package/src/utils/provider-checker/time-formatter.js +3 -2
- package/src/utils/provider-manager.js +28 -20
- package/src/utils/provider-registry.js +35 -3
- package/src/utils/requirements-navigator/index.js +94 -0
- package/src/utils/requirements-navigator/input-handler.js +217 -0
- package/src/utils/requirements-navigator/section-loader.js +188 -0
- package/src/utils/requirements-navigator/tree-builder.js +105 -0
- package/src/utils/requirements-navigator/tree-renderer.js +50 -0
- package/src/utils/requirements-navigator.js +2 -583
- package/src/utils/trui-clarifications.js +188 -0
- package/src/utils/trui-feedback.js +54 -1
- package/src/utils/trui-kiro-integration.js +398 -0
- package/src/utils/trui-main-handlers.js +194 -0
- package/src/utils/trui-main-menu.js +235 -0
- package/src/utils/trui-nav-agents.js +178 -25
- package/src/utils/trui-nav-requirements.js +203 -27
- package/src/utils/trui-nav-settings.js +114 -1
- package/src/utils/trui-nav-specifications.js +44 -3
- package/src/utils/trui-navigation-backup.js +603 -0
- package/src/utils/trui-navigation.js +70 -228
- package/src/utils/trui-provider-health.js +274 -0
- package/src/utils/trui-provider-manager.js +376 -0
- package/src/utils/trui-quick-menu.js +25 -1
- package/src/utils/trui-req-actions-backup.js +507 -0
- package/src/utils/trui-req-actions.js +148 -216
- package/src/utils/trui-req-editor.js +170 -0
- package/src/utils/trui-req-file-ops.js +278 -0
- package/src/utils/trui-req-tree-old.js +719 -0
- package/src/utils/trui-req-tree.js +348 -627
- package/src/utils/trui-specifications.js +25 -7
- package/src/utils/trui-windsurf.js +231 -10
- package/src/utils/welcome-screen-extracted.js +2 -2
- 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 };
|