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,1233 @@
1
+ import "./chunk-UF3XQ3R3.js";
2
+ import {
3
+ THEMES,
4
+ fitVisible,
5
+ panel,
6
+ visLen
7
+ } from "./chunk-XTOOEKLM.js";
8
+ import "./chunk-ZLVIVFSO.js";
9
+ import {
10
+ readConfig
11
+ } from "./chunk-LDJB5V4N.js";
12
+ import "./chunk-3RG5ZIWI.js";
13
+
14
+ // src/menu/index.ts
15
+ import readline from "readline";
16
+ import { spawnSync } from "child_process";
17
+ import chalk2 from "chalk";
18
+
19
+ // src/menu/commands.ts
20
+ var CATEGORIES = [
21
+ { id: "all", name: "All Tools", icon: "\u26A1", badge: "ALL" },
22
+ { id: "health", name: "Health & Diagnostics", icon: "\u{1FA7A}", badge: "HEALTH" },
23
+ { id: "dashboards", name: "Dashboards & Live", icon: "\u{1F4CA}", badge: "LIVE" },
24
+ { id: "performance", name: "Performance & Benchmarks", icon: "\u{1F680}", badge: "PERF" },
25
+ { id: "maintenance", name: "System & Maintenance", icon: "\u{1F9F9}", badge: "SYS" },
26
+ { id: "network", name: "Network & Remote", icon: "\u{1F310}", badge: "NET" },
27
+ { id: "config", name: "Config & Utilities", icon: "\u2699\uFE0F", badge: "CFG" }
28
+ ];
29
+ var ALL_COMMANDS = [
30
+ // --- Dashboards & Live ---
31
+ {
32
+ id: "live",
33
+ name: "Live Dashboard",
34
+ cli: "pyre live",
35
+ category: "dashboards",
36
+ badge: "LIVE",
37
+ description: "Interactive full-screen terminal dashboard with real-time graphs and telemetry",
38
+ detailedHelp: "Launches Pyre's signature full-terminal dashboard. Displays live sparklines for CPU/GPU/memory/network, Apple Silicon cluster telemetry, process tree, custom themes, and continuous CSV logging.",
39
+ tags: ["dashboard", "live", "monitor", "graph", "realtime", "cpu", "memory", "top", "htop"],
40
+ interactive: true,
41
+ flags: [
42
+ { flag: "--interval <sec>", desc: "Refresh interval (default: 2s)" },
43
+ { flag: "--theme <name>", desc: "Color theme (dracula, cyberpunk, nord, gruvbox)" },
44
+ { flag: "--detailed", desc: "Include powermetrics per-core sensors" },
45
+ { flag: "--log", desc: "Start continuous CSV logging immediately" },
46
+ { flag: "--tree", desc: "Show process hierarchy tree view" }
47
+ ],
48
+ examples: ["pyre live", "pyre live --interval 1 --theme dracula", "pyre live --detailed"]
49
+ },
50
+ {
51
+ id: "ui",
52
+ name: "Activity Monitor UI Window",
53
+ cli: "pyre ui",
54
+ category: "dashboards",
55
+ badge: "GUI",
56
+ description: "Launch native desktop window with real-time charts and process inspector",
57
+ detailedHelp: "Opens Pyre's native graphical desktop dashboard. Provides click-to-inspect process tree, live charts, dark mode, and compact widget mode.",
58
+ tags: ["gui", "window", "native", "desktop", "activity monitor", "widget"],
59
+ interactive: true,
60
+ flags: [
61
+ { flag: "--compact", desc: "Launch in minimalist compact widget mode" },
62
+ { flag: "--ontop", desc: "Keep window always on top of other apps" }
63
+ ],
64
+ examples: ["pyre ui", "pyre ui --compact", "pyre ui --ontop"]
65
+ },
66
+ {
67
+ id: "web",
68
+ name: "Web Dashboard Server",
69
+ cli: "pyre web",
70
+ category: "dashboards",
71
+ badge: "WEB",
72
+ description: "Serve an auto-refreshing live web portal with SSE streams and REST endpoints",
73
+ detailedHelp: "Starts a high-performance web dashboard accessible from your browser or mobile device on the same local network. Includes charts, process manager, and remote settings.",
74
+ tags: ["web", "browser", "server", "http", "sse", "mobile", "lan"],
75
+ interactive: true,
76
+ flags: [
77
+ { flag: "--port <port>", desc: "HTTP port number (default: 3000)" },
78
+ { flag: "--lan", desc: "Expose to Wi-Fi / local network" },
79
+ { flag: "--no-lan", desc: "Bind to localhost only" },
80
+ { flag: "--install", desc: "Install as background launchd / system service" }
81
+ ],
82
+ examples: ["pyre web", "pyre web --port 8080 --lan"]
83
+ },
84
+ {
85
+ id: "pipe",
86
+ name: "JSON Stream Pipe",
87
+ cli: "pyre pipe",
88
+ category: "dashboards",
89
+ badge: "PIPE",
90
+ description: "Continuous newline-delimited JSON stream for scripting and automation",
91
+ detailedHelp: "Emits a steady NDJSON (newline-delimited JSON) stream of system metrics to stdout. Ideal for piping into jq, Logstash, Vector, or custom scripts.",
92
+ tags: ["pipe", "json", "ndjson", "stream", "stdout", "scripting"],
93
+ flags: [
94
+ { flag: "--interval <sec>", desc: "Sampling interval (default: 1s)" },
95
+ { flag: "--detailed", desc: "Include full sensor readings" }
96
+ ],
97
+ examples: ["pyre pipe | jq .cpu", "pyre pipe --interval 5"]
98
+ },
99
+ {
100
+ id: "replay",
101
+ name: "Session Log Replay",
102
+ cli: "pyre replay",
103
+ category: "dashboards",
104
+ badge: "REPLAY",
105
+ description: "Play back a historical CSV log file inside the interactive TUI",
106
+ detailedHelp: "Replays previously recorded Pyre CSV sessions with controllable speed multiplier, play/pause controls, and time seeking.",
107
+ tags: ["replay", "log", "csv", "playback", "history"],
108
+ interactive: true,
109
+ requiresArgs: {
110
+ prompt: "Enter path to Pyre CSV log file:",
111
+ placeholder: "./pyre-exports/pyre-log-....csv",
112
+ defaultVal: "./pyre-exports"
113
+ },
114
+ flags: [
115
+ { flag: "--speed <n>", desc: "Playback speed multiplier (e.g. 2, 0.5)" },
116
+ { flag: "--plain", desc: "Plain text output without ANSI colors" }
117
+ ],
118
+ examples: ["pyre replay session.csv", "pyre replay session.csv --speed 2"]
119
+ },
120
+ {
121
+ id: "fleet",
122
+ name: "Fleet Monitor",
123
+ cli: "pyre fleet",
124
+ category: "dashboards",
125
+ badge: "FLEET",
126
+ description: "Multi-host live dashboard monitoring multiple servers simultaneously",
127
+ detailedHelp: "Combines telemetry streams from multiple machines over SSH or P2P into a unified multi-node dashboard.",
128
+ tags: ["fleet", "cluster", "multi-host", "servers", "ssh", "remote"],
129
+ interactive: true,
130
+ requiresArgs: {
131
+ prompt: "Enter host list (space-separated, e.g. user@host1 user@host2):",
132
+ placeholder: "user@server1.local user@server2.local",
133
+ defaultVal: "localhost"
134
+ },
135
+ examples: ["pyre fleet server1 server2 server3"]
136
+ },
137
+ {
138
+ id: "ssh",
139
+ name: "SSH Remote Stream",
140
+ cli: "pyre ssh",
141
+ category: "dashboards",
142
+ badge: "SSH",
143
+ description: "Stream live stats from a remote host securely over SSH",
144
+ detailedHelp: "Connects to a remote server via SSH, runs Pyre on the remote machine (or streams lightweight metrics), and visualizes it locally.",
145
+ tags: ["ssh", "remote", "stream", "server", "linux"],
146
+ interactive: true,
147
+ requiresArgs: {
148
+ prompt: "Enter SSH remote host (e.g. user@hostname):",
149
+ placeholder: "user@myserver.com"
150
+ },
151
+ examples: ["pyre ssh user@myserver.com"]
152
+ },
153
+ {
154
+ id: "containers",
155
+ name: "Container Telemetry",
156
+ cli: "pyre containers",
157
+ category: "dashboards",
158
+ badge: "CTNR",
159
+ description: "Inspect CPU, memory limits, and network I/O of Docker and Podman containers",
160
+ detailedHelp: "Queries active Docker, Podman, or OrbStack containers and reports container IDs, names, images, CPU percentage, memory usage against host limits, and network throughput.",
161
+ tags: ["containers", "docker", "podman", "orbstack", "cgroups", "microvm"],
162
+ examples: ["pyre containers", "pyre containers --json"]
163
+ },
164
+ // --- Health & Diagnostics ---
165
+ {
166
+ id: "check",
167
+ name: "System Health Check",
168
+ cli: "pyre check",
169
+ category: "health",
170
+ badge: "CHECK",
171
+ description: "One-line plain-English instant health and thermal check",
172
+ detailedHelp: "Performs an instant health audit of CPU load, memory pressure, thermals, battery, and storage. Returns green/yellow/red status with plain-English summary.",
173
+ tags: ["health", "check", "status", "quick", "summary", "temperature", "alert"],
174
+ flags: [
175
+ { flag: "--detailed", desc: "Include full sensor table in output" },
176
+ { flag: "--exit-code", desc: "Exit with non-zero status code if warnings/alerts are active (for CI/CD)" }
177
+ ],
178
+ examples: ["pyre check", "pyre check --detailed", "pyre check --exit-code"]
179
+ },
180
+ {
181
+ id: "doctor",
182
+ name: "Doctor Diagnostics",
183
+ cli: "pyre doctor",
184
+ category: "health",
185
+ badge: "DIAG",
186
+ description: "Comprehensive system diagnostic audit (SIP, sensors, permissions, battery)",
187
+ detailedHelp: "Audits hardware sensors, sudo powermetrics permissions, Apple Silicon SMC accessibility, System Integrity Protection (SIP), thermal pressure, and disk health.",
188
+ tags: ["doctor", "diagnostics", "audit", "permissions", "sip", "sensors", "troubleshoot"],
189
+ flags: [
190
+ { flag: "--plain", desc: "Output in plain accessible text" }
191
+ ],
192
+ examples: ["pyre doctor", "pyre doctor --plain"]
193
+ },
194
+ {
195
+ id: "smart",
196
+ name: "S.M.A.R.T. Disk Health",
197
+ cli: "pyre smart",
198
+ category: "health",
199
+ badge: "DISK",
200
+ description: "Read S.M.A.R.T. telemetry, SSD wear levels, and drive health indicators",
201
+ detailedHelp: "Queries physical drives via smartctl/NVMe telemetry. Reports remaining spare capacity, drive temperatures, wear percentage, read/write error counts, and failure warnings.",
202
+ tags: ["smart", "disk", "ssd", "nvme", "health", "storage", "hardware", "drive"],
203
+ examples: ["pyre smart"]
204
+ },
205
+ {
206
+ id: "anomalies",
207
+ name: "Anomaly Detection",
208
+ cli: "pyre anomalies",
209
+ category: "health",
210
+ badge: "ANOMALY",
211
+ description: "Detect z-score statistical resource spikes and unexpected system behaviors",
212
+ detailedHelp: "Analyzes historical metric logs using statistical z-score models. Identifies runaway background processes, memory leaks, thermal throttles, and abnormal network bursts.",
213
+ tags: ["anomalies", "zscore", "spikes", "leaks", "statistical", "digest", "history"],
214
+ flags: [
215
+ { flag: "--since <range>", desc: "Time range to analyze (default: 7d, e.g. 1d, 3d, 14d)" },
216
+ { flag: "--export-dir <dir>", desc: "Directory containing historical log files" }
217
+ ],
218
+ examples: ["pyre anomalies", "pyre anomalies --since 1d", "pyre anomalies --since 7d"]
219
+ },
220
+ {
221
+ id: "explain",
222
+ name: "AI Anomaly Explanation",
223
+ cli: "pyre explain",
224
+ category: "health",
225
+ badge: "AI",
226
+ description: "Plain-English AI diagnosis and remedy recommendations for detected anomalies",
227
+ detailedHelp: "Feeds detected performance anomalies into a local or cloud LLM (builtin zero-setup rules engine, Ollama, or OpenAI). Explains causes in plain English with concrete fix suggestions.",
228
+ tags: ["ai", "explain", "llm", "ollama", "openai", "diagnose", "remedy"],
229
+ flags: [
230
+ { flag: "--llm-backend <bkg>", desc: "Backend: builtin (zero-setup), ollama, or openai" },
231
+ { flag: "--llm-model <model>", desc: "Model name (e.g. llama3.2, gpt-4o-mini)" },
232
+ { flag: "--api-key <key>", desc: "OpenAI API key (or set OPENAI_API_KEY env)" },
233
+ { flag: "--since <range>", desc: "Time range for anomaly digestion (default: 7d)" }
234
+ ],
235
+ examples: ["pyre explain", "pyre explain --llm-backend ollama --llm-model llama3.2"]
236
+ },
237
+ {
238
+ id: "alert-test",
239
+ name: "Test Alert Notification",
240
+ cli: "pyre alert test",
241
+ category: "health",
242
+ badge: "ALERT",
243
+ description: "Send test notifications to Slack, Discord, Pushover, and ntfy.sh",
244
+ detailedHelp: "Dispatches a test notification through all configured webhook and push notification channels to verify that alert webhooks are functioning properly.",
245
+ tags: ["alert", "notification", "slack", "discord", "pushover", "ntfy", "test", "webhook"],
246
+ flags: [
247
+ { flag: "--alert-slack <url>", desc: "Test a Slack webhook URL" },
248
+ { flag: "--alert-discord <url>", desc: "Test a Discord webhook URL" },
249
+ { flag: "--alert-ntfy <url>", desc: "Test an ntfy.sh topic URL" }
250
+ ],
251
+ examples: ["pyre alert test"]
252
+ },
253
+ {
254
+ id: "flight",
255
+ name: "Forensic Flight Recorder",
256
+ cli: "pyre flight",
257
+ category: "health",
258
+ badge: "BLACKBOX",
259
+ description: "In-memory black box recording recent telemetry for post-mortem analysis",
260
+ detailedHelp: "Maintains a rolling circular buffer of high-resolution telemetry in RAM. Generates forensic reports showing peak CPU, memory, and top runaway processes leading up to a system freeze or crash.",
261
+ tags: ["flight", "blackbox", "forensic", "dump", "crash", "freeze", "ram"],
262
+ flags: [
263
+ { flag: "dump", desc: "Generate post-mortem forensic report file" },
264
+ { flag: "status", desc: "Display active buffer frames and RAM footprint" },
265
+ { flag: "record", desc: "Record fresh frames for a duration" }
266
+ ],
267
+ examples: ["pyre flight dump", "pyre flight status", "pyre flight record --duration 30"]
268
+ },
269
+ {
270
+ id: "inspect",
271
+ name: "Process Deep-Dive Inspector",
272
+ cli: "pyre inspect",
273
+ category: "health",
274
+ badge: "PROC",
275
+ description: "Inspect open file handles (lsof), active sockets, threads, and adjust nice priority",
276
+ detailedHelp: "Examines low-level process attributes for any PID: open files, network sockets, parent hierarchy, memory RSS, and allows dynamic priority adjustment via renice.",
277
+ tags: ["inspect", "lsof", "sockets", "files", "nice", "renice", "priority", "pid"],
278
+ requiresArgs: {
279
+ prompt: "Enter PID to inspect:",
280
+ placeholder: "1234"
281
+ },
282
+ flags: [
283
+ { flag: "--nice <val>", desc: "Adjust nice priority (-20 to 19)" },
284
+ { flag: "--json", desc: "Output details in structured JSON" }
285
+ ],
286
+ examples: ["pyre inspect 1234", "pyre inspect 1234 --nice 10"]
287
+ },
288
+ // --- Performance & Benchmarks ---
289
+ {
290
+ id: "benchmark",
291
+ name: "CPU Benchmark",
292
+ cli: "pyre benchmark",
293
+ category: "performance",
294
+ badge: "BENCH",
295
+ description: "Run standard 1-minute CPU benchmark calculating digits of PI",
296
+ detailedHelp: "Executes a multi-threaded compute benchmark calculating digits of Pi. Measures floating-point and integer performance, thermal climb, power wattage, and gives a standardized score.",
297
+ tags: ["benchmark", "cpu", "pi", "performance", "speed", "score", "stress"],
298
+ examples: ["pyre benchmark"]
299
+ },
300
+ {
301
+ id: "bench",
302
+ name: "Command Execution Benchmark",
303
+ cli: "pyre bench",
304
+ category: "performance",
305
+ badge: "BENCH",
306
+ description: "Profile execution time, CPU, memory, power draw, and real kWh cost of any command",
307
+ detailedHelp: "Wraps any shell command and monitors its full lifecycle: execution time, peak memory, average CPU, total joules consumed, and estimated electrical cost in dollars/kWh.",
308
+ tags: ["bench", "profile", "energy", "power", "kwh", "cost", "command", "exec"],
309
+ requiresArgs: {
310
+ prompt: "Enter shell command to benchmark (e.g. npm test):",
311
+ placeholder: "npm test",
312
+ defaultVal: "ls -la"
313
+ },
314
+ examples: ['pyre bench "npm run build"', 'pyre bench "cargo build --release"']
315
+ },
316
+ {
317
+ id: "stress",
318
+ name: "System Stress Test",
319
+ cli: "pyre stress",
320
+ category: "performance",
321
+ badge: "LOAD",
322
+ description: "Synthetic multi-threaded CPU load generator to test cooling and stability",
323
+ detailedHelp: "Spawns worker threads to saturate all CPU cores. Ideal for testing cooling efficiency, fan curves, thermal throttling thresholds, and power supply stability.",
324
+ tags: ["stress", "load", "burnin", "thermal", "cooling", "fans", "throttle"],
325
+ flags: [
326
+ { flag: "-d, --duration <sec>", desc: "Duration in seconds (default: 30s)" },
327
+ { flag: "--cores <n>", desc: "Number of worker threads (default: all cores)" }
328
+ ],
329
+ examples: ["pyre stress --duration 10", "pyre stress --duration 60 --cores 8"]
330
+ },
331
+ {
332
+ id: "profile-proc",
333
+ name: "Process Call-Tree Profiler",
334
+ cli: "pyre profile-proc",
335
+ category: "performance",
336
+ badge: "PROF",
337
+ description: "CPU call-tree profiler and hot function sampler for any running PID",
338
+ detailedHelp: "Attaches to a running process and takes stack trace samples to pinpoint CPU hotspots, spin-locks, and slow function call chains.",
339
+ tags: ["profile-proc", "profiler", "sampling", "calltree", "hotspots", "stacktrace", "pid"],
340
+ requiresArgs: {
341
+ prompt: "Enter PID to profile:",
342
+ placeholder: "1234"
343
+ },
344
+ flags: [
345
+ { flag: "-d, --duration <sec>", desc: "Sampling duration in seconds (default: 5s)" },
346
+ { flag: "--raw", desc: "Output unparsed profiler raw text" }
347
+ ],
348
+ examples: ["pyre profile-proc 1234", "pyre profile-proc 1234 --duration 10"]
349
+ },
350
+ {
351
+ id: "run",
352
+ name: "Performance Budgeting Runner",
353
+ cli: "pyre run",
354
+ category: "performance",
355
+ badge: "GATE",
356
+ description: "Execute command and enforce CPU, memory, or runtime ceilings for CI/CD gates",
357
+ detailedHelp: "Wraps any shell command and monitors its full execution lifecycle. Enforces performance ceilings (--budget-cpu, --budget-mem, --budget-time), exiting non-zero if limits are breached. Perfect for regression testing in CI.",
358
+ tags: ["run", "budget", "ci", "gate", "threshold", "ceiling", "benchmark"],
359
+ requiresArgs: {
360
+ prompt: "Enter command to run with budget (e.g. --budget-time 10 -- ls -la):",
361
+ placeholder: "--budget-cpu 80 -- ls -la",
362
+ defaultVal: "--budget-cpu 90 -- ls -la"
363
+ },
364
+ flags: [
365
+ { flag: "--budget-cpu <pct>", desc: "Maximum allowable peak CPU %" },
366
+ { flag: "--budget-mem <bytes>", desc: "Maximum allowable peak memory (e.g. 500MB, 2GB)" },
367
+ { flag: "--budget-time <sec>", desc: "Maximum allowable duration in seconds" },
368
+ { flag: "--markdown", desc: "Output formatted markdown report" }
369
+ ],
370
+ examples: ["pyre run --budget-time 30 -- npm test", "pyre run --budget-mem 1GB --budget-cpu 80 -- make"]
371
+ },
372
+ {
373
+ id: "bench-git",
374
+ name: "Git Branch Benchmark Differ",
375
+ cli: "pyre bench-git",
376
+ category: "performance",
377
+ badge: "GIT",
378
+ description: "Benchmark a command across two git branches and compute percentage deltas",
379
+ detailedHelp: "Checks out branch A, measures duration, memory, and CPU usage, checks out branch B, measures again, and outputs a formatted side-by-side comparison with green/red regression indicators.",
380
+ tags: ["bench-git", "git", "branch", "compare", "delta", "regression", "diff"],
381
+ requiresArgs: {
382
+ prompt: 'Enter branchA branchB "command":',
383
+ placeholder: 'main feature "npm test"',
384
+ defaultVal: 'main HEAD "npm test"'
385
+ },
386
+ examples: ['pyre bench-git main feature "npm test"']
387
+ },
388
+ // --- System & Maintenance ---
389
+ {
390
+ id: "optimize",
391
+ name: "Mac & System Optimizer",
392
+ cli: "pyre optimize",
393
+ category: "maintenance",
394
+ badge: "CLEAN",
395
+ description: "Mac optimizer: quit idle apps, purge memory, clean caches, empty trash",
396
+ detailedHelp: "Intelligently manages system resources: discovers foreground apps idle for hours and gracefully terminates them, flushes inactive memory caches, cleans build artifacts, and purges DNS caches.",
397
+ tags: ["optimize", "clean", "idle", "memory", "caches", "trash", "purge", "mac"],
398
+ flags: [
399
+ { flag: "apps", desc: "Quit long-idle background applications" },
400
+ { flag: "memory", desc: "Purge inactive memory pages" },
401
+ { flag: "caches", desc: "Clean system & user application cache files" },
402
+ { flag: "trash", desc: "Safely empty user Trash" },
403
+ { flag: "dns", desc: "Flush local DNS resolver cache" },
404
+ { flag: "--idle <dur>", desc: "Idle time threshold (e.g. 30m, 2h)" },
405
+ { flag: "-y, --yes", desc: "Skip interactive confirmation prompt" }
406
+ ],
407
+ examples: ["pyre optimize apps", "pyre optimize memory", "pyre optimize caches", "pyre optimize dns"]
408
+ },
409
+ {
410
+ id: "brew",
411
+ name: "Package Manager Health",
412
+ cli: "pyre brew",
413
+ category: "maintenance",
414
+ badge: "PKGS",
415
+ description: "Package manager health panel (Homebrew, apt, pacman, winget)",
416
+ detailedHelp: "Checks outdated packages, broken symlinks, orphaned dependencies, disk footprint of installed packages, and cache usage for Homebrew, apt, pacman, or winget.",
417
+ tags: ["brew", "homebrew", "apt", "packages", "outdated", "clean", "disk", "winget"],
418
+ examples: ["pyre brew"]
419
+ },
420
+ {
421
+ id: "extensions",
422
+ name: "System Extensions & Drivers",
423
+ cli: "pyre extensions",
424
+ category: "maintenance",
425
+ badge: "DRVR",
426
+ description: "Inspect active macOS system extensions, network filters, and endpoint drivers",
427
+ detailedHelp: "Scans driver extensions, network filter extensions, and endpoint security agents (CrowdStrike, SentinelOne, Little Snitch, VPNs) for performance impacts.",
428
+ tags: ["extensions", "drivers", "kext", "security", "antivirus", "filter", "macos"],
429
+ examples: ["pyre extensions"]
430
+ },
431
+ {
432
+ id: "watchdog",
433
+ name: "Autonomous Watchdog Daemon",
434
+ cli: "pyre watchdog",
435
+ category: "maintenance",
436
+ badge: "DAEMON",
437
+ description: "Autonomous background daemon enforcing memory limits and auto-killing rogues",
438
+ detailedHelp: "Runs a background rule engine that watches for run-away processes exceeding CPU or memory limits, dispatches alert notifications, and optionally terminates offenders.",
439
+ tags: ["watchdog", "daemon", "rules", "kill", "limits", "autonomous", "protect"],
440
+ flags: [
441
+ { flag: "--watchdog-config <file>", desc: "Path to custom rules JSON" },
442
+ { flag: "--dry-run", desc: "Simulate rules without executing kills" },
443
+ { flag: "--interval <sec>", desc: "Polling interval in seconds" }
444
+ ],
445
+ examples: ["pyre watchdog --dry-run", "pyre watchdog"]
446
+ },
447
+ {
448
+ id: "build",
449
+ name: "Compiler & Build Tracker",
450
+ cli: "pyre build",
451
+ category: "maintenance",
452
+ badge: "BUILD",
453
+ description: "Track active build systems (Xcode, cargo, clang, rustc, webpack, go)",
454
+ detailedHelp: "Detects active build processes across the system, monitors parallel compiler thread usage, CPU load, and estimated build completion.",
455
+ tags: ["build", "compile", "xcode", "cargo", "clang", "rustc", "webpack", "ninja"],
456
+ flags: [
457
+ { flag: "--json", desc: "Output build data as JSON" }
458
+ ],
459
+ examples: ["pyre build"]
460
+ },
461
+ {
462
+ id: "update",
463
+ name: "Check for Updates",
464
+ cli: "pyre update",
465
+ category: "maintenance",
466
+ badge: "UPD",
467
+ description: "Check for Pyre CLI updates from npm and GitHub releases",
468
+ detailedHelp: "Queries the registry to compare the running version with the latest release, showing change highlights and one-line upgrade commands.",
469
+ tags: ["update", "upgrade", "version", "npm", "new"],
470
+ examples: ["pyre update"]
471
+ },
472
+ {
473
+ id: "playbook",
474
+ name: "Macro Recipes & Playbooks",
475
+ cli: "pyre playbook",
476
+ category: "maintenance",
477
+ badge: "RECIPE",
478
+ description: "Execute sequenced multi-step audit, benchmark, or maintenance playbooks",
479
+ detailedHelp: "Runs pre-configured or custom multi-step Pyre sequences with progress reporting. Includes built-in recipes: audit (health+doctor+smart+brew), clean (optimize apps+mem+caches+dns), and triage (anomalies+netusage+extensions).",
480
+ tags: ["playbook", "recipe", "macro", "sequence", "audit", "clean", "triage"],
481
+ flags: [
482
+ { flag: "list", desc: "List all available playbooks" },
483
+ { flag: "run <id>", desc: "Run a playbook (audit, clean, triage, bench)" }
484
+ ],
485
+ examples: ["pyre playbook list", "pyre playbook run audit", "pyre playbook run clean"]
486
+ },
487
+ // --- Network & Remote ---
488
+ {
489
+ id: "netusage",
490
+ name: "Application Network Usage",
491
+ cli: "pyre netusage",
492
+ category: "network",
493
+ badge: "NET",
494
+ description: "Real-time per-application network bandwidth and socket connection breakdown",
495
+ detailedHelp: "Attributes active network throughput to individual running applications and processes. Shows upload/download rates, open sockets, and remote IP addresses.",
496
+ tags: ["netusage", "network", "bandwidth", "sockets", "rx", "tx", "apps", "connections"],
497
+ flags: [
498
+ { flag: "--top <n>", desc: "Number of top processes to show (default: 15)" },
499
+ { flag: "--sort <rx|tx|total>", desc: "Sort by download, upload, or total" },
500
+ { flag: "--json", desc: "Output in structured JSON" }
501
+ ],
502
+ examples: ["pyre netusage", "pyre netusage --top 10 --sort rx"]
503
+ },
504
+ {
505
+ id: "topo",
506
+ name: "Network Topology & Peer Discovery",
507
+ cli: "pyre topo",
508
+ category: "network",
509
+ badge: "TOPO",
510
+ description: "Map local network neighbors, gateways, and discover Pyre P2P peers",
511
+ detailedHelp: "Scans the local subnet for active devices, routers, access points, and peer Pyre instances broadcasting live metrics.",
512
+ tags: ["topo", "topology", "lan", "arp", "discovery", "mesh", "peers", "scanner"],
513
+ flags: [
514
+ { flag: "--scan", desc: "Perform active network sweep on LAN neighbors" },
515
+ { flag: "--json", desc: "Output topology as JSON" }
516
+ ],
517
+ examples: ["pyre topo", "pyre topo --scan"]
518
+ },
519
+ {
520
+ id: "p2p",
521
+ name: "P2P Mesh Streaming",
522
+ cli: "pyre p2p",
523
+ category: "network",
524
+ badge: "P2P",
525
+ description: "Encrypted peer-to-peer real-time metrics streaming between two systems over TCP/TLS",
526
+ detailedHelp: "Enables real-time system monitoring over local networks without requiring cloud relays. One machine runs `server` while the second connects as `client`.",
527
+ tags: ["p2p", "mesh", "tcp", "tls", "stream", "remote", "peer"],
528
+ flags: [
529
+ { flag: "server", desc: "Start streaming server on this machine" },
530
+ { flag: "connect", desc: "Connect to remote server instance" },
531
+ { flag: "--p2p-port <port>", desc: "Port number (default: 9876)" },
532
+ { flag: "--p2p-password <pwd>", desc: "Shared authentication password" },
533
+ { flag: "--p2p-tls", desc: "Enable TLS encryption" }
534
+ ],
535
+ examples: ["pyre server", "pyre p2p server --p2p-password secret", "pyre p2p connect --p2p-host 192.168.1.10 --p2p-password secret"]
536
+ },
537
+ {
538
+ id: "server",
539
+ name: "P2P Quick Setup Helper",
540
+ cli: "pyre server",
541
+ category: "network",
542
+ badge: "P2P",
543
+ description: "Generate copy-paste ready commands for P2P server and client connection",
544
+ detailedHelp: "Detects the local IP address, generates a cryptographically secure random session password, and prints exact commands to run on both machines.",
545
+ tags: ["server", "p2p", "helper", "quickstart", "connect"],
546
+ examples: ["pyre server"]
547
+ },
548
+ {
549
+ id: "serve",
550
+ name: "REST API Server",
551
+ cli: "pyre serve",
552
+ category: "network",
553
+ badge: "API",
554
+ description: "Lightweight structured JSON REST API server for external scrapers",
555
+ detailedHelp: "Runs a headless HTTP daemon providing JSON endpoints (`/api/stats`, `/api/health`, `/api/processes`) for custom integrations, Grafana, or monitoring tools.",
556
+ tags: ["serve", "rest", "api", "http", "json", "endpoints"],
557
+ flags: [
558
+ { flag: "--port <port>", desc: "Port number (default: 8080)" },
559
+ { flag: "--api-key <key>", desc: "Require Bearer API key authentication" },
560
+ { flag: "--detailed", desc: "Include full sensor readings in API responses" }
561
+ ],
562
+ examples: ["pyre serve --port 8080"]
563
+ },
564
+ {
565
+ id: "prometheus",
566
+ name: "Prometheus Exporter",
567
+ cli: "pyre prometheus",
568
+ category: "network",
569
+ badge: "PROM",
570
+ description: "Start standard Prometheus metrics exporter on localhost:9090",
571
+ detailedHelp: "Exposes Pyre metrics at `/metrics` formatted for Prometheus, Grafana, Datadog, and OpenTelemetry collectors.",
572
+ tags: ["prometheus", "grafana", "metrics", "exporter", "opentelemetry"],
573
+ flags: [
574
+ { flag: "--prometheus-port <port>", desc: "Port number (default: 9090)" },
575
+ { flag: "--interval <sec>", desc: "Scrape collection interval (default: 15s)" }
576
+ ],
577
+ examples: ["pyre prometheus", "pyre prometheus --prometheus-port 9100"]
578
+ },
579
+ // --- Configuration & Utilities ---
580
+ {
581
+ id: "info",
582
+ name: "Hardware Summary",
583
+ cli: "pyre info",
584
+ category: "config",
585
+ badge: "INFO",
586
+ description: "Concise summary of CPU architecture, memory, GPU, and OS build",
587
+ detailedHelp: "Prints a clean overview of processor brand, core configuration (Performance vs Efficiency cores), RAM speed, GPU adapter, and kernel release.",
588
+ tags: ["info", "hardware", "specs", "system", "cpu", "ram", "gpu", "os"],
589
+ examples: ["pyre info"]
590
+ },
591
+ {
592
+ id: "history",
593
+ name: "Historical Resource Trends",
594
+ cli: "pyre history",
595
+ category: "config",
596
+ badge: "HIST",
597
+ description: "Graph historical resource trends from recorded CSV logs",
598
+ detailedHelp: "Parses logged CSV telemetry files and displays multi-day terminal trend graphs for CPU, memory, and thermal levels.",
599
+ tags: ["history", "trends", "csv", "graph", "days", "analytics"],
600
+ flags: [
601
+ { flag: "--days <n>", desc: "Number of days to analyze (default: 7)" }
602
+ ],
603
+ examples: ["pyre history", "pyre history --days 14"]
604
+ },
605
+ {
606
+ id: "diff",
607
+ name: "Snapshot Side-by-Side Diff",
608
+ cli: "pyre diff",
609
+ category: "config",
610
+ badge: "DIFF",
611
+ description: "Compare two saved snapshot JSON files side-by-side",
612
+ detailedHelp: "Highlights system state changes between two points in time (e.g. before and after running a build or benchmark): process differences, memory shifts, and disk deltas.",
613
+ tags: ["diff", "compare", "snapshots", "deltas", "json"],
614
+ requiresArgs: {
615
+ prompt: "Enter two snapshot JSON file paths (file1 file2):",
616
+ placeholder: "snap1.json snap2.json"
617
+ },
618
+ examples: ["pyre diff snap1.json snap2.json"]
619
+ },
620
+ {
621
+ id: "config",
622
+ name: "Persistent Configuration",
623
+ cli: "pyre config",
624
+ category: "config",
625
+ badge: "CFG",
626
+ description: "View or reset persistent ~/.config/pyre/config.json settings",
627
+ detailedHelp: "Displays all stored user preferences: active theme, refresh rates, alert thresholds, webhook URLs, default launch mode, and panel visibility.",
628
+ tags: ["config", "settings", "defaults", "preferences", "reset", "json"],
629
+ flags: [
630
+ { flag: "show", desc: "Display current config JSON" },
631
+ { flag: "reset", desc: "Reset all settings to factory defaults" }
632
+ ],
633
+ examples: ["pyre config show", "pyre config reset"]
634
+ },
635
+ {
636
+ id: "profile",
637
+ name: "Config Profiles",
638
+ cli: "pyre profile",
639
+ category: "config",
640
+ badge: "PROF",
641
+ description: "Save, load, and manage atomic configuration profiles",
642
+ detailedHelp: 'Switch effortlessly between specialized monitoring setups (e.g. "gaming", "server", "coding", "minimal") by saving and loading named profiles.',
643
+ tags: ["profile", "presets", "save", "load", "switch", "themes"],
644
+ flags: [
645
+ { flag: "list", desc: "List all saved profiles" },
646
+ { flag: "save <name>", desc: "Save current state as a new profile" },
647
+ { flag: "load <name>", desc: "Activate an existing profile" }
648
+ ],
649
+ examples: ["pyre profile list", "pyre profile save coding", "pyre profile load coding"]
650
+ },
651
+ {
652
+ id: "sync",
653
+ name: "Cloud Profile Sync",
654
+ cli: "pyre sync",
655
+ category: "config",
656
+ badge: "SYNC",
657
+ description: "Synchronize config profiles across multiple Macs via iCloud/Drive/Firebase",
658
+ detailedHelp: "Synchronizes your custom themes, bookmarks, and alert rules across multiple workstations using iCloud Drive, Dropbox, or Firebase Firestore.",
659
+ tags: ["sync", "cloud", "icloud", "drive", "firebase", "workstations"],
660
+ flags: [
661
+ { flag: "status", desc: "Check synchronization state" },
662
+ { flag: "push", desc: "Upload local configuration to cloud" },
663
+ { flag: "pull", desc: "Download configuration from cloud" },
664
+ { flag: "--sync-dir <dir>", desc: "Custom cloud folder path" }
665
+ ],
666
+ examples: ["pyre sync status", "pyre sync push", "pyre sync pull"]
667
+ },
668
+ {
669
+ id: "blender",
670
+ name: "Blender Render Tracker",
671
+ cli: "pyre blender",
672
+ category: "config",
673
+ badge: "3D",
674
+ description: "Track active Blender 3D render jobs, frame progress, and engines",
675
+ detailedHelp: "Auto-detects active Blender render instances, displaying current frame number, total frames, rendering engine (Cycles/Eevee), and elapsed time.",
676
+ tags: ["blender", "render", "3d", "cycles", "eevee", "frames", "gpu"],
677
+ examples: ["pyre blender"]
678
+ },
679
+ {
680
+ id: "xbar",
681
+ name: "Menu Bar Plugin Generator",
682
+ cli: "pyre xbar",
683
+ category: "config",
684
+ badge: "MENU",
685
+ description: "Generate an xbar / SwiftBar macOS menu bar plugin script",
686
+ detailedHelp: "Creates a lightweight script that lives in your macOS menu bar, showing glanceable live CPU/memory stats and thermal warnings.",
687
+ tags: ["xbar", "swiftbar", "menubar", "macos", "plugin", "tray"],
688
+ examples: ["pyre xbar"]
689
+ },
690
+ {
691
+ id: "completions",
692
+ name: "Shell Auto-Completions",
693
+ cli: "pyre completions",
694
+ category: "config",
695
+ badge: "SHELL",
696
+ description: "Generate shell auto-completion scripts (zsh, bash, fish, PowerShell)",
697
+ detailedHelp: "Outputs full tab-completion scripts for your favorite shell, enabling instant command and flag autocomplete.",
698
+ tags: ["completions", "shell", "zsh", "bash", "fish", "powershell", "autocomplete"],
699
+ requiresArgs: {
700
+ prompt: "Select shell (zsh, bash, fish, powershell):",
701
+ placeholder: "zsh",
702
+ defaultVal: "zsh"
703
+ },
704
+ examples: ["pyre completions zsh", "pyre completions fish", "pyre completions bash"]
705
+ },
706
+ {
707
+ id: "statusline",
708
+ name: "Headless Terminal Statusline",
709
+ cli: "pyre statusline",
710
+ category: "config",
711
+ badge: "STATUS",
712
+ description: "Ultra-fast telemetry string for Tmux, Starship prompt, Waybar, or Neovim",
713
+ detailedHelp: "Emits sub-millisecond status strings formatted for Tmux status-right, Starship shell prompt, Pango markup (Waybar/Polybar), or minimal JSON for Neovim lualine.",
714
+ tags: ["statusline", "tmux", "starship", "waybar", "polybar", "neovim", "prompt"],
715
+ flags: [
716
+ { flag: "--format tmux", desc: "Format with Tmux color codes" },
717
+ { flag: "--format starship", desc: "Plain formatted string for Starship" },
718
+ { flag: "--format json", desc: "Raw minimal JSON object" },
719
+ { flag: "--format pango", desc: "Pango XML for Waybar and Polybar" }
720
+ ],
721
+ examples: ["pyre statusline --format tmux", "pyre statusline --format starship", "pyre statusline --format json"]
722
+ }
723
+ ];
724
+ function filterCommands(commands, category, query) {
725
+ let list = commands;
726
+ if (category !== "all") {
727
+ list = list.filter((c) => c.category === category);
728
+ }
729
+ const q = query.trim().toLowerCase();
730
+ if (!q) {
731
+ return list;
732
+ }
733
+ return list.map((cmd) => {
734
+ let score = 0;
735
+ const id = cmd.id.toLowerCase();
736
+ const cli = cmd.cli.toLowerCase();
737
+ const name = cmd.name.toLowerCase();
738
+ const desc = cmd.description.toLowerCase();
739
+ if (id === q || cli === q) score += 100;
740
+ else if (id.startsWith(q) || cli.startsWith(q)) score += 50;
741
+ else if (name.toLowerCase().includes(q)) score += 30;
742
+ else if (cmd.tags.some((t) => t.toLowerCase() === q)) score += 40;
743
+ else if (cmd.tags.some((t) => t.toLowerCase().includes(q))) score += 20;
744
+ else if (desc.includes(q)) score += 10;
745
+ else if (cmd.detailedHelp.toLowerCase().includes(q)) score += 5;
746
+ return { cmd, score };
747
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).map((item) => item.cmd);
748
+ }
749
+
750
+ // src/menu/render.ts
751
+ import chalk from "chalk";
752
+ function renderMenuScreen(state, filteredCommands) {
753
+ const width = Math.max(60, state.termWidth || 80);
754
+ const height = Math.max(16, state.termHeight || 24);
755
+ const theme = THEMES[state.theme] || THEMES.default;
756
+ const accent = theme.cpu || chalk.cyan;
757
+ const border = theme.border || chalk.dim;
758
+ const highlight = theme.graphs || chalk.hex("#ff6a39");
759
+ const lines = [];
760
+ const title = ` \u{1F525} pyre \u2014 Command Center & Tools Navigator `;
761
+ const headerLeft = accent(chalk.bold(title));
762
+ const headerRight = chalk.dim(`[?] Help [q] Quit `);
763
+ const topBarInner = headerLeft + "\u2500".repeat(Math.max(0, width - visLen(title) - visLen(headerRight) - 2)) + headerRight;
764
+ lines.push(border("\u256D") + topBarInner + border("\u256E"));
765
+ const subtitle = " Browse, search and execute all 38 pyre commands without memorization";
766
+ lines.push(border("\u2502") + fitVisible(chalk.dim(subtitle), width - 2) + border("\u2502"));
767
+ let searchInput = state.searchQuery;
768
+ if (state.inputMode === "search") {
769
+ searchInput += chalk.bgWhite.black(" ");
770
+ }
771
+ const searchPrompt = ` \u{1F50D} Search: `;
772
+ const searchContent = searchPrompt + (state.searchQuery ? chalk.bold.yellow(searchInput) : chalk.dim("Type command, keyword or description... (e.g. doctor, clean, smart)"));
773
+ const matchCount = chalk.dim(`(${filteredCommands.length}/${38} tools) `);
774
+ const searchBar = fitVisible(searchContent, width - visLen(matchCount) - 2) + matchCount;
775
+ lines.push(border("\u251C") + border("\u2500".repeat(width - 2)) + border("\u2524"));
776
+ lines.push(border("\u2502") + fitVisible(searchBar, width - 2) + border("\u2502"));
777
+ const catTabs = CATEGORIES.map((cat) => {
778
+ const isSelected = cat.id === state.category;
779
+ if (isSelected) {
780
+ return chalk.bgHex("#ff6a39").black(` ${cat.icon} ${cat.name} `);
781
+ }
782
+ return chalk.dim(` ${cat.icon} ${cat.name} `);
783
+ }).join(chalk.dim("\u2502"));
784
+ lines.push(border("\u2502") + fitVisible(" " + catTabs, width - 2) + border("\u2502"));
785
+ lines.push(border("\u251C") + border("\u2500".repeat(width - 2)) + border("\u2524"));
786
+ const bodyHeight = Math.max(8, height - 9);
787
+ const isDualPane = width >= 80;
788
+ const listWidth = isDualPane ? Math.min(46, Math.floor(width * 0.42)) : width - 2;
789
+ const inspectorWidth = width - 2 - listWidth - (isDualPane ? 1 : 0);
790
+ const listLines = [];
791
+ const selectedIdx = Math.min(state.selectedIndex, Math.max(0, filteredCommands.length - 1));
792
+ let startIdx = state.scrollOffset;
793
+ if (selectedIdx < startIdx) {
794
+ startIdx = selectedIdx;
795
+ } else if (selectedIdx >= startIdx + bodyHeight) {
796
+ startIdx = selectedIdx - bodyHeight + 1;
797
+ }
798
+ state.scrollOffset = startIdx;
799
+ const endIdx = Math.min(filteredCommands.length, startIdx + bodyHeight);
800
+ if (filteredCommands.length === 0) {
801
+ listLines.push(chalk.yellow(" No commands match your search."));
802
+ listLines.push(chalk.dim(" Press Esc or backspace to clear query."));
803
+ } else {
804
+ for (let i = startIdx; i < endIdx; i++) {
805
+ const cmd = filteredCommands[i];
806
+ const isCur = i === selectedIdx;
807
+ const badgeText = `[${cmd.badge}]`;
808
+ if (isCur) {
809
+ const rowText = ` \u25B6 ${cmd.cli} ${badgeText}`;
810
+ listLines.push(chalk.bgHex("#ff6a39").black(fitVisible(rowText, listWidth)));
811
+ } else {
812
+ const badgeStyled = chalk.dim(badgeText);
813
+ const cliStyled = chalk.bold(cmd.cli);
814
+ const rowText = ` ${cliStyled} ${badgeStyled}`;
815
+ listLines.push(fitVisible(rowText, listWidth));
816
+ }
817
+ }
818
+ }
819
+ while (listLines.length < bodyHeight) {
820
+ listLines.push(" ".repeat(listWidth));
821
+ }
822
+ const inspectorLines = [];
823
+ const curCmd = filteredCommands[selectedIdx];
824
+ if (curCmd && isDualPane) {
825
+ inspectorLines.push(accent(chalk.bold(` ${curCmd.name} `)) + chalk.dim(`[${curCmd.badge}]`));
826
+ inspectorLines.push(chalk.dim(` Syntax: `) + highlight(chalk.bold(curCmd.cli)));
827
+ inspectorLines.push(chalk.dim("\u2500".repeat(inspectorWidth - 2)));
828
+ inspectorLines.push(fitVisible(` ${curCmd.description}`, inspectorWidth - 2));
829
+ inspectorLines.push("");
830
+ inspectorLines.push(fitVisible(` ${curCmd.detailedHelp}`, inspectorWidth - 2));
831
+ inspectorLines.push("");
832
+ if (curCmd.flags && curCmd.flags.length > 0) {
833
+ inspectorLines.push(chalk.bold.dim(" Options & Flags:"));
834
+ for (const f of curCmd.flags.slice(0, 4)) {
835
+ const flagLabel = chalk.yellow(` ${f.flag}`);
836
+ const flagDesc = chalk.dim(` - ${f.desc}`);
837
+ inspectorLines.push(fitVisible(`${flagLabel}${flagDesc}`, inspectorWidth - 2));
838
+ }
839
+ inspectorLines.push("");
840
+ }
841
+ if (curCmd.examples && curCmd.examples.length > 0) {
842
+ inspectorLines.push(chalk.bold.dim(" Examples:"));
843
+ for (const ex of curCmd.examples.slice(0, 2)) {
844
+ inspectorLines.push(chalk.cyan(` $ ${ex}`));
845
+ }
846
+ inspectorLines.push("");
847
+ }
848
+ inspectorLines.push(chalk.dim("\u2500".repeat(inspectorWidth - 2)));
849
+ const runPrompt = curCmd.requiresArgs ? chalk.yellow(" [Enter] Enter arguments & Run [c] Copy [e] Edit Flags") : chalk.green(" [Enter] Execute Command [c] Copy [e] Edit Flags");
850
+ inspectorLines.push(fitVisible(runPrompt, inspectorWidth - 2));
851
+ }
852
+ while (inspectorLines.length < bodyHeight) {
853
+ inspectorLines.push(" ".repeat(inspectorWidth));
854
+ }
855
+ for (let r = 0; r < bodyHeight; r++) {
856
+ const left = fitVisible(listLines[r] || "", listWidth);
857
+ if (isDualPane) {
858
+ const right = fitVisible(inspectorLines[r] || "", inspectorWidth);
859
+ lines.push(border("\u2502") + left + border("\u2502") + right + border("\u2502"));
860
+ } else {
861
+ lines.push(border("\u2502") + left + border("\u2502"));
862
+ }
863
+ }
864
+ lines.push(border("\u251C") + border("\u2500".repeat(width - 2)) + border("\u2524"));
865
+ const statusMsg = state.statusMessage ? chalk.yellow(` ${state.statusMessage} `) : "";
866
+ const shortcuts = ` [\u2191/\u2193] Select [Enter] Run [Tab] Category [/] Search [e] Edit [?] Help [q] Quit`;
867
+ const footerRow = fitVisible(shortcuts, width - visLen(statusMsg) - 2) + statusMsg;
868
+ lines.push(border("\u2502") + fitVisible(footerRow, width - 2) + border("\u2502"));
869
+ lines.push(border("\u2570") + border("\u2500".repeat(width - 2)) + border("\u256F"));
870
+ if (state.inputMode === "prompt-args" && state.activeCommand) {
871
+ overlayPromptModal(lines, width, height, state.activeCommand, state.argPromptValue, accent, border);
872
+ } else if (state.inputMode === "edit-cmd" && state.activeCommand) {
873
+ overlayEditCmdModal(lines, width, height, state.activeCommand, state.argPromptValue, accent, border);
874
+ } else if (state.inputMode === "help") {
875
+ overlayHelpModal(lines, width, height, accent, border);
876
+ }
877
+ return lines.join("\r\n");
878
+ }
879
+ function overlayPromptModal(lines, width, height, cmd, val, accent, border) {
880
+ const modalW = Math.min(width - 4, 64);
881
+ const promptText = cmd.requiresArgs?.prompt || "Enter arguments:";
882
+ const modalLines = [
883
+ chalk.bold.yellow(` Run: ${cmd.cli}`),
884
+ chalk.dim(` ${cmd.description}`),
885
+ "",
886
+ chalk.white(` ${promptText}`),
887
+ ` > ${chalk.bold.cyan(val)}${chalk.bgWhite.black(" ")}`,
888
+ "",
889
+ chalk.dim(" [Enter] Run Command [Esc] Cancel")
890
+ ];
891
+ const box = panel(` Execute: ${cmd.name} `, modalLines, modalW, accent, border, modalLines.length);
892
+ stampOverlay(lines, box, width, height);
893
+ }
894
+ function overlayEditCmdModal(lines, width, height, cmd, val, accent, border) {
895
+ const modalW = Math.min(width - 4, 70);
896
+ const modalLines = [
897
+ chalk.bold.yellow(` Customize Command Line:`),
898
+ chalk.dim(` Edit flags or arguments and press Enter to execute.`),
899
+ "",
900
+ ` $ ${chalk.bold.cyan(val)}${chalk.bgWhite.black(" ")}`,
901
+ "",
902
+ chalk.dim(" [Enter] Execute Custom Command [Esc] Cancel")
903
+ ];
904
+ const box = panel(` Edit & Run `, modalLines, modalW, accent, border, modalLines.length);
905
+ stampOverlay(lines, box, width, height);
906
+ }
907
+ function overlayHelpModal(lines, width, height, accent, border) {
908
+ const modalW = Math.min(width - 4, 66);
909
+ const modalLines = [
910
+ chalk.bold.yellow(" Pyre Command Center \u2014 Keyboard Shortcuts"),
911
+ "",
912
+ ` ${chalk.cyan("\u2191 / k, \u2193 / j")} Navigate command list`,
913
+ ` ${chalk.cyan("PgUp / PgDn")} Scroll command list fast`,
914
+ ` ${chalk.cyan("Tab / Shift+Tab")} Cycle category filters`,
915
+ ` ${chalk.cyan("1 - 7")} Jump directly to category`,
916
+ ` ${chalk.cyan("Enter")} Execute selected command`,
917
+ ` ${chalk.cyan("e")} Edit flags/arguments before running`,
918
+ ` ${chalk.cyan("c")} Copy command to clipboard / show snippet`,
919
+ ` ${chalk.cyan("/ (or type)")} Search / filter commands instantly`,
920
+ ` ${chalk.cyan("Backspace")} Delete search query character`,
921
+ ` ${chalk.cyan("Esc")} Clear search or close modal`,
922
+ ` ${chalk.cyan("q")} Quit to shell`,
923
+ "",
924
+ chalk.dim(" Press any key or Esc to close help...")
925
+ ];
926
+ const box = panel(" Help & Shortcuts ", modalLines, modalW, accent, border, modalLines.length);
927
+ stampOverlay(lines, box, width, height);
928
+ }
929
+ function stampOverlay(screenLines, box, width, height) {
930
+ const boxH = box.length;
931
+ const startRow = Math.max(0, Math.floor((screenLines.length - boxH) / 2));
932
+ const boxW = visLen(box[0] || "");
933
+ const startCol = Math.max(0, Math.floor((width - boxW) / 2));
934
+ for (let r = 0; r < boxH; r++) {
935
+ const targetRow = startRow + r;
936
+ if (targetRow < screenLines.length) {
937
+ const orig = screenLines[targetRow];
938
+ const left = orig.slice(0, startCol);
939
+ const right = orig.slice(startCol + boxW);
940
+ screenLines[targetRow] = fitVisible(left, startCol) + box[r] + fitVisible(right, Math.max(0, width - startCol - boxW));
941
+ }
942
+ }
943
+ }
944
+
945
+ // src/menu/index.ts
946
+ async function runMenu(opts) {
947
+ const config = readConfig();
948
+ const theme = opts?.theme || config.theme || "default";
949
+ const state = {
950
+ category: opts?.initialCategory || "all",
951
+ categoryIndex: CATEGORIES.findIndex((c) => c.id === (opts?.initialCategory || "all")) || 0,
952
+ searchQuery: "",
953
+ selectedIndex: 0,
954
+ scrollOffset: 0,
955
+ inputMode: "search",
956
+ argPromptValue: "",
957
+ activeCommand: null,
958
+ statusMessage: "",
959
+ termWidth: process.stdout.columns || 80,
960
+ termHeight: process.stdout.rows || 24,
961
+ theme
962
+ };
963
+ let isRunning = true;
964
+ let statusTimer = null;
965
+ function setStatus(msg, durationMs = 3e3) {
966
+ state.statusMessage = msg;
967
+ if (statusTimer) clearTimeout(statusTimer);
968
+ statusTimer = setTimeout(() => {
969
+ state.statusMessage = "";
970
+ if (isRunning) render();
971
+ }, durationMs);
972
+ }
973
+ function getFiltered() {
974
+ return filterCommands(ALL_COMMANDS, state.category, state.searchQuery);
975
+ }
976
+ function render() {
977
+ if (!isRunning) return;
978
+ const filtered = getFiltered();
979
+ const output = renderMenuScreen(state, filtered);
980
+ process.stdout.write("\x1B[H" + output);
981
+ }
982
+ function onResize() {
983
+ state.termWidth = process.stdout.columns || 80;
984
+ state.termHeight = process.stdout.rows || 24;
985
+ render();
986
+ }
987
+ function enterTui() {
988
+ process.stdout.write("\x1B[?1049h");
989
+ process.stdout.write("\x1B[?25l");
990
+ process.stdout.write("\x1B[2J\x1B[H");
991
+ readline.emitKeypressEvents(process.stdin);
992
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
993
+ process.stdin.resume();
994
+ process.stdout.on("resize", onResize);
995
+ render();
996
+ }
997
+ function leaveTui() {
998
+ process.stdout.removeListener("resize", onResize);
999
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1000
+ process.stdin.pause();
1001
+ process.stdout.write("\x1B[?25h");
1002
+ process.stdout.write("\x1B[?1049l");
1003
+ }
1004
+ function executeCommand(rawCommandLine, isInteractive = false) {
1005
+ leaveTui();
1006
+ const tokens = rawCommandLine.trim().split(/\s+/).filter(Boolean);
1007
+ if (tokens[0] === "pyre") {
1008
+ tokens.shift();
1009
+ }
1010
+ console.log(chalk2.bold(`
1011
+ \u{1F525} Executing: ${chalk2.hex("#ff6a39")(rawCommandLine)}
1012
+ `));
1013
+ try {
1014
+ const scriptPath = process.argv[1];
1015
+ spawnSync(process.argv[0], [scriptPath, ...tokens], {
1016
+ stdio: "inherit",
1017
+ env: process.env
1018
+ });
1019
+ } catch (err) {
1020
+ console.error(chalk2.red(`
1021
+ \u2716 Error executing command: ${err.message}`));
1022
+ }
1023
+ if (!isInteractive) {
1024
+ console.log(chalk2.dim("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1025
+ console.log(chalk2.yellow(" Press any key to return to Command Center (or [q] to exit)..."));
1026
+ if (process.stdin.isTTY) {
1027
+ process.stdin.setRawMode(true);
1028
+ process.stdin.resume();
1029
+ process.stdin.once("data", (buf) => {
1030
+ const str = buf.toString().toLowerCase();
1031
+ if (str === "q" || str === "") {
1032
+ process.exit(0);
1033
+ }
1034
+ enterTui();
1035
+ });
1036
+ return;
1037
+ }
1038
+ }
1039
+ enterTui();
1040
+ }
1041
+ enterTui();
1042
+ return new Promise((resolve) => {
1043
+ function onKeypress(str, key) {
1044
+ if (!isRunning) return;
1045
+ if (!key) return;
1046
+ if (key.ctrl && key.name === "c") {
1047
+ isRunning = false;
1048
+ leaveTui();
1049
+ if (opts?.onExit) opts.onExit();
1050
+ else process.exit(0);
1051
+ resolve();
1052
+ return;
1053
+ }
1054
+ if (state.inputMode === "help") {
1055
+ state.inputMode = "search";
1056
+ render();
1057
+ return;
1058
+ }
1059
+ if (state.inputMode === "prompt-args") {
1060
+ if (key.name === "escape") {
1061
+ state.inputMode = "search";
1062
+ state.argPromptValue = "";
1063
+ render();
1064
+ return;
1065
+ }
1066
+ if (key.name === "return" || key.name === "enter") {
1067
+ const cmd = state.activeCommand;
1068
+ const args = state.argPromptValue.trim();
1069
+ state.inputMode = "search";
1070
+ state.argPromptValue = "";
1071
+ if (cmd) {
1072
+ const fullCmd = args ? `${cmd.cli} ${args}` : cmd.cli;
1073
+ executeCommand(fullCmd, cmd.interactive);
1074
+ }
1075
+ return;
1076
+ }
1077
+ if (key.name === "backspace") {
1078
+ state.argPromptValue = state.argPromptValue.slice(0, -1);
1079
+ render();
1080
+ return;
1081
+ }
1082
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
1083
+ state.argPromptValue += str;
1084
+ render();
1085
+ return;
1086
+ }
1087
+ return;
1088
+ }
1089
+ if (state.inputMode === "edit-cmd") {
1090
+ if (key.name === "escape") {
1091
+ state.inputMode = "search";
1092
+ state.argPromptValue = "";
1093
+ render();
1094
+ return;
1095
+ }
1096
+ if (key.name === "return" || key.name === "enter") {
1097
+ const fullCmd = state.argPromptValue.trim();
1098
+ const cmd = state.activeCommand;
1099
+ state.inputMode = "search";
1100
+ state.argPromptValue = "";
1101
+ if (fullCmd) {
1102
+ executeCommand(fullCmd, cmd?.interactive);
1103
+ }
1104
+ return;
1105
+ }
1106
+ if (key.name === "backspace") {
1107
+ state.argPromptValue = state.argPromptValue.slice(0, -1);
1108
+ render();
1109
+ return;
1110
+ }
1111
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
1112
+ state.argPromptValue += str;
1113
+ render();
1114
+ return;
1115
+ }
1116
+ return;
1117
+ }
1118
+ const filtered = getFiltered();
1119
+ const count = filtered.length;
1120
+ if (key.name === "up" || key.name === "k" && state.searchQuery === "") {
1121
+ if (count > 0) {
1122
+ state.selectedIndex = (state.selectedIndex - 1 + count) % count;
1123
+ render();
1124
+ }
1125
+ return;
1126
+ }
1127
+ if (key.name === "down" || key.name === "j" && state.searchQuery === "") {
1128
+ if (count > 0) {
1129
+ state.selectedIndex = (state.selectedIndex + 1) % count;
1130
+ render();
1131
+ }
1132
+ return;
1133
+ }
1134
+ if (key.name === "pageup") {
1135
+ state.selectedIndex = Math.max(0, state.selectedIndex - 6);
1136
+ render();
1137
+ return;
1138
+ }
1139
+ if (key.name === "pagedown") {
1140
+ state.selectedIndex = Math.min(count - 1, state.selectedIndex + 6);
1141
+ render();
1142
+ return;
1143
+ }
1144
+ if (key.name === "tab") {
1145
+ const dir = key.shift ? -1 : 1;
1146
+ state.categoryIndex = (state.categoryIndex + dir + CATEGORIES.length) % CATEGORIES.length;
1147
+ state.category = CATEGORIES[state.categoryIndex].id;
1148
+ state.selectedIndex = 0;
1149
+ state.scrollOffset = 0;
1150
+ render();
1151
+ return;
1152
+ }
1153
+ if (str === "?" || str === "h" && state.searchQuery === "") {
1154
+ state.inputMode = "help";
1155
+ render();
1156
+ return;
1157
+ }
1158
+ if (str === "c" && state.searchQuery === "") {
1159
+ const cur = filtered[state.selectedIndex];
1160
+ if (cur) {
1161
+ setStatus(`CLI command: ${cur.cli}`);
1162
+ }
1163
+ return;
1164
+ }
1165
+ if (str === "e" && state.searchQuery === "") {
1166
+ const cur = filtered[state.selectedIndex];
1167
+ if (cur) {
1168
+ state.inputMode = "edit-cmd";
1169
+ state.activeCommand = cur;
1170
+ state.argPromptValue = cur.cli;
1171
+ render();
1172
+ }
1173
+ return;
1174
+ }
1175
+ if (key.name === "return" || key.name === "enter") {
1176
+ const cur = filtered[state.selectedIndex];
1177
+ if (!cur) return;
1178
+ if (cur.requiresArgs) {
1179
+ state.inputMode = "prompt-args";
1180
+ state.activeCommand = cur;
1181
+ state.argPromptValue = cur.requiresArgs.defaultVal || "";
1182
+ render();
1183
+ return;
1184
+ }
1185
+ executeCommand(cur.cli, cur.interactive);
1186
+ return;
1187
+ }
1188
+ if (key.name === "backspace") {
1189
+ if (state.searchQuery.length > 0) {
1190
+ state.searchQuery = state.searchQuery.slice(0, -1);
1191
+ state.selectedIndex = 0;
1192
+ state.scrollOffset = 0;
1193
+ render();
1194
+ }
1195
+ return;
1196
+ }
1197
+ if (key.name === "escape") {
1198
+ if (state.searchQuery.length > 0) {
1199
+ state.searchQuery = "";
1200
+ state.selectedIndex = 0;
1201
+ state.scrollOffset = 0;
1202
+ render();
1203
+ } else {
1204
+ isRunning = false;
1205
+ leaveTui();
1206
+ if (opts?.onExit) opts.onExit();
1207
+ else process.exit(0);
1208
+ resolve();
1209
+ }
1210
+ return;
1211
+ }
1212
+ if (str === "q" && state.searchQuery === "") {
1213
+ isRunning = false;
1214
+ leaveTui();
1215
+ if (opts?.onExit) opts.onExit();
1216
+ else process.exit(0);
1217
+ resolve();
1218
+ return;
1219
+ }
1220
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
1221
+ state.searchQuery += str;
1222
+ state.selectedIndex = 0;
1223
+ state.scrollOffset = 0;
1224
+ render();
1225
+ return;
1226
+ }
1227
+ }
1228
+ process.stdin.on("keypress", onKeypress);
1229
+ });
1230
+ }
1231
+ export {
1232
+ runMenu
1233
+ };