@gachlab/devup 0.4.0 → 0.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.
- package/CHANGELOG.md +22 -0
- package/README.md +7 -3
- package/dist/config/types.d.ts +8 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/index.js +167 -46
- package/dist/index.js.map +1 -1
- package/dist/process/manager.d.ts.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/LogsPanel.d.ts +2 -1
- package/dist/tui/LogsPanel.d.ts.map +1 -1
- package/dist/tui/StatsPanel.d.ts +2 -1
- package/dist/tui/StatsPanel.d.ts.map +1 -1
- package/dist/tui/hooks/useKeyBindings.d.ts +5 -0
- package/dist/tui/hooks/useKeyBindings.d.ts.map +1 -1
- package/dist/tui/hooks/useProcessManager.d.ts +2 -0
- package/dist/tui/hooks/useProcessManager.d.ts.map +1 -1
- package/dist/utils.d.ts +25 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ All notable changes to `@gachlab/devup` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.0] — 2026-05-21
|
|
9
|
+
|
|
10
|
+
Config power release — six features that sharpen day-to-day debugging in a long-running stack.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Regex search in logs** (#8). `/` accepts vim-style `/pattern/flags` in addition to the existing case-insensitive substring mode. `/error/`, `/^api: \d+/`, `/foo/g` all work. Case-insensitive by default — add explicit flags after the slash if needed. Invalid regex falls back to substring search and shows `(invalid regex)` in the logs panel header so the user can correct it. Plain strings (including ones with slashes inside) keep working as substring matches.
|
|
14
|
+
- **`healthCheck.startPeriod` grace window** (#15). New optional field, in seconds. Probes are fully suppressed during the window, status stays `starting`, `health` stays `wait`. Eliminates spurious failed probes during slow boots (Angular cold-start, big webpack builds) that otherwise inflate `state.errors` and pollute the TUI.
|
|
15
|
+
- **Customizable error pattern per service** (#16). New `errorPattern?: string` field on `ServiceConfig`. When set, only stderr lines matching the regex (same `/pattern/flags` grammar as `readyPattern`) bump `state.errors`. Without it, every non-empty stderr line counts (existing behavior). Useful for libraries that write info to stderr — Angular CLI is the worst offender.
|
|
16
|
+
- **Filter logs by level** (#19). Each log line is tagged with a level on ingestion: `error > warn > info`. New `L` key cycles the filter: `all → error → warn+error → all`. Detection is keyword-based with conjugations (`error`, `fail(ed|ure|s)`, `fatal`, `exception`, `crash(ed|es)` → error; `warn(ed|ing|s)`, `deprec` → warn). Devup's own log markers count: `❌`/`✗`/`⛔` → error, `⚠` → warn. `a` (show all) also resets the level filter.
|
|
17
|
+
- **Verbose stats** (#21). New `v` key toggles the stats panel between compact mode and verbose mode. Verbose mode adds two dim indented lines per service: `cmd: <cmd> <resolved args>` (after `buildProcessArgs`, so devup-injected flags like `--max-old-space-size` are visible) and `env: KEY=value ...` (only when `extraEnv` is non-empty). Env values are auto-redacted (`***`) for keys matching `/secret|token|password|api[_-]?key|auth/i`.
|
|
18
|
+
- **Resource awareness — RAM watchdog banner** (#24). When system RAM usage crosses 80 % the stats panel shows a banner: `⚠ RAM 84% — top: app-api 520MB, staff-web 480MB, admin-web 460MB`. Hysteresis-driven (turns off only below 75 %, no flicker at the boundary). Top consumers are sorted by `stats.get(name).mem` and capped at 3.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `LogEntry` interface gains a required `level: LogLevel` field; both `pushLog()` and the manager-driven `onLog` handler compute it on ingestion.
|
|
22
|
+
- StatusBar shows the new `L` Level and `v` Verbose bindings.
|
|
23
|
+
- The Logs panel header gains `[level: error]` / `[level: warn+error]` markers when a level filter is active.
|
|
24
|
+
|
|
25
|
+
### Internals
|
|
26
|
+
- New pure helpers in `utils.ts`: `compileSearchPattern`, `detectLogLevel`, `redactSecrets`, `nextRamBannerVisibility`. All exported, all individually tested.
|
|
27
|
+
- Test suite grown from 274 to ~299. New suites: `compileSearchPattern` (6), `detectLogLevel` (5), `redactSecrets` (3), `nextRamBannerVisibility` (4), plus 2 manager tests for `errorPattern` and 1 for `healthCheck.startPeriod`.
|
|
28
|
+
|
|
8
29
|
## [0.4.0] — 2026-05-21
|
|
9
30
|
|
|
10
31
|
Polish + standalone CLI release. Eight focused improvements landed as a single PR with one commit per issue.
|
|
@@ -135,6 +156,7 @@ Initial release.
|
|
|
135
156
|
- Config file resolution order: `devup.config.ts` → `.js` → `.json`, with `--config <path>` override. TypeScript loaded via the `tsx` import hook.
|
|
136
157
|
- CLI flags: `--only`, `--services`, `--skip`, `--lazy`/`--no-lazy`, `--timeout`, `--proxy`, `--proxy-host`, `--proxy-conf`, `--proxy-tls`/`--no-proxy-tls`, `--proxy-entrypoint`, `--config`.
|
|
137
158
|
|
|
159
|
+
[0.5.0]: https://github.com/gachlab/devup/releases/tag/0.5.0
|
|
138
160
|
[0.4.0]: https://github.com/gachlab/devup/releases/tag/0.4.0
|
|
139
161
|
[0.3.0]: https://github.com/gachlab/devup/releases/tag/0.3.0
|
|
140
162
|
[0.2.0]: https://github.com/gachlab/devup/releases/tag/0.2.0
|
package/README.md
CHANGED
|
@@ -125,6 +125,7 @@ Then `devup --profile check-in` boots that subset. Composable with `--skip`. The
|
|
|
125
125
|
| `extraEnv` | `Record<string, string>` | | Extra environment variables for this service |
|
|
126
126
|
| `healthCheck` | `HealthCheckConfig` | | Override the readiness check for this service. Default: TCP probe on `port` |
|
|
127
127
|
| `readyPattern` | `string` | | Regex matched against stdout/stderr lines. On match, service is marked `up` immediately, short-circuiting the next health-check poll. Plain string or vim-style `/pattern/flags`. Case-insensitive by default |
|
|
128
|
+
| `errorPattern` | `string` | | Only stderr lines matching this regex bump `state.errors`. Without it every non-empty stderr line counts. Same `/pattern/flags` grammar as `readyPattern` |
|
|
128
129
|
|
|
129
130
|
### `HealthCheckConfig`
|
|
130
131
|
|
|
@@ -135,6 +136,7 @@ Then `devup --profile check-in` boots that subset. Composable with `--skip`. The
|
|
|
135
136
|
| `expect` | `number \| number[]` | | HTTP-only acceptable status code(s). Default: any 2xx (200-299) |
|
|
136
137
|
| `host` | `string` | | Override target host for the HTTP check. Default: `127.0.0.1` |
|
|
137
138
|
| `timeoutMs` | `number` | | Per-check socket/request timeout in ms. Default: `2000` |
|
|
139
|
+
| `startPeriod` | `number` | | Grace period in seconds before the first probe runs. Useful for slow boots (Angular cold-start, etc.) so failed probes during boot don't pollute `state.errors`. Default: `0` |
|
|
138
140
|
|
|
139
141
|
```typescript
|
|
140
142
|
// Wait for /healthz to return 200 before considering the service up
|
|
@@ -318,14 +320,16 @@ devup writes a separate `.log` file per service to disk. Lines are prefixed with
|
|
|
318
320
|
| `[` / `]` (or `Ctrl+B` / `Ctrl+F`) | Page up / page down |
|
|
319
321
|
| `Ctrl+A` / `Ctrl+E` | Jump to top / bottom of the focused panel |
|
|
320
322
|
| `f` | Filter logs by service |
|
|
321
|
-
| `
|
|
322
|
-
|
|
|
323
|
+
| `L` | Cycle log level filter (all → error → warn+error → all) |
|
|
324
|
+
| `a` | Show all logs (clear service / search / level filters) |
|
|
325
|
+
| `/` | Search in logs (accepts `/pattern/flags` regex) |
|
|
323
326
|
| `p` | Pause/resume log output (auto-engaged when you scroll up) |
|
|
324
327
|
| `t` | Toggle timestamps |
|
|
325
328
|
| `c` | Clear logs |
|
|
326
329
|
| `s` | Cycle sort mode (name → memory → errors) |
|
|
327
330
|
| `r` | Restart a service |
|
|
328
|
-
| `o` | Open a web service in browser |
|
|
331
|
+
| `o` | Open a web service in browser (TLS-aware when `--proxy`) |
|
|
332
|
+
| `v` | Verbose stats: show resolved `cmd`/args/env per service (env secrets redacted) |
|
|
329
333
|
| `T` | Toggle reverse proxy config sync |
|
|
330
334
|
|
|
331
335
|
When you scroll the Logs panel up, devup auto-pauses the log stream so new lines don't push your reading position. New lines are buffered and replay when you return to the bottom (`Ctrl+E` or scroll all the way down).
|
package/dist/config/types.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export interface ServiceConfig {
|
|
|
17
17
|
* health-check poll. Speeds up phase transitions on cold boots.
|
|
18
18
|
* Examples: '/ready in \\d+ ms/' (Vite), '/compiled successfully/' (Angular). */
|
|
19
19
|
readyPattern?: string;
|
|
20
|
+
/** Case-insensitive regex. When set, only stderr lines matching this pattern
|
|
21
|
+
* bump `state.errors`. Without it, every non-empty stderr line counts.
|
|
22
|
+
* Useful for libraries that write info messages to stderr (Angular CLI). */
|
|
23
|
+
errorPattern?: string;
|
|
20
24
|
}
|
|
21
25
|
export interface HealthCheckConfig {
|
|
22
26
|
/** 'tcp' (default) checks that the port accepts connections. 'http' makes an HTTP GET. */
|
|
@@ -29,6 +33,10 @@ export interface HealthCheckConfig {
|
|
|
29
33
|
host?: string;
|
|
30
34
|
/** Per-check socket timeout in ms. Default: 2000 */
|
|
31
35
|
timeoutMs?: number;
|
|
36
|
+
/** Grace period (seconds) before the first probe runs. Useful for slow boots
|
|
37
|
+
* (Angular cold-start, big webpack builds) so failed probes during boot don't
|
|
38
|
+
* pollute state.errors. Default: 0 (no grace). */
|
|
39
|
+
startPeriod?: number;
|
|
32
40
|
}
|
|
33
41
|
export interface LazyConfig {
|
|
34
42
|
alwaysOn: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC;;;sFAGkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,0FAA0F;IAC1F,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC;;;sFAGkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;iFAE6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,0FAA0F;IAC1F,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;uDAEmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,0FAA0F;IAC1F,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,6FAA6F;IAC7F,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACpC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAC;CAC9B;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAEnE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/config/validator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/config/validator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,CA0LrF;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,CAExE"}
|
package/dist/index.js
CHANGED
|
@@ -114,6 +114,19 @@ function validateConfig(config, cwd) {
|
|
|
114
114
|
if (svc.cwd && !existsSync2(resolve2(cwd, svc.cwd))) {
|
|
115
115
|
errors.push({ field: `services[${svc.name}].cwd`, message: `Directory not found: ${svc.cwd}` });
|
|
116
116
|
}
|
|
117
|
+
if (svc.errorPattern !== void 0) {
|
|
118
|
+
if (typeof svc.errorPattern !== "string" || !svc.errorPattern.length) {
|
|
119
|
+
errors.push({ field: `services[${svc.name}].errorPattern`, message: `errorPattern must be a non-empty string` });
|
|
120
|
+
} else {
|
|
121
|
+
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(svc.errorPattern);
|
|
122
|
+
try {
|
|
123
|
+
if (slashed) new RegExp(slashed[1], slashed[2] || "i");
|
|
124
|
+
else new RegExp(svc.errorPattern, "i");
|
|
125
|
+
} catch (e) {
|
|
126
|
+
errors.push({ field: `services[${svc.name}].errorPattern`, message: `Invalid regex: ${e.message}` });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
117
130
|
if (svc.readyPattern !== void 0) {
|
|
118
131
|
if (typeof svc.readyPattern !== "string" || !svc.readyPattern.length) {
|
|
119
132
|
errors.push({ field: `services[${svc.name}].readyPattern`, message: `readyPattern must be a non-empty string` });
|
|
@@ -138,6 +151,9 @@ function validateConfig(config, cwd) {
|
|
|
138
151
|
if (hc.type !== "tcp" && hc.type !== "http") {
|
|
139
152
|
errors.push({ field: `services[${svc.name}].healthCheck.type`, message: `Invalid healthCheck.type: ${hc.type} (must be "tcp" or "http")` });
|
|
140
153
|
}
|
|
154
|
+
if (hc.startPeriod !== void 0 && (typeof hc.startPeriod !== "number" || hc.startPeriod < 0)) {
|
|
155
|
+
errors.push({ field: `services[${svc.name}].healthCheck.startPeriod`, message: `startPeriod must be a non-negative number (seconds), got ${hc.startPeriod}` });
|
|
156
|
+
}
|
|
141
157
|
if (hc.type === "http" && hc.path && !hc.path.startsWith("/")) {
|
|
142
158
|
errors.push({ field: `services[${svc.name}].healthCheck.path`, message: `healthCheck.path must start with "/": got "${hc.path}"` });
|
|
143
159
|
}
|
|
@@ -489,6 +505,41 @@ function parseEnvFile(filePath, baseEnv = {}) {
|
|
|
489
505
|
}
|
|
490
506
|
return env;
|
|
491
507
|
}
|
|
508
|
+
function nextRamBannerVisibility(usagePct, previousVisible, highWatermark = 80, lowWatermark = 75) {
|
|
509
|
+
if (usagePct >= highWatermark) return true;
|
|
510
|
+
if (usagePct < lowWatermark) return false;
|
|
511
|
+
return previousVisible;
|
|
512
|
+
}
|
|
513
|
+
function redactSecrets(env) {
|
|
514
|
+
if (!env) return {};
|
|
515
|
+
const out = {};
|
|
516
|
+
for (const [k, v] of Object.entries(env)) {
|
|
517
|
+
out[k] = /secret|token|password|api[_-]?key|auth/i.test(k) ? "***" : v;
|
|
518
|
+
}
|
|
519
|
+
return out;
|
|
520
|
+
}
|
|
521
|
+
function detectLogLevel(line) {
|
|
522
|
+
const l = line.toLowerCase();
|
|
523
|
+
if (/\b(?:error|err|fail(?:ed|ure|ures|s)?|fatal|exception|crash(?:ed|es)?)\b/.test(l) || /❌|✗|⛔/.test(line)) return "error";
|
|
524
|
+
if (/\b(?:warn(?:ed|ing|s|ings)?|deprec)\b/.test(l) || /⚠/.test(line)) return "warn";
|
|
525
|
+
return "info";
|
|
526
|
+
}
|
|
527
|
+
function compileSearchPattern(term) {
|
|
528
|
+
if (!term) return null;
|
|
529
|
+
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(term);
|
|
530
|
+
if (slashed) {
|
|
531
|
+
const flags = slashed[2].includes("i") ? slashed[2] : slashed[2] + "i";
|
|
532
|
+
try {
|
|
533
|
+
const re = new RegExp(slashed[1], flags);
|
|
534
|
+
return { test: (l) => re.test(l), regex: re };
|
|
535
|
+
} catch {
|
|
536
|
+
const lower2 = term.toLowerCase();
|
|
537
|
+
return { test: (l) => l.toLowerCase().includes(lower2), invalid: true };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const lower = term.toLowerCase();
|
|
541
|
+
return { test: (l) => l.toLowerCase().includes(lower) };
|
|
542
|
+
}
|
|
492
543
|
function fmtUptime(ms) {
|
|
493
544
|
if (!ms || ms < 0) return "-";
|
|
494
545
|
const s = Math.floor(ms / 1e3);
|
|
@@ -903,7 +954,7 @@ function detectProxyProvider(name) {
|
|
|
903
954
|
}
|
|
904
955
|
|
|
905
956
|
// src/tui/App.tsx
|
|
906
|
-
import { useEffect as useEffect5, useState as
|
|
957
|
+
import { useEffect as useEffect5, useState as useState6, useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
907
958
|
import { Box as Box6, Text as Text6, useStdout } from "ink";
|
|
908
959
|
|
|
909
960
|
// src/tui/hooks/useProcessManager.ts
|
|
@@ -1072,12 +1123,14 @@ var ProcessManager = class {
|
|
|
1072
1123
|
this.events.onStateChange(svc.name, state);
|
|
1073
1124
|
}
|
|
1074
1125
|
};
|
|
1126
|
+
const errorRegex = compileReadyPattern(svc.errorPattern);
|
|
1127
|
+
const countsAsError = (line) => errorRegex ? errorRegex.test(line) : true;
|
|
1075
1128
|
const stdoutBuf = lineBuffer((line) => {
|
|
1076
1129
|
markReadyIfMatch(line);
|
|
1077
1130
|
this.log(svc.name, line, colorIdx);
|
|
1078
1131
|
});
|
|
1079
1132
|
const stderrBuf = lineBuffer((line) => {
|
|
1080
|
-
state.errors += 1;
|
|
1133
|
+
if (countsAsError(line)) state.errors += 1;
|
|
1081
1134
|
markReadyIfMatch(line);
|
|
1082
1135
|
this.log(svc.name, line, colorIdx);
|
|
1083
1136
|
});
|
|
@@ -1212,6 +1265,10 @@ var ProcessManager = class {
|
|
|
1212
1265
|
st.health = st.status === "idle" ? "idle" : "down";
|
|
1213
1266
|
continue;
|
|
1214
1267
|
}
|
|
1268
|
+
const startPeriodMs = (st.svc.healthCheck?.startPeriod ?? 0) * 1e3;
|
|
1269
|
+
if (startPeriodMs > 0 && st.startedAt && Date.now() - st.startedAt < startPeriodMs) {
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1215
1272
|
const isUp = await checkHealth(st.svc.port, st.svc.healthCheck);
|
|
1216
1273
|
const prev = st.health;
|
|
1217
1274
|
st.health = deriveHealth(isUp, st.status);
|
|
@@ -1283,7 +1340,7 @@ function useProcessManager(platform, baseCwd, env, logSink = null) {
|
|
|
1283
1340
|
events: {
|
|
1284
1341
|
onLog: (svcName, text, colorIdx) => {
|
|
1285
1342
|
sinkRef.current?.write(svcName, text);
|
|
1286
|
-
const entry = { svcName, text, colorIdx, ts: Date.now() };
|
|
1343
|
+
const entry = { svcName, text, colorIdx, ts: Date.now(), level: detectLogLevel(text) };
|
|
1287
1344
|
if (pausedRef.current) {
|
|
1288
1345
|
pendingLogsRef.current.push(entry);
|
|
1289
1346
|
if (pendingLogsRef.current.length > 5e3) {
|
|
@@ -1341,7 +1398,7 @@ function useProcessManager(platform, baseCwd, env, logSink = null) {
|
|
|
1341
1398
|
}, []);
|
|
1342
1399
|
const pushLog = useCallback((svcName, text, colorIdx = 0) => {
|
|
1343
1400
|
sinkRef.current?.write(svcName, text);
|
|
1344
|
-
const entry = { svcName, text, colorIdx, ts: Date.now() };
|
|
1401
|
+
const entry = { svcName, text, colorIdx, ts: Date.now(), level: detectLogLevel(text) };
|
|
1345
1402
|
if (pausedRef.current) {
|
|
1346
1403
|
pendingLogsRef.current.push(entry);
|
|
1347
1404
|
if (pendingLogsRef.current.length > 5e3) {
|
|
@@ -1414,8 +1471,11 @@ function useKeyBindings(opts) {
|
|
|
1414
1471
|
sortIdx: 0,
|
|
1415
1472
|
proxyEnabled: false,
|
|
1416
1473
|
logsScrollOffset: 0,
|
|
1417
|
-
statsScrollOffset: 0
|
|
1474
|
+
statsScrollOffset: 0,
|
|
1475
|
+
levelFilter: "all",
|
|
1476
|
+
verboseStats: false
|
|
1418
1477
|
});
|
|
1478
|
+
const LEVEL_CYCLE = ["all", "error", "warn"];
|
|
1419
1479
|
const setModal = useCallback2((modal) => setState((s) => ({ ...s, modal })), []);
|
|
1420
1480
|
const setFilter = useCallback2((f) => setState((s) => ({ ...s, logFilter: f, modal: "none" })), []);
|
|
1421
1481
|
const setSearch = useCallback2((t) => setState((s) => ({ ...s, searchTerm: t, modal: "none" })), []);
|
|
@@ -1437,14 +1497,15 @@ function useKeyBindings(opts) {
|
|
|
1437
1497
|
else if (input === "r") setModal("restart");
|
|
1438
1498
|
else if (input === "o") setModal("open");
|
|
1439
1499
|
else if (input === "/") setModal("search");
|
|
1440
|
-
else if (input === "a") setState((s) => ({ ...s, logFilter: null, searchTerm: null }));
|
|
1500
|
+
else if (input === "a") setState((s) => ({ ...s, logFilter: null, searchTerm: null, levelFilter: "all" }));
|
|
1441
1501
|
else if (input === "p") setState((s) => ({ ...s, logsPaused: !s.logsPaused }));
|
|
1442
1502
|
else if (input === "t") setState((s) => ({ ...s, showTimestamps: !s.showTimestamps }));
|
|
1443
1503
|
else if (input === "s") setState((s) => ({ ...s, sortIdx: (s.sortIdx + 1) % SORT_MODES.length }));
|
|
1444
1504
|
else if (input === "T") {
|
|
1445
1505
|
opts.onToggleProxy();
|
|
1446
1506
|
setState((s) => ({ ...s, proxyEnabled: !s.proxyEnabled }));
|
|
1447
|
-
}
|
|
1507
|
+
} else if (input === "L") setState((s) => ({ ...s, levelFilter: LEVEL_CYCLE[(LEVEL_CYCLE.indexOf(s.levelFilter) + 1) % LEVEL_CYCLE.length] }));
|
|
1508
|
+
else if (input === "v") setState((s) => ({ ...s, verboseStats: !s.verboseStats }));
|
|
1448
1509
|
}, { isActive });
|
|
1449
1510
|
return {
|
|
1450
1511
|
...state,
|
|
@@ -1486,11 +1547,12 @@ function useProxySync(provider, opts, states, enabled) {
|
|
|
1486
1547
|
}
|
|
1487
1548
|
|
|
1488
1549
|
// src/tui/LogsPanel.tsx
|
|
1489
|
-
import { useEffect as useEffect3 } from "react";
|
|
1550
|
+
import { useEffect as useEffect3, useMemo } from "react";
|
|
1490
1551
|
import { Box, Text } from "ink";
|
|
1491
1552
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1492
|
-
function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLen, height, focused, scrollOffset, resetScroll }) {
|
|
1493
|
-
const
|
|
1553
|
+
function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLen, height, focused, scrollOffset, resetScroll, levelFilter = "all" }) {
|
|
1554
|
+
const byService = filter ? logs.filter((l) => l.svcName === filter) : logs;
|
|
1555
|
+
const filtered = levelFilter === "all" ? byService : levelFilter === "error" ? byService.filter((l) => l.level === "error") : byService.filter((l) => l.level === "error" || l.level === "warn");
|
|
1494
1556
|
const contentHeight = Math.max(1, height - 2);
|
|
1495
1557
|
const totalLines = filtered.length;
|
|
1496
1558
|
const maxOffset = Math.max(0, totalLines - contentHeight);
|
|
@@ -1501,11 +1563,14 @@ function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLe
|
|
|
1501
1563
|
useEffect3(() => {
|
|
1502
1564
|
resetScroll();
|
|
1503
1565
|
}, [filter, searchTerm, resetScroll]);
|
|
1566
|
+
const matcher = useMemo(() => compileSearchPattern(searchTerm), [searchTerm]);
|
|
1504
1567
|
const scrolled = effectiveOffset > 0;
|
|
1505
1568
|
const label = [
|
|
1506
1569
|
"Logs",
|
|
1507
1570
|
filter ? `[${filter}]` : "",
|
|
1508
1571
|
searchTerm ? `/${searchTerm}` : "",
|
|
1572
|
+
matcher?.invalid ? "(invalid regex)" : "",
|
|
1573
|
+
levelFilter !== "all" ? `[level: ${levelFilter}${levelFilter === "warn" ? "+error" : ""}]` : "",
|
|
1509
1574
|
paused ? "[PAUSED]" : "",
|
|
1510
1575
|
scrolled ? "[SCROLL]" : "",
|
|
1511
1576
|
`${filtered.length} lines`,
|
|
@@ -1521,7 +1586,7 @@ function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLe
|
|
|
1521
1586
|
const color = tagColors[entry.colorIdx % tagColors.length];
|
|
1522
1587
|
const ts = showTimestamps ? new Date(entry.ts).toLocaleTimeString("en-GB") + " " : "";
|
|
1523
1588
|
const line = entry.text;
|
|
1524
|
-
const isMatch =
|
|
1589
|
+
const isMatch = matcher ? matcher.test(line) : false;
|
|
1525
1590
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
1526
1591
|
showTimestamps && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ts }),
|
|
1527
1592
|
/* @__PURE__ */ jsxs(Text, { color, children: [
|
|
@@ -1537,7 +1602,7 @@ function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLe
|
|
|
1537
1602
|
}
|
|
1538
1603
|
|
|
1539
1604
|
// src/tui/StatsPanel.tsx
|
|
1540
|
-
import { useEffect as useEffect4 } from "react";
|
|
1605
|
+
import { useEffect as useEffect4, useState as useState3 } from "react";
|
|
1541
1606
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
1542
1607
|
import os from "os";
|
|
1543
1608
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
@@ -1551,31 +1616,67 @@ var MAX_RESTARTS2 = 3;
|
|
|
1551
1616
|
function isCrashLooped(st) {
|
|
1552
1617
|
return st.status === "crashed" && st.restarts >= MAX_RESTARTS2;
|
|
1553
1618
|
}
|
|
1554
|
-
function Row({ name, st, stat, ml }) {
|
|
1619
|
+
function Row({ name, st, stat, ml, verbose }) {
|
|
1555
1620
|
const looped = isCrashLooped(st);
|
|
1556
1621
|
const indicator = looped ? /* @__PURE__ */ jsx2(Text2, { color: "red", bold: true, children: "\u2716" }) : /* @__PURE__ */ jsx2(Text2, { color: (H[st.health] ?? H["down"]).color, children: (H[st.health] ?? H["down"]).c });
|
|
1557
1622
|
const color = tagColors[st.colorIdx % tagColors.length];
|
|
1558
1623
|
const sc = looped ? "red" : st.status === "running" ? "green" : st.status === "starting" ? "yellow" : st.status === "idle" ? "blue" : "red";
|
|
1559
1624
|
const statusLabel = looped ? "looping" : st.status;
|
|
1560
1625
|
const up = st.startedAt ? fmtUptime(Date.now() - st.startedAt) : "-";
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1626
|
+
if (!verbose) {
|
|
1627
|
+
return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1628
|
+
indicator,
|
|
1629
|
+
" ",
|
|
1630
|
+
/* @__PURE__ */ jsx2(Text2, { color, children: name.padEnd(ml) }),
|
|
1631
|
+
" ",
|
|
1632
|
+
String(st.svc.port).padStart(5),
|
|
1633
|
+
" ",
|
|
1634
|
+
/* @__PURE__ */ jsx2(Text2, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
|
|
1635
|
+
" ",
|
|
1636
|
+
(stat?.cpu ?? "-").padStart(6),
|
|
1637
|
+
" ",
|
|
1638
|
+
(stat?.mem ?? "-").padStart(8),
|
|
1639
|
+
" ",
|
|
1640
|
+
String(st.errors).padStart(3),
|
|
1641
|
+
" ",
|
|
1642
|
+
String(st.restarts).padStart(3),
|
|
1643
|
+
" ",
|
|
1644
|
+
up.padStart(6)
|
|
1645
|
+
] });
|
|
1646
|
+
}
|
|
1647
|
+
const resolvedArgs = buildProcessArgs(st.svc).join(" ");
|
|
1648
|
+
const env = redactSecrets(st.svc.extraEnv);
|
|
1649
|
+
const envStr = Object.entries(env).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
1650
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1651
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1652
|
+
indicator,
|
|
1653
|
+
" ",
|
|
1654
|
+
/* @__PURE__ */ jsx2(Text2, { color, children: name.padEnd(ml) }),
|
|
1655
|
+
" ",
|
|
1656
|
+
String(st.svc.port).padStart(5),
|
|
1657
|
+
" ",
|
|
1658
|
+
/* @__PURE__ */ jsx2(Text2, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
|
|
1659
|
+
" ",
|
|
1660
|
+
(stat?.cpu ?? "-").padStart(6),
|
|
1661
|
+
" ",
|
|
1662
|
+
(stat?.mem ?? "-").padStart(8),
|
|
1663
|
+
" ",
|
|
1664
|
+
String(st.errors).padStart(3),
|
|
1665
|
+
" ",
|
|
1666
|
+
String(st.restarts).padStart(3),
|
|
1667
|
+
" ",
|
|
1668
|
+
up.padStart(6)
|
|
1669
|
+
] }),
|
|
1670
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1671
|
+
" cmd: ",
|
|
1672
|
+
st.svc.cmd,
|
|
1673
|
+
" ",
|
|
1674
|
+
resolvedArgs
|
|
1675
|
+
] }),
|
|
1676
|
+
envStr && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1677
|
+
" env: ",
|
|
1678
|
+
envStr
|
|
1679
|
+
] })
|
|
1579
1680
|
] });
|
|
1580
1681
|
}
|
|
1581
1682
|
function ColHeader({ ml }) {
|
|
@@ -1594,7 +1695,7 @@ function ColHeader({ ml }) {
|
|
|
1594
1695
|
"Up".padStart(6)
|
|
1595
1696
|
] });
|
|
1596
1697
|
}
|
|
1597
|
-
function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scrollOffset, resetScroll }) {
|
|
1698
|
+
function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scrollOffset, resetScroll, verbose = false }) {
|
|
1598
1699
|
const names = [...states.keys()];
|
|
1599
1700
|
const stObj = Object.fromEntries([...states].map(([k, v]) => [k, { errors: v.errors }]));
|
|
1600
1701
|
const statsObj = Object.fromEntries([...stats].map(([k, v]) => [k, v]));
|
|
@@ -1633,6 +1734,12 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1633
1734
|
const positionInfo = focused && totalRowsLong > 0 ? `(${effectiveOffset + 1}-${Math.min(effectiveOffset + rowsPerCol, totalRowsLong)}/${totalRowsLong})` : "";
|
|
1634
1735
|
const scrolled = effectiveOffset > 0;
|
|
1635
1736
|
const loopedCount = [...states.values()].filter(isCrashLooped).length;
|
|
1737
|
+
const ramPct = parseFloat(usedGB) / parseFloat(totalGB) * 100;
|
|
1738
|
+
const [ramBanner, setRamBanner] = useState3(false);
|
|
1739
|
+
useEffect4(() => {
|
|
1740
|
+
setRamBanner((prev) => nextRamBannerVisibility(ramPct, prev));
|
|
1741
|
+
}, [ramPct]);
|
|
1742
|
+
const topConsumers = ramBanner ? [...stats.entries()].map(([n, s]) => ({ name: n, mb: parseFloat(s.mem) || 0 })).sort((a, b) => b.mb - a.mb).slice(0, 3) : [];
|
|
1636
1743
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: focused ? "green" : "gray", height, children: [
|
|
1637
1744
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1638
1745
|
/* @__PURE__ */ jsxs2(Text2, { bold: true, color: "green", children: [
|
|
@@ -1674,6 +1781,14 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1674
1781
|
sortMode
|
|
1675
1782
|
] })
|
|
1676
1783
|
] }),
|
|
1784
|
+
ramBanner && /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1785
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "yellow", bold: true, children: [
|
|
1786
|
+
" \u26A0 RAM ",
|
|
1787
|
+
ramPct.toFixed(0),
|
|
1788
|
+
"% \u2014 top: "
|
|
1789
|
+
] }),
|
|
1790
|
+
/* @__PURE__ */ jsx2(Text2, { color: "yellow", children: topConsumers.map((c) => `${c.name} ${c.mb.toFixed(0)}MB`).join(", ") })
|
|
1791
|
+
] }),
|
|
1677
1792
|
/* @__PURE__ */ jsxs2(Box2, { flexGrow: 1, children: [
|
|
1678
1793
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
|
|
1679
1794
|
/* @__PURE__ */ jsxs2(Text2, { bold: true, color: "cyan", children: [
|
|
@@ -1682,7 +1797,7 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1682
1797
|
")"
|
|
1683
1798
|
] }),
|
|
1684
1799
|
/* @__PURE__ */ jsx2(ColHeader, { ml }),
|
|
1685
|
-
visibleApis.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml }, n))
|
|
1800
|
+
visibleApis.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml, verbose }, n))
|
|
1686
1801
|
] }),
|
|
1687
1802
|
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", width: 1, children: Array.from({ length: contentHeight }, (_, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2502" }, i)) }),
|
|
1688
1803
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
|
|
@@ -1692,7 +1807,7 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1692
1807
|
")"
|
|
1693
1808
|
] }),
|
|
1694
1809
|
/* @__PURE__ */ jsx2(ColHeader, { ml }),
|
|
1695
|
-
visibleWebs.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml }, n))
|
|
1810
|
+
visibleWebs.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml, verbose }, n))
|
|
1696
1811
|
] })
|
|
1697
1812
|
] })
|
|
1698
1813
|
] });
|
|
@@ -1717,6 +1832,8 @@ function StatusBar() {
|
|
|
1717
1832
|
" Clear ",
|
|
1718
1833
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "f" }),
|
|
1719
1834
|
" Filter ",
|
|
1835
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "L" }),
|
|
1836
|
+
" Level ",
|
|
1720
1837
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "a" }),
|
|
1721
1838
|
" All ",
|
|
1722
1839
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "r" }),
|
|
@@ -1731,23 +1848,25 @@ function StatusBar() {
|
|
|
1731
1848
|
" Pause ",
|
|
1732
1849
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "t" }),
|
|
1733
1850
|
" Time ",
|
|
1851
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "v" }),
|
|
1852
|
+
" Verbose ",
|
|
1734
1853
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "T" }),
|
|
1735
1854
|
" Proxy"
|
|
1736
1855
|
] }) });
|
|
1737
1856
|
}
|
|
1738
1857
|
|
|
1739
1858
|
// src/tui/ServiceList.tsx
|
|
1740
|
-
import { useState as
|
|
1859
|
+
import { useState as useState4, useMemo as useMemo2 } from "react";
|
|
1741
1860
|
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
1742
1861
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1743
1862
|
function ServiceList({ title, services, onSelect, onClose, filterType }) {
|
|
1744
|
-
const allNames =
|
|
1863
|
+
const allNames = useMemo2(
|
|
1745
1864
|
() => [...services.keys()].filter((n) => !filterType || services.get(n).svc.type === filterType),
|
|
1746
1865
|
[services, filterType]
|
|
1747
1866
|
);
|
|
1748
|
-
const [idx, setIdx] =
|
|
1749
|
-
const [query, setQuery] =
|
|
1750
|
-
const names =
|
|
1867
|
+
const [idx, setIdx] = useState4(0);
|
|
1868
|
+
const [query, setQuery] = useState4("");
|
|
1869
|
+
const names = useMemo2(() => {
|
|
1751
1870
|
if (!query) return allNames;
|
|
1752
1871
|
const q = query.toLowerCase();
|
|
1753
1872
|
return allNames.filter((n) => n.toLowerCase().includes(q));
|
|
@@ -1804,11 +1923,11 @@ function ServiceList({ title, services, onSelect, onClose, filterType }) {
|
|
|
1804
1923
|
}
|
|
1805
1924
|
|
|
1806
1925
|
// src/tui/SearchInput.tsx
|
|
1807
|
-
import { useState as
|
|
1926
|
+
import { useState as useState5 } from "react";
|
|
1808
1927
|
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
1809
1928
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1810
1929
|
function SearchInput({ onSubmit, onClose }) {
|
|
1811
|
-
const [value, setValue] =
|
|
1930
|
+
const [value, setValue] = useState5("");
|
|
1812
1931
|
useInput3((input, key) => {
|
|
1813
1932
|
if (key.escape) onClose();
|
|
1814
1933
|
else if (key.return) onSubmit(value.trim() || null);
|
|
@@ -2044,7 +2163,7 @@ function buildServiceUrl(name, port, proxyActive, proxyOpts) {
|
|
|
2044
2163
|
}
|
|
2045
2164
|
function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider, proxyOpts, logSink }) {
|
|
2046
2165
|
const { stdout } = useStdout();
|
|
2047
|
-
const [rows, setRows] =
|
|
2166
|
+
const [rows, setRows] = useState6(stdout?.rows ?? 40);
|
|
2048
2167
|
useEffect5(() => {
|
|
2049
2168
|
if (!stdout) return;
|
|
2050
2169
|
const onResize = () => setRows(stdout.rows ?? 40);
|
|
@@ -2057,11 +2176,11 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2057
2176
|
const statsHeight = rows - logsHeight - 2;
|
|
2058
2177
|
const maxNameLen = Math.max(...services.map((s) => s.name.length), 10);
|
|
2059
2178
|
const pm = useProcessManager(platform, baseCwd, env, logSink);
|
|
2060
|
-
const [booted, setBooted] =
|
|
2179
|
+
const [booted, setBooted] = useState6(false);
|
|
2061
2180
|
const lazyProxies = useRef3(/* @__PURE__ */ new Map());
|
|
2062
2181
|
const externals = useRef3([]);
|
|
2063
2182
|
const shownTips = useRef3(/* @__PURE__ */ new Set());
|
|
2064
|
-
const [activeTip, setActiveTip] =
|
|
2183
|
+
const [activeTip, setActiveTip] = useState6(null);
|
|
2065
2184
|
const kb = useKeyBindings({
|
|
2066
2185
|
onQuit: () => {
|
|
2067
2186
|
void shutdown();
|
|
@@ -2254,7 +2373,8 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2254
2373
|
height: logsHeight,
|
|
2255
2374
|
focused: kb.panel === "logs",
|
|
2256
2375
|
scrollOffset: kb.logsScrollOffset,
|
|
2257
|
-
resetScroll: kb.resetLogsScroll
|
|
2376
|
+
resetScroll: kb.resetLogsScroll,
|
|
2377
|
+
levelFilter: kb.levelFilter
|
|
2258
2378
|
}
|
|
2259
2379
|
),
|
|
2260
2380
|
/* @__PURE__ */ jsx6(
|
|
@@ -2267,7 +2387,8 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2267
2387
|
height: statsHeight,
|
|
2268
2388
|
focused: kb.panel === "stats",
|
|
2269
2389
|
scrollOffset: kb.statsScrollOffset,
|
|
2270
|
-
resetScroll: kb.resetStatsScroll
|
|
2390
|
+
resetScroll: kb.resetStatsScroll,
|
|
2391
|
+
verbose: kb.verboseStats
|
|
2271
2392
|
}
|
|
2272
2393
|
),
|
|
2273
2394
|
kb.modal === "filter" && /* @__PURE__ */ jsx6(ServiceList, { title: "Filter by service", services: pm.states, onSelect: handleFilterSelect, onClose: () => kb.setModal("none") }),
|