pyre-cli 10.3.0 → 10.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
- package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
- package/dist/benchGitCmd-HIXUC5EK.js +87 -0
- package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
- package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
- package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
- package/dist/budgetCmd-TGO3RYLN.js +11 -0
- package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
- package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
- package/dist/chunk-JH7P6KLN.js +143 -0
- package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
- package/dist/chunk-NMGQVRCA.js +111 -0
- package/dist/chunk-OCL7P7N7.js +936 -0
- package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
- package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
- package/dist/chunk-UF3XQ3R3.js +3800 -0
- package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
- package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
- package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
- package/dist/containersCmd-FSMYOBG2.js +84 -0
- package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
- package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
- package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
- package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
- package/dist/flightRecorder-GJIAQOXC.js +162 -0
- package/dist/index.js +736 -4125
- package/dist/inspectCmd-5FXJBJTK.js +137 -0
- package/dist/menu-AJFGDLBP.js +1233 -0
- package/dist/monitors/platform/windows-ui.ps1 +31 -29
- package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
- package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
- package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
- package/dist/playbookCmd-FRROIEXR.js +145 -0
- package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
- package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
- package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
- package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
- package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
- package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
- package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
- package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
- package/dist/statuslineCmd-EUKYRR2G.js +75 -0
- package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
- package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
- package/dist/topoCmd-RTXQJ6XA.js +189 -0
- package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
- package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
- package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
- package/package.json +2 -2
- package/src/benchGitCmd.ts +110 -0
- package/src/budgetCmd.ts +199 -0
- package/src/completions.ts +42 -3
- package/src/containersCmd.ts +106 -0
- package/src/doctor.ts +86 -6
- package/src/easy_features.test.ts +83 -0
- package/src/flightRecorder.ts +214 -0
- package/src/index.ts +181 -8
- package/src/inspectCmd.ts +165 -0
- package/src/live/input.ts +70 -3
- package/src/live/render.ts +1 -1
- package/src/live/state.ts +2 -1
- package/src/menu/commands.ts +754 -0
- package/src/menu/index.ts +358 -0
- package/src/menu/render.ts +272 -0
- package/src/menu/types.ts +63 -0
- package/src/menu.test.ts +110 -0
- package/src/monitors/blender.ts +49 -19
- package/src/monitors/buildTracker.ts +72 -54
- package/src/monitors/platform/windows-ui.ps1 +31 -29
- package/src/monitors/platform/windows.ts +613 -280
- package/src/optimizeCmd.ts +1248 -0
- package/src/platform.test.ts +135 -0
- package/src/playbookCmd.ts +156 -0
- package/src/power_features.test.ts +251 -0
- package/src/profileProcCmd.ts +68 -21
- package/src/replayCmd.ts +15 -6
- package/src/smartCmd.ts +149 -10
- package/src/state/config.ts +13 -0
- package/src/statuslineCmd.ts +87 -0
- package/src/syncCmd.ts +12 -0
- package/src/topoCmd.ts +83 -50
- package/src/watchdogCmd.ts +40 -3
- package/src/xbar.ts +195 -17
- package/dist/chunk-M73HCQD2.js +0 -95
- package/dist/topoCmd-DZ3YS5IL.js +0 -148
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive TUI Command Center & Tool Navigator for Pyre.
|
|
3
|
+
*/
|
|
4
|
+
import readline from 'node:readline';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { readConfig } from '../state/config.js';
|
|
8
|
+
import type { CommandDefinition, MenuState, CommandCategory } from './types.js';
|
|
9
|
+
import { ALL_COMMANDS, CATEGORIES, filterCommands } from './commands.js';
|
|
10
|
+
import { renderMenuScreen } from './render.js';
|
|
11
|
+
|
|
12
|
+
export interface MenuOptions {
|
|
13
|
+
theme?: string;
|
|
14
|
+
initialCategory?: CommandCategory;
|
|
15
|
+
onExit?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runMenu(opts?: MenuOptions): Promise<void> {
|
|
19
|
+
const config = readConfig();
|
|
20
|
+
const theme = opts?.theme || config.theme || 'default';
|
|
21
|
+
|
|
22
|
+
const state: MenuState = {
|
|
23
|
+
category: opts?.initialCategory || 'all',
|
|
24
|
+
categoryIndex: CATEGORIES.findIndex((c) => c.id === (opts?.initialCategory || 'all')) || 0,
|
|
25
|
+
searchQuery: '',
|
|
26
|
+
selectedIndex: 0,
|
|
27
|
+
scrollOffset: 0,
|
|
28
|
+
inputMode: 'search',
|
|
29
|
+
argPromptValue: '',
|
|
30
|
+
activeCommand: null,
|
|
31
|
+
statusMessage: '',
|
|
32
|
+
termWidth: process.stdout.columns || 80,
|
|
33
|
+
termHeight: process.stdout.rows || 24,
|
|
34
|
+
theme,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let isRunning = true;
|
|
38
|
+
let statusTimer: NodeJS.Timeout | null = null;
|
|
39
|
+
|
|
40
|
+
function setStatus(msg: string, durationMs = 3000) {
|
|
41
|
+
state.statusMessage = msg;
|
|
42
|
+
if (statusTimer) clearTimeout(statusTimer);
|
|
43
|
+
statusTimer = setTimeout(() => {
|
|
44
|
+
state.statusMessage = '';
|
|
45
|
+
if (isRunning) render();
|
|
46
|
+
}, durationMs);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getFiltered(): CommandDefinition[] {
|
|
50
|
+
return filterCommands(ALL_COMMANDS, state.category, state.searchQuery);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function render() {
|
|
54
|
+
if (!isRunning) return;
|
|
55
|
+
const filtered = getFiltered();
|
|
56
|
+
const output = renderMenuScreen(state, filtered);
|
|
57
|
+
process.stdout.write('\x1b[H' + output);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onResize() {
|
|
61
|
+
state.termWidth = process.stdout.columns || 80;
|
|
62
|
+
state.termHeight = process.stdout.rows || 24;
|
|
63
|
+
render();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function enterTui() {
|
|
67
|
+
process.stdout.write('\x1b[?1049h'); // Enter alternate screen buffer
|
|
68
|
+
process.stdout.write('\x1b[?25l'); // Hide cursor
|
|
69
|
+
process.stdout.write('\x1b[2J\x1b[H'); // Clear screen
|
|
70
|
+
readline.emitKeypressEvents(process.stdin);
|
|
71
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
72
|
+
process.stdin.resume();
|
|
73
|
+
process.stdout.on('resize', onResize);
|
|
74
|
+
render();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function leaveTui() {
|
|
78
|
+
process.stdout.removeListener('resize', onResize);
|
|
79
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
80
|
+
process.stdin.pause();
|
|
81
|
+
process.stdout.write('\x1b[?25h'); // Show cursor
|
|
82
|
+
process.stdout.write('\x1b[?1049l'); // Leave alternate screen buffer
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function executeCommand(rawCommandLine: string, isInteractive = false) {
|
|
86
|
+
leaveTui();
|
|
87
|
+
|
|
88
|
+
// Parse tokens from command line string
|
|
89
|
+
// e.g. "pyre check --detailed" -> ["check", "--detailed"]
|
|
90
|
+
const tokens = rawCommandLine.trim().split(/\s+/).filter(Boolean);
|
|
91
|
+
if (tokens[0] === 'pyre') {
|
|
92
|
+
tokens.shift();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(chalk.bold(`\n 🔥 Executing: ${chalk.hex('#ff6a39')(rawCommandLine)}\n`));
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Execute through the current node binary and entry point
|
|
99
|
+
const scriptPath = process.argv[1];
|
|
100
|
+
spawnSync(process.argv[0], [scriptPath, ...tokens], {
|
|
101
|
+
stdio: 'inherit',
|
|
102
|
+
env: process.env,
|
|
103
|
+
});
|
|
104
|
+
} catch (err: any) {
|
|
105
|
+
console.error(chalk.red(`\n ✖ Error executing command: ${err.message}`));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!isInteractive) {
|
|
109
|
+
console.log(chalk.dim('\n ─────────────────────────────────────────────────────────────'));
|
|
110
|
+
console.log(chalk.yellow(' Press any key to return to Command Center (or [q] to exit)...'));
|
|
111
|
+
|
|
112
|
+
if (process.stdin.isTTY) {
|
|
113
|
+
process.stdin.setRawMode(true);
|
|
114
|
+
process.stdin.resume();
|
|
115
|
+
process.stdin.once('data', (buf) => {
|
|
116
|
+
const str = buf.toString().toLowerCase();
|
|
117
|
+
if (str === 'q' || str === '\u0003') {
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
enterTui();
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
enterTui();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
enterTui();
|
|
130
|
+
|
|
131
|
+
return new Promise<void>((resolve) => {
|
|
132
|
+
function onKeypress(str: string, key: readline.Key) {
|
|
133
|
+
if (!isRunning) return;
|
|
134
|
+
if (!key) return;
|
|
135
|
+
|
|
136
|
+
// Ctrl+C exits immediately
|
|
137
|
+
if (key.ctrl && key.name === 'c') {
|
|
138
|
+
isRunning = false;
|
|
139
|
+
leaveTui();
|
|
140
|
+
if (opts?.onExit) opts.onExit();
|
|
141
|
+
else process.exit(0);
|
|
142
|
+
resolve();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 1. HELP MODAL MODE
|
|
147
|
+
if (state.inputMode === 'help') {
|
|
148
|
+
state.inputMode = 'search';
|
|
149
|
+
render();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 2. PROMPT ARGS MODE
|
|
154
|
+
if (state.inputMode === 'prompt-args') {
|
|
155
|
+
if (key.name === 'escape') {
|
|
156
|
+
state.inputMode = 'search';
|
|
157
|
+
state.argPromptValue = '';
|
|
158
|
+
render();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
162
|
+
const cmd = state.activeCommand;
|
|
163
|
+
const args = state.argPromptValue.trim();
|
|
164
|
+
state.inputMode = 'search';
|
|
165
|
+
state.argPromptValue = '';
|
|
166
|
+
if (cmd) {
|
|
167
|
+
const fullCmd = args ? `${cmd.cli} ${args}` : cmd.cli;
|
|
168
|
+
executeCommand(fullCmd, cmd.interactive);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (key.name === 'backspace') {
|
|
173
|
+
state.argPromptValue = state.argPromptValue.slice(0, -1);
|
|
174
|
+
render();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
178
|
+
state.argPromptValue += str;
|
|
179
|
+
render();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 3. EDIT COMMAND LINE MODE
|
|
186
|
+
if (state.inputMode === 'edit-cmd') {
|
|
187
|
+
if (key.name === 'escape') {
|
|
188
|
+
state.inputMode = 'search';
|
|
189
|
+
state.argPromptValue = '';
|
|
190
|
+
render();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
194
|
+
const fullCmd = state.argPromptValue.trim();
|
|
195
|
+
const cmd = state.activeCommand;
|
|
196
|
+
state.inputMode = 'search';
|
|
197
|
+
state.argPromptValue = '';
|
|
198
|
+
if (fullCmd) {
|
|
199
|
+
executeCommand(fullCmd, cmd?.interactive);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (key.name === 'backspace') {
|
|
204
|
+
state.argPromptValue = state.argPromptValue.slice(0, -1);
|
|
205
|
+
render();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
209
|
+
state.argPromptValue += str;
|
|
210
|
+
render();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 4. MAIN SEARCH & BROWSING MODE
|
|
217
|
+
const filtered = getFiltered();
|
|
218
|
+
const count = filtered.length;
|
|
219
|
+
|
|
220
|
+
// Navigation: Up / k
|
|
221
|
+
if (key.name === 'up' || (key.name === 'k' && state.searchQuery === '')) {
|
|
222
|
+
if (count > 0) {
|
|
223
|
+
state.selectedIndex = (state.selectedIndex - 1 + count) % count;
|
|
224
|
+
render();
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Navigation: Down / j
|
|
230
|
+
if (key.name === 'down' || (key.name === 'j' && state.searchQuery === '')) {
|
|
231
|
+
if (count > 0) {
|
|
232
|
+
state.selectedIndex = (state.selectedIndex + 1) % count;
|
|
233
|
+
render();
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Navigation: PageUp
|
|
239
|
+
if (key.name === 'pageup') {
|
|
240
|
+
state.selectedIndex = Math.max(0, state.selectedIndex - 6);
|
|
241
|
+
render();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Navigation: PageDown
|
|
246
|
+
if (key.name === 'pagedown') {
|
|
247
|
+
state.selectedIndex = Math.min(count - 1, state.selectedIndex + 6);
|
|
248
|
+
render();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Cycle Category: Tab / Shift+Tab
|
|
253
|
+
if (key.name === 'tab') {
|
|
254
|
+
const dir = key.shift ? -1 : 1;
|
|
255
|
+
state.categoryIndex = (state.categoryIndex + dir + CATEGORIES.length) % CATEGORIES.length;
|
|
256
|
+
state.category = CATEGORIES[state.categoryIndex].id;
|
|
257
|
+
state.selectedIndex = 0;
|
|
258
|
+
state.scrollOffset = 0;
|
|
259
|
+
render();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Help: ?
|
|
264
|
+
if (str === '?' || (str === 'h' && state.searchQuery === '')) {
|
|
265
|
+
state.inputMode = 'help';
|
|
266
|
+
render();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Copy: c
|
|
271
|
+
if (str === 'c' && state.searchQuery === '') {
|
|
272
|
+
const cur = filtered[state.selectedIndex];
|
|
273
|
+
if (cur) {
|
|
274
|
+
setStatus(`CLI command: ${cur.cli}`);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Edit Command: e
|
|
280
|
+
if (str === 'e' && state.searchQuery === '') {
|
|
281
|
+
const cur = filtered[state.selectedIndex];
|
|
282
|
+
if (cur) {
|
|
283
|
+
state.inputMode = 'edit-cmd';
|
|
284
|
+
state.activeCommand = cur;
|
|
285
|
+
state.argPromptValue = cur.cli;
|
|
286
|
+
render();
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Execute: Enter / Return
|
|
292
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
293
|
+
const cur = filtered[state.selectedIndex];
|
|
294
|
+
if (!cur) return;
|
|
295
|
+
|
|
296
|
+
if (cur.requiresArgs) {
|
|
297
|
+
state.inputMode = 'prompt-args';
|
|
298
|
+
state.activeCommand = cur;
|
|
299
|
+
state.argPromptValue = cur.requiresArgs.defaultVal || '';
|
|
300
|
+
render();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
executeCommand(cur.cli, cur.interactive);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Backspace in search
|
|
309
|
+
if (key.name === 'backspace') {
|
|
310
|
+
if (state.searchQuery.length > 0) {
|
|
311
|
+
state.searchQuery = state.searchQuery.slice(0, -1);
|
|
312
|
+
state.selectedIndex = 0;
|
|
313
|
+
state.scrollOffset = 0;
|
|
314
|
+
render();
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Esc: clear search or exit
|
|
320
|
+
if (key.name === 'escape') {
|
|
321
|
+
if (state.searchQuery.length > 0) {
|
|
322
|
+
state.searchQuery = '';
|
|
323
|
+
state.selectedIndex = 0;
|
|
324
|
+
state.scrollOffset = 0;
|
|
325
|
+
render();
|
|
326
|
+
} else {
|
|
327
|
+
isRunning = false;
|
|
328
|
+
leaveTui();
|
|
329
|
+
if (opts?.onExit) opts.onExit();
|
|
330
|
+
else process.exit(0);
|
|
331
|
+
resolve();
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Quit: q (when search is empty)
|
|
337
|
+
if (str === 'q' && state.searchQuery === '') {
|
|
338
|
+
isRunning = false;
|
|
339
|
+
leaveTui();
|
|
340
|
+
if (opts?.onExit) opts.onExit();
|
|
341
|
+
else process.exit(0);
|
|
342
|
+
resolve();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Regular character typing into search
|
|
347
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
348
|
+
state.searchQuery += str;
|
|
349
|
+
state.selectedIndex = 0;
|
|
350
|
+
state.scrollOffset = 0;
|
|
351
|
+
render();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
process.stdin.on('keypress', onKeypress);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Renderer for Pyre's Command Center & Tool Navigator.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { THEMES, type ThemeName, panel, fitVisible, visLen } from '../formatters/index.js';
|
|
6
|
+
import type { MenuState, CommandDefinition } from './types.js';
|
|
7
|
+
import { CATEGORIES } from './commands.js';
|
|
8
|
+
|
|
9
|
+
export function renderMenuScreen(state: MenuState, filteredCommands: CommandDefinition[]): string {
|
|
10
|
+
const width = Math.max(60, state.termWidth || 80);
|
|
11
|
+
const height = Math.max(16, state.termHeight || 24);
|
|
12
|
+
const theme = THEMES[state.theme as ThemeName] || THEMES.default;
|
|
13
|
+
const accent = theme.cpu || chalk.cyan;
|
|
14
|
+
const border = theme.border || chalk.dim;
|
|
15
|
+
const highlight = theme.graphs || chalk.hex('#ff6a39');
|
|
16
|
+
|
|
17
|
+
const lines: string[] = [];
|
|
18
|
+
|
|
19
|
+
// --- 1. Header Banner ---
|
|
20
|
+
const title = ` 🔥 pyre — Command Center & Tools Navigator `;
|
|
21
|
+
const headerLeft = accent(chalk.bold(title));
|
|
22
|
+
const headerRight = chalk.dim(`[?] Help [q] Quit `);
|
|
23
|
+
const topBarInner = headerLeft + '─'.repeat(Math.max(0, width - visLen(title) - visLen(headerRight) - 2)) + headerRight;
|
|
24
|
+
lines.push(border('╭') + topBarInner + border('╮'));
|
|
25
|
+
|
|
26
|
+
const subtitle = ' Browse, search and execute all 38 pyre commands without memorization';
|
|
27
|
+
lines.push(border('│') + fitVisible(chalk.dim(subtitle), width - 2) + border('│'));
|
|
28
|
+
|
|
29
|
+
// --- 2. Search Bar & Status ---
|
|
30
|
+
let searchInput = state.searchQuery;
|
|
31
|
+
if (state.inputMode === 'search') {
|
|
32
|
+
searchInput += chalk.bgWhite.black(' ');
|
|
33
|
+
}
|
|
34
|
+
const searchPrompt = ` 🔍 Search: `;
|
|
35
|
+
const searchContent = searchPrompt + (state.searchQuery ? chalk.bold.yellow(searchInput) : chalk.dim('Type command, keyword or description... (e.g. doctor, clean, smart)'));
|
|
36
|
+
const matchCount = chalk.dim(`(${filteredCommands.length}/${38} tools) `);
|
|
37
|
+
const searchBar = fitVisible(searchContent, width - visLen(matchCount) - 2) + matchCount;
|
|
38
|
+
lines.push(border('├') + border('─'.repeat(width - 2)) + border('┤'));
|
|
39
|
+
lines.push(border('│') + fitVisible(searchBar, width - 2) + border('│'));
|
|
40
|
+
|
|
41
|
+
// --- 3. Category Tabs Bar ---
|
|
42
|
+
const catTabs = CATEGORIES.map((cat) => {
|
|
43
|
+
const isSelected = cat.id === state.category;
|
|
44
|
+
if (isSelected) {
|
|
45
|
+
return chalk.bgHex('#ff6a39').black(` ${cat.icon} ${cat.name} `);
|
|
46
|
+
}
|
|
47
|
+
return chalk.dim(` ${cat.icon} ${cat.name} `);
|
|
48
|
+
}).join(chalk.dim('│'));
|
|
49
|
+
lines.push(border('│') + fitVisible(' ' + catTabs, width - 2) + border('│'));
|
|
50
|
+
lines.push(border('├') + border('─'.repeat(width - 2)) + border('┤'));
|
|
51
|
+
|
|
52
|
+
// --- 4. Main Body (Dual Pane or Single Pane) ---
|
|
53
|
+
const bodyHeight = Math.max(8, height - 9); // Total rows available for list/inspector
|
|
54
|
+
const isDualPane = width >= 80;
|
|
55
|
+
const listWidth = isDualPane ? Math.min(46, Math.floor(width * 0.42)) : width - 2;
|
|
56
|
+
const inspectorWidth = width - 2 - listWidth - (isDualPane ? 1 : 0);
|
|
57
|
+
|
|
58
|
+
// Left: Command list rows
|
|
59
|
+
const listLines: string[] = [];
|
|
60
|
+
const selectedIdx = Math.min(state.selectedIndex, Math.max(0, filteredCommands.length - 1));
|
|
61
|
+
|
|
62
|
+
// Compute scroll window
|
|
63
|
+
let startIdx = state.scrollOffset;
|
|
64
|
+
if (selectedIdx < startIdx) {
|
|
65
|
+
startIdx = selectedIdx;
|
|
66
|
+
} else if (selectedIdx >= startIdx + bodyHeight) {
|
|
67
|
+
startIdx = selectedIdx - bodyHeight + 1;
|
|
68
|
+
}
|
|
69
|
+
state.scrollOffset = startIdx;
|
|
70
|
+
const endIdx = Math.min(filteredCommands.length, startIdx + bodyHeight);
|
|
71
|
+
|
|
72
|
+
if (filteredCommands.length === 0) {
|
|
73
|
+
listLines.push(chalk.yellow(' No commands match your search.'));
|
|
74
|
+
listLines.push(chalk.dim(' Press Esc or backspace to clear query.'));
|
|
75
|
+
} else {
|
|
76
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
77
|
+
const cmd = filteredCommands[i];
|
|
78
|
+
const isCur = i === selectedIdx;
|
|
79
|
+
const badgeText = `[${cmd.badge}]`;
|
|
80
|
+
|
|
81
|
+
if (isCur) {
|
|
82
|
+
const rowText = ` ▶ ${cmd.cli} ${badgeText}`;
|
|
83
|
+
listLines.push(chalk.bgHex('#ff6a39').black(fitVisible(rowText, listWidth)));
|
|
84
|
+
} else {
|
|
85
|
+
const badgeStyled = chalk.dim(badgeText);
|
|
86
|
+
const cliStyled = chalk.bold(cmd.cli);
|
|
87
|
+
const rowText = ` ${cliStyled} ${badgeStyled}`;
|
|
88
|
+
listLines.push(fitVisible(rowText, listWidth));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
while (listLines.length < bodyHeight) {
|
|
94
|
+
listLines.push(' '.repeat(listWidth));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Right: Inspector Pane
|
|
98
|
+
const inspectorLines: string[] = [];
|
|
99
|
+
const curCmd = filteredCommands[selectedIdx];
|
|
100
|
+
|
|
101
|
+
if (curCmd && isDualPane) {
|
|
102
|
+
inspectorLines.push(accent(chalk.bold(` ${curCmd.name} `)) + chalk.dim(`[${curCmd.badge}]`));
|
|
103
|
+
inspectorLines.push(chalk.dim(` Syntax: `) + highlight(chalk.bold(curCmd.cli)));
|
|
104
|
+
inspectorLines.push(chalk.dim('─'.repeat(inspectorWidth - 2)));
|
|
105
|
+
|
|
106
|
+
// Description & Help
|
|
107
|
+
inspectorLines.push(fitVisible(` ${curCmd.description}`, inspectorWidth - 2));
|
|
108
|
+
inspectorLines.push('');
|
|
109
|
+
inspectorLines.push(fitVisible(` ${curCmd.detailedHelp}`, inspectorWidth - 2));
|
|
110
|
+
inspectorLines.push('');
|
|
111
|
+
|
|
112
|
+
// Flags section if present
|
|
113
|
+
if (curCmd.flags && curCmd.flags.length > 0) {
|
|
114
|
+
inspectorLines.push(chalk.bold.dim(' Options & Flags:'));
|
|
115
|
+
for (const f of curCmd.flags.slice(0, 4)) {
|
|
116
|
+
const flagLabel = chalk.yellow(` ${f.flag}`);
|
|
117
|
+
const flagDesc = chalk.dim(` - ${f.desc}`);
|
|
118
|
+
inspectorLines.push(fitVisible(`${flagLabel}${flagDesc}`, inspectorWidth - 2));
|
|
119
|
+
}
|
|
120
|
+
inspectorLines.push('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Example section if present
|
|
124
|
+
if (curCmd.examples && curCmd.examples.length > 0) {
|
|
125
|
+
inspectorLines.push(chalk.bold.dim(' Examples:'));
|
|
126
|
+
for (const ex of curCmd.examples.slice(0, 2)) {
|
|
127
|
+
inspectorLines.push(chalk.cyan(` $ ${ex}`));
|
|
128
|
+
}
|
|
129
|
+
inspectorLines.push('');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Quick action prompt
|
|
133
|
+
inspectorLines.push(chalk.dim('─'.repeat(inspectorWidth - 2)));
|
|
134
|
+
const runPrompt = curCmd.requiresArgs
|
|
135
|
+
? chalk.yellow(' [Enter] Enter arguments & Run [c] Copy [e] Edit Flags')
|
|
136
|
+
: chalk.green(' [Enter] Execute Command [c] Copy [e] Edit Flags');
|
|
137
|
+
inspectorLines.push(fitVisible(runPrompt, inspectorWidth - 2));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
while (inspectorLines.length < bodyHeight) {
|
|
141
|
+
inspectorLines.push(' '.repeat(inspectorWidth));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Combine Left & Right panes into body rows
|
|
145
|
+
for (let r = 0; r < bodyHeight; r++) {
|
|
146
|
+
const left = fitVisible(listLines[r] || '', listWidth);
|
|
147
|
+
if (isDualPane) {
|
|
148
|
+
const right = fitVisible(inspectorLines[r] || '', inspectorWidth);
|
|
149
|
+
lines.push(border('│') + left + border('│') + right + border('│'));
|
|
150
|
+
} else {
|
|
151
|
+
lines.push(border('│') + left + border('│'));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- 5. Footer Shortcuts ---
|
|
156
|
+
lines.push(border('├') + border('─'.repeat(width - 2)) + border('┤'));
|
|
157
|
+
const statusMsg = state.statusMessage ? chalk.yellow(` ${state.statusMessage} `) : '';
|
|
158
|
+
const shortcuts = ` [↑/↓] Select [Enter] Run [Tab] Category [/] Search [e] Edit [?] Help [q] Quit`;
|
|
159
|
+
const footerRow = fitVisible(shortcuts, width - visLen(statusMsg) - 2) + statusMsg;
|
|
160
|
+
lines.push(border('│') + fitVisible(footerRow, width - 2) + border('│'));
|
|
161
|
+
lines.push(border('╰') + border('─'.repeat(width - 2)) + border('╯'));
|
|
162
|
+
|
|
163
|
+
// --- 6. Modal Overlays (if active) ---
|
|
164
|
+
if (state.inputMode === 'prompt-args' && state.activeCommand) {
|
|
165
|
+
overlayPromptModal(lines, width, height, state.activeCommand, state.argPromptValue, accent, border);
|
|
166
|
+
} else if (state.inputMode === 'edit-cmd' && state.activeCommand) {
|
|
167
|
+
overlayEditCmdModal(lines, width, height, state.activeCommand, state.argPromptValue, accent, border);
|
|
168
|
+
} else if (state.inputMode === 'help') {
|
|
169
|
+
overlayHelpModal(lines, width, height, accent, border);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return lines.join('\r\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Render centered modal prompt for command arguments */
|
|
176
|
+
function overlayPromptModal(
|
|
177
|
+
lines: string[],
|
|
178
|
+
width: number,
|
|
179
|
+
height: number,
|
|
180
|
+
cmd: CommandDefinition,
|
|
181
|
+
val: string,
|
|
182
|
+
accent: (s: string) => string,
|
|
183
|
+
border: (s: string) => string
|
|
184
|
+
) {
|
|
185
|
+
const modalW = Math.min(width - 4, 64);
|
|
186
|
+
const promptText = cmd.requiresArgs?.prompt || 'Enter arguments:';
|
|
187
|
+
const modalLines = [
|
|
188
|
+
chalk.bold.yellow(` Run: ${cmd.cli}`),
|
|
189
|
+
chalk.dim(` ${cmd.description}`),
|
|
190
|
+
'',
|
|
191
|
+
chalk.white(` ${promptText}`),
|
|
192
|
+
` > ${chalk.bold.cyan(val)}${chalk.bgWhite.black(' ')}`,
|
|
193
|
+
'',
|
|
194
|
+
chalk.dim(' [Enter] Run Command [Esc] Cancel'),
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
const box = panel(` Execute: ${cmd.name} `, modalLines, modalW, accent, border, modalLines.length);
|
|
198
|
+
stampOverlay(lines, box, width, height);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Render centered modal for customizing command line */
|
|
202
|
+
function overlayEditCmdModal(
|
|
203
|
+
lines: string[],
|
|
204
|
+
width: number,
|
|
205
|
+
height: number,
|
|
206
|
+
cmd: CommandDefinition,
|
|
207
|
+
val: string,
|
|
208
|
+
accent: (s: string) => string,
|
|
209
|
+
border: (s: string) => string
|
|
210
|
+
) {
|
|
211
|
+
const modalW = Math.min(width - 4, 70);
|
|
212
|
+
const modalLines = [
|
|
213
|
+
chalk.bold.yellow(` Customize Command Line:`),
|
|
214
|
+
chalk.dim(` Edit flags or arguments and press Enter to execute.`),
|
|
215
|
+
'',
|
|
216
|
+
` $ ${chalk.bold.cyan(val)}${chalk.bgWhite.black(' ')}`,
|
|
217
|
+
'',
|
|
218
|
+
chalk.dim(' [Enter] Execute Custom Command [Esc] Cancel'),
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const box = panel(` Edit & Run `, modalLines, modalW, accent, border, modalLines.length);
|
|
222
|
+
stampOverlay(lines, box, width, height);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Render help overlay */
|
|
226
|
+
function overlayHelpModal(
|
|
227
|
+
lines: string[],
|
|
228
|
+
width: number,
|
|
229
|
+
height: number,
|
|
230
|
+
accent: (s: string) => string,
|
|
231
|
+
border: (s: string) => string
|
|
232
|
+
) {
|
|
233
|
+
const modalW = Math.min(width - 4, 66);
|
|
234
|
+
const modalLines = [
|
|
235
|
+
chalk.bold.yellow(' Pyre Command Center — Keyboard Shortcuts'),
|
|
236
|
+
'',
|
|
237
|
+
` ${chalk.cyan('↑ / k, ↓ / j')} Navigate command list`,
|
|
238
|
+
` ${chalk.cyan('PgUp / PgDn')} Scroll command list fast`,
|
|
239
|
+
` ${chalk.cyan('Tab / Shift+Tab')} Cycle category filters`,
|
|
240
|
+
` ${chalk.cyan('1 - 7')} Jump directly to category`,
|
|
241
|
+
` ${chalk.cyan('Enter')} Execute selected command`,
|
|
242
|
+
` ${chalk.cyan('e')} Edit flags/arguments before running`,
|
|
243
|
+
` ${chalk.cyan('c')} Copy command to clipboard / show snippet`,
|
|
244
|
+
` ${chalk.cyan('/ (or type)')} Search / filter commands instantly`,
|
|
245
|
+
` ${chalk.cyan('Backspace')} Delete search query character`,
|
|
246
|
+
` ${chalk.cyan('Esc')} Clear search or close modal`,
|
|
247
|
+
` ${chalk.cyan('q')} Quit to shell`,
|
|
248
|
+
'',
|
|
249
|
+
chalk.dim(' Press any key or Esc to close help...'),
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
const box = panel(' Help & Shortcuts ', modalLines, modalW, accent, border, modalLines.length);
|
|
253
|
+
stampOverlay(lines, box, width, height);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Stamp a box array in the center of the terminal lines */
|
|
257
|
+
function stampOverlay(screenLines: string[], box: string[], width: number, height: number) {
|
|
258
|
+
const boxH = box.length;
|
|
259
|
+
const startRow = Math.max(0, Math.floor((screenLines.length - boxH) / 2));
|
|
260
|
+
const boxW = visLen(box[0] || '');
|
|
261
|
+
const startCol = Math.max(0, Math.floor((width - boxW) / 2));
|
|
262
|
+
|
|
263
|
+
for (let r = 0; r < boxH; r++) {
|
|
264
|
+
const targetRow = startRow + r;
|
|
265
|
+
if (targetRow < screenLines.length) {
|
|
266
|
+
const orig = screenLines[targetRow];
|
|
267
|
+
const left = orig.slice(0, startCol);
|
|
268
|
+
const right = orig.slice(startCol + boxW);
|
|
269
|
+
screenLines[targetRow] = fitVisible(left, startCol) + box[r] + fitVisible(right, Math.max(0, width - startCol - boxW));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Pyre's interactive TUI Command Center.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type CommandCategory =
|
|
6
|
+
| 'all'
|
|
7
|
+
| 'dashboards'
|
|
8
|
+
| 'health'
|
|
9
|
+
| 'performance'
|
|
10
|
+
| 'maintenance'
|
|
11
|
+
| 'network'
|
|
12
|
+
| 'config';
|
|
13
|
+
|
|
14
|
+
export interface CategoryInfo {
|
|
15
|
+
id: CommandCategory;
|
|
16
|
+
name: string;
|
|
17
|
+
icon: string;
|
|
18
|
+
badge: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CommandFlag {
|
|
22
|
+
flag: string;
|
|
23
|
+
desc: string;
|
|
24
|
+
example?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CommandDefinition {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
cli: string;
|
|
31
|
+
category: CommandCategory;
|
|
32
|
+
badge: string;
|
|
33
|
+
description: string;
|
|
34
|
+
detailedHelp: string;
|
|
35
|
+
flags?: CommandFlag[];
|
|
36
|
+
examples?: string[];
|
|
37
|
+
tags: string[];
|
|
38
|
+
/** Whether the command runs interactively (replaces screen) or one-shot */
|
|
39
|
+
interactive?: boolean;
|
|
40
|
+
/** Whether the command requires argument input before running */
|
|
41
|
+
requiresArgs?: {
|
|
42
|
+
prompt: string;
|
|
43
|
+
placeholder: string;
|
|
44
|
+
defaultVal?: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type MenuInputMode = 'search' | 'prompt-args' | 'edit-cmd' | 'help';
|
|
49
|
+
|
|
50
|
+
export interface MenuState {
|
|
51
|
+
category: CommandCategory;
|
|
52
|
+
categoryIndex: number;
|
|
53
|
+
searchQuery: string;
|
|
54
|
+
selectedIndex: number;
|
|
55
|
+
scrollOffset: number;
|
|
56
|
+
inputMode: MenuInputMode;
|
|
57
|
+
argPromptValue: string;
|
|
58
|
+
activeCommand: CommandDefinition | null;
|
|
59
|
+
statusMessage: string;
|
|
60
|
+
termWidth: number;
|
|
61
|
+
termHeight: number;
|
|
62
|
+
theme: string;
|
|
63
|
+
}
|