pyre-cli 10.3.0 → 10.5.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.
Files changed (88) hide show
  1. package/README.md +41 -0
  2. package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
  3. package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
  4. package/dist/benchGitCmd-HIXUC5EK.js +87 -0
  5. package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
  6. package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
  7. package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
  8. package/dist/budgetCmd-TGO3RYLN.js +11 -0
  9. package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
  10. package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
  11. package/dist/chunk-3RG5ZIWI.js +10 -0
  12. package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
  13. package/dist/chunk-JH7P6KLN.js +143 -0
  14. package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
  15. package/dist/chunk-NMGQVRCA.js +111 -0
  16. package/dist/chunk-OCL7P7N7.js +936 -0
  17. package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
  18. package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
  19. package/dist/chunk-UF3XQ3R3.js +3800 -0
  20. package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
  21. package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
  22. package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
  23. package/dist/containersCmd-FSMYOBG2.js +84 -0
  24. package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
  25. package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
  26. package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
  27. package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
  28. package/dist/flightRecorder-GJIAQOXC.js +162 -0
  29. package/dist/index.js +736 -4125
  30. package/dist/inspectCmd-5FXJBJTK.js +137 -0
  31. package/dist/menu-AJFGDLBP.js +1233 -0
  32. package/dist/monitors/platform/windows-ui.ps1 +31 -29
  33. package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
  34. package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
  35. package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
  36. package/dist/playbookCmd-FRROIEXR.js +145 -0
  37. package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
  38. package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
  39. package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
  40. package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
  41. package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
  42. package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
  43. package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
  44. package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
  45. package/dist/statuslineCmd-EUKYRR2G.js +75 -0
  46. package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
  47. package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
  48. package/dist/topoCmd-RTXQJ6XA.js +189 -0
  49. package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
  50. package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
  51. package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
  52. package/package.json +2 -2
  53. package/src/benchGitCmd.ts +110 -0
  54. package/src/budgetCmd.ts +199 -0
  55. package/src/completions.ts +42 -3
  56. package/src/containersCmd.ts +106 -0
  57. package/src/doctor.ts +86 -6
  58. package/src/easy_features.test.ts +83 -0
  59. package/src/flightRecorder.ts +214 -0
  60. package/src/index.ts +181 -8
  61. package/src/inspectCmd.ts +165 -0
  62. package/src/live/input.ts +70 -3
  63. package/src/live/render.ts +1 -1
  64. package/src/live/state.ts +2 -1
  65. package/src/menu/commands.ts +754 -0
  66. package/src/menu/index.ts +358 -0
  67. package/src/menu/render.ts +272 -0
  68. package/src/menu/types.ts +63 -0
  69. package/src/menu.test.ts +110 -0
  70. package/src/monitors/blender.ts +49 -19
  71. package/src/monitors/buildTracker.ts +72 -54
  72. package/src/monitors/platform/windows-ui.ps1 +31 -29
  73. package/src/monitors/platform/windows.ts +636 -280
  74. package/src/optimizeCmd.ts +1248 -0
  75. package/src/platform.test.ts +146 -0
  76. package/src/playbookCmd.ts +156 -0
  77. package/src/power_features.test.ts +251 -0
  78. package/src/profileProcCmd.ts +68 -21
  79. package/src/replayCmd.ts +15 -6
  80. package/src/smartCmd.ts +149 -10
  81. package/src/state/config.ts +13 -0
  82. package/src/statuslineCmd.ts +87 -0
  83. package/src/syncCmd.ts +12 -0
  84. package/src/topoCmd.ts +83 -50
  85. package/src/watchdogCmd.ts +40 -3
  86. package/src/xbar.ts +195 -17
  87. package/dist/chunk-M73HCQD2.js +0 -95
  88. 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
+ }