@gachlab/devup 0.1.1 → 0.3.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 (43) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +185 -13
  3. package/dist/config/cli.d.ts +8 -2
  4. package/dist/config/cli.d.ts.map +1 -1
  5. package/dist/config/types.d.ts +40 -0
  6. package/dist/config/types.d.ts.map +1 -1
  7. package/dist/config/validator.d.ts.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +987 -182
  10. package/dist/index.js.map +1 -1
  11. package/dist/lazy/proxy.d.ts.map +1 -1
  12. package/dist/orchestrator/dry-run.d.ts +15 -0
  13. package/dist/orchestrator/dry-run.d.ts.map +1 -0
  14. package/dist/orchestrator/once.d.ts +19 -0
  15. package/dist/orchestrator/once.d.ts.map +1 -0
  16. package/dist/process/external.d.ts +30 -0
  17. package/dist/process/external.d.ts.map +1 -0
  18. package/dist/process/health.d.ts +10 -1
  19. package/dist/process/health.d.ts.map +1 -1
  20. package/dist/process/installer.d.ts +0 -10
  21. package/dist/process/installer.d.ts.map +1 -1
  22. package/dist/process/log-sink.d.ts +23 -0
  23. package/dist/process/log-sink.d.ts.map +1 -0
  24. package/dist/process/manager.d.ts +12 -3
  25. package/dist/process/manager.d.ts.map +1 -1
  26. package/dist/process/types.d.ts +2 -0
  27. package/dist/process/types.d.ts.map +1 -1
  28. package/dist/proxy-config/caddy.d.ts +10 -0
  29. package/dist/proxy-config/caddy.d.ts.map +1 -0
  30. package/dist/proxy-config/detect.d.ts.map +1 -1
  31. package/dist/proxy-config/nginx.d.ts +10 -0
  32. package/dist/proxy-config/nginx.d.ts.map +1 -0
  33. package/dist/tui/App.d.ts +3 -1
  34. package/dist/tui/App.d.ts.map +1 -1
  35. package/dist/tui/LogsPanel.d.ts.map +1 -1
  36. package/dist/tui/StatsPanel.d.ts.map +1 -1
  37. package/dist/tui/hooks/useKeyBindings.d.ts.map +1 -1
  38. package/dist/tui/hooks/useProcessManager.d.ts +7 -3
  39. package/dist/tui/hooks/useProcessManager.d.ts.map +1 -1
  40. package/dist/tui/hooks/useProxySync.d.ts.map +1 -1
  41. package/dist/utils.d.ts +0 -5
  42. package/dist/utils.d.ts.map +1 -1
  43. package/package.json +4 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,112 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@gachlab/devup` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0] — 2026-05-21
9
+
10
+ ### Added
11
+ - **Profiles / scenarios** (#4). New `profiles: Record<string, string[]>` field on `DevStackConfig` plus a `--profile <name>` CLI flag. Lets you save common service-subset combinations under a name (e.g. `'check-in'`, `'pickup'`) and boot them with one short command instead of typing `--services` every time. Composable with `--skip`. Unknown profile names produce a friendly error listing what's available.
12
+ - **`readyPattern` for instant up detection** (#13). New per-service field accepting a plain string or vim-style `/pattern/flags` regex. On the first matching stdout/stderr line devup flips the service to `up` immediately, short-circuiting the next 3-second health-check poll. Speeds up phase transitions when frameworks print recognisable boot lines (Vite's `ready in 423 ms`, Angular's `Compiled successfully`, Fastify's `server listening`). The periodic health-check still runs as a fallback.
13
+ - **`preBuild` and `watchBuild` hooks** (#12). The fields existed in the type but were ignored. Now implemented properly:
14
+ - `preBuild` runs synchronously before the spawn through the platform shell (`sh -c` / `cmd /c`); non-zero exit marks the service `crashed` and skips the spawn.
15
+ - `watchBuild` is spawned as a sibling process and killed (kill-tree) on stop/restart/cleanup.
16
+ - Output is tagged `[build]` / `[watch]` in the logs panel and flows through the same line buffer + log sink pipeline.
17
+ - Replaces the awkward `sh -c 'npm run build && (npx tsup --watch &) && node ...'` workaround in projects with TypeScript services.
18
+ - **`external` / pre hooks for external dependencies** (#14). New top-level `external: ExternalService[]` field for databases, queues, etc. Externals run **before phase 0** through the platform shell with optional `healthCheck` gating and `stopCmd` on shutdown. devup aborts the boot (and runs every `stopCmd`) if any external fails its healthCheck within `startTimeout` (default 60 s). Closes the "do `docker compose up -d` then run devup" loop. Logs are tagged `ext:<name>` and persisted to `~/.devup/logs/<proj>/ext_<name>.log`.
19
+
20
+ ### Changed
21
+ - `filterServices()` now accepts an optional `config` arg to resolve `--profile`. Calls from `index.ts` updated.
22
+ - `--dry-run` header now shows the active profile and a new `Externals (N):` section with each entry's healthCheck tag.
23
+ - `ProcessState` gains an optional `watchProc` field tracking the `watchBuild` side-car.
24
+ - `useProcessManager` exposes `pushLog()` so non-service log lines (externals, future side-cars) flow through the same pause buffer and log sink as regular service lines.
25
+
26
+ ### Fixed
27
+ - Validator catches profile entries that reference unknown services or are empty arrays.
28
+ - Validator catches invalid `readyPattern` regex and empty strings.
29
+ - Validator catches empty `preBuild` / `watchBuild` strings.
30
+ - Validator catches external dependencies with missing `cmd`, duplicate names, missing `port` when a healthCheck is set, or `http` healthCheck paths without a leading `/`.
31
+
32
+ ### Internals
33
+ - New module `src/process/external.ts` (`startExternals` / `stopExternals`).
34
+ - Test suite grown from 200 to ~237 — new suites: `ready-pattern`, `external` (Unix-only, follows the existing skip-on-Windows convention used by integration tests), validator coverage for every new field.
35
+ - Shell-dependent `preBuild`/`watchBuild` integration tests skipped on Windows. The feature itself works on both platforms because the runtime code path already routes through `sh -c` / `cmd /c`; only writing a single test command that exercises spawn behaviour across both shells without per-platform branching is awkward.
36
+
37
+ ## [0.2.0] — 2026-05-21
38
+
39
+ ### Added
40
+ - **HTTP health-checks per service.** New `healthCheck` config field on `ServiceConfig`. Supports `type: 'tcp'` (default) and `type: 'http'` with configurable `path`, `expect` (status code or list), `host`, and `timeoutMs`. Used by both the periodic in-TUI health poll and `--once`.
41
+ - **Persistent log files.** Every line streamed to `~/.devup/logs/<project>/<service>.log`, prefixed with an ISO-8601 timestamp. On each launch the previous file is rotated to `<service>.log.prev`. New flags `--no-log-file` (disable) and `--log-dir <path>` (override root).
42
+ - **`--dry-run`.** Prints the resolved boot plan — phases, commands with their final args/env, lazy proxies with their `realPort`, and the proxy YAML/conf that would be generated — then exits `0` without starting anything.
43
+ - **`--once` (+ `--once-timeout N`).** Boots every service phase-by-phase without rendering the TUI, waits for each API to become healthy, and exits `0` (all up) or `1` (timeout). Default timeout: 90s. Built for CI smoke tests.
44
+ - **Nginx proxy provider.** Generates one `server { }` block per healthy service, with TLS / non-TLS variants and WebSocket-upgrade headers wired by default.
45
+ - **Caddy proxy provider.** Generates a Caddyfile with `reverse_proxy` directives; TLS provisioning is delegated to Caddy by default.
46
+ - **Scroll indicators.** `[SCROLL]` badge appears in the Logs and Stats panel headers when the view is off the natural anchor (bottom for logs, top for stats).
47
+ - **`fmtUptime` now formats days.** Services running longer than a day display as `2d3h` instead of `120h0m`.
48
+
49
+ ### Changed
50
+ - **TUI scroll completely rewritten.** Logs now use a `bottomOffset` model (0 = follow latest, N = N lines back); Stats use a coherent `topOffset` model. Arrow keys, `[`/`]`, `Ctrl+B`/`Ctrl+F`, and `Ctrl+A`/`Ctrl+E` always move in the expected visual direction regardless of which panel is focused.
51
+ - **Auto-pause when scrolling Logs.** New lines are buffered (capped at 5,000) while you're scrolled up, then replayed when you return to the bottom (`Ctrl+E`). The `p` key still works manually.
52
+ - **`p` (pause logs) actually pauses.** Before, it only changed the header label while logs kept streaming.
53
+ - **`c` (clear logs) actually clears.** Was a no-op; now properly cabled to `pm.clearLogs()`.
54
+ - **Manual `r` (restart) resets the auto-restart counter to 0.** Lets the user grant a fresh budget after fixing a flapping service.
55
+ - **`install()` accepts an explicit `colorIdx`.** Install logs no longer all appear in cyan; they match the service's tag color.
56
+ - **`cleanup()` is now async** and the TUI awaits it before `process.exit(0)`. Ensures the SIGKILL fallback (3 s after SIGTERM) actually has time to run.
57
+ - **`useProxySync` no longer recreates its interval on every state change** and skips writes when the generated content hasn't changed.
58
+ - **Reverse proxy provider docs.** README now covers Traefik, Nginx, and Caddy each with a code snippet.
59
+
60
+ ### Fixed
61
+ - **Lazy proxy idle timer respects active connections.** Long-lived connections (WebSockets, SSE, HTTP/2 keep-alive) no longer get the underlying service killed mid-flight. The timer only fires when there are no active connections and no recent activity.
62
+ - **Lazy proxy fails cleanly when the on-demand start fails.** Pending connections are destroyed with a logged error instead of being piped to a dead target.
63
+ - **Log lines are no longer split mid-message.** Per-stream line buffer (`lineBuffer`) reassembles partial chunks from stdout/stderr.
64
+ - **`stderr` error count is no longer inflated.** Was counting blank lines from chunk splits; now counts one error per real line.
65
+ - **Terminal resize is now respected.** `stdout.on('resize')` re-renders the layout. Before, the height was captured on first paint and never updated.
66
+ - **Validator detects lazy-port collisions** between `service.port` and `otherService.port + 10000` and reports them at config-load time.
67
+ - **Validator validates `healthCheck` shape.** Rejects unknown `type` values and paths without a leading `/`.
68
+ - **Key bindings: `Ctrl+F` (page down) no longer triggers the filter modal.** Reordered handler so ctrl-modified keys are checked before single-letter shortcuts.
69
+
70
+ ### Removed
71
+ - Dead `blessed`-style helpers from `utils.ts`: `highlightSearch`, `findSearchMatch`, `formatLogLine`, `shouldLogLine`, `buildLogsLabel`. The TUI is fully Ink-based and never used them.
72
+ - `installBatch` from `installer.ts`. Unused, with a subtle race in its `Promise.race` cleanup.
73
+
74
+ ### Internals
75
+ - Test suite grown from 122 to 200 (`+78`). New: `health.test.ts` HTTP cases, `log-sink.test.ts`, `dry-run.test.ts`, `once.test.ts` (integration), `nginx.test.ts`, `caddy.test.ts`.
76
+ - New `src/orchestrator/` directory (`dry-run.ts`, `once.ts`) separates non-TUI flows from the React layer.
77
+
78
+ ## [0.1.1] — 2026-05-07
79
+
80
+ ### Added
81
+ - TUI panel navigation: `Tab` to switch focus between Logs and Stats, with focused-border highlighting.
82
+
83
+ ### Fixed
84
+ - Cross-platform glob quoting in `test:*` npm scripts (Windows).
85
+ - Integration tests: more socket-error codes accepted (Windows), `os.tmpdir()` in validator test (Windows), longer timeouts on macOS CI, lifecycle test enabled on macOS while skipped on Windows.
86
+
87
+ ### CI / packaging
88
+ - GitHub Actions workflow runs on Linux, macOS, and Windows.
89
+ - Split unit (every branch, 3 OSes) and integration (main only, 3 OSes) jobs.
90
+ - Upgraded actions to v5 / Node 24 runner.
91
+ - Publish workflow added — triggered by GitHub Release, runs tests on 3 OSes, then publishes to npm using trusted publishing (OIDC, no `NPM_TOKEN`).
92
+
93
+ ## [0.1.0] — 2026-05-01
94
+
95
+ Initial release.
96
+
97
+ ### Added
98
+ - Phased startup of services with TCP port-readiness detection.
99
+ - Lazy mode: on-demand start via a TCP proxy on the public port; service runs on `port + 10000`; idle timeout to stop the underlying process.
100
+ - Cross-platform process management (Linux/macOS via `ps` + `kill -pid`; Windows via `wmic` + `taskkill /T /F`) and browser-opening (`xdg-open`, `open`, `cmd /c start`).
101
+ - TUI dashboard built with Ink: live logs (filter, search, pause, timestamps), stats panel (CPU, memory, health, errors, restarts) with sort modes.
102
+ - Reverse-proxy config generation: Traefik file provider (YAML), health-aware (only `health === 'up'` services routed).
103
+ - Automatic dependency installation with hash-based stamps to skip redundant `npm install`s.
104
+ - Auto-restart with exponential backoff (2s → 4s → 8s), capped at 3 attempts.
105
+ - Port-in-use detection before starting a service.
106
+ - Config file resolution order: `devup.config.ts` → `.js` → `.json`, with `--config <path>` override. TypeScript loaded via the `tsx` import hook.
107
+ - CLI flags: `--only`, `--services`, `--skip`, `--lazy`/`--no-lazy`, `--timeout`, `--proxy`, `--proxy-host`, `--proxy-conf`, `--proxy-tls`/`--no-proxy-tls`, `--proxy-entrypoint`, `--config`.
108
+
109
+ [0.3.0]: https://github.com/gachlab/devup/releases/tag/0.3.0
110
+ [0.2.0]: https://github.com/gachlab/devup/releases/tag/0.2.0
111
+ [0.1.1]: https://github.com/gachlab/devup/releases/tag/0.1.1
112
+ [0.1.0]: https://github.com/gachlab/devup/releases/tag/0.1.0
package/README.md CHANGED
@@ -9,14 +9,17 @@ Built with TypeScript 6, Ink (React for terminals), and zero test dependencies (
9
9
  ## Features
10
10
 
11
11
  - **Phased startup** — boot services in dependency order with automatic port readiness detection
12
- - **Lazy mode** — only start services when they receive traffic. Idle services shut down after a configurable timeout
13
- - **TUI dashboard** — live logs and process stats (CPU, memory, health, errors, restarts) in a split-panel terminal UI
12
+ - **Lazy mode** — only start services when they receive traffic. Idle services shut down after a configurable timeout (respects active connections, no killing mid-WebSocket)
13
+ - **TUI dashboard** — live logs and process stats (CPU, memory, health, errors, restarts) in a split-panel terminal UI with scrolling, search, filter, and auto-pause when you scroll up
14
14
  - **Cross-platform** — Linux, macOS, and Windows. Platform-specific process management, stats collection, and browser opening
15
- - **Reverse proxy config** — generate Traefik (or other) dynamic config from running services. Health-aware: only routes to healthy services
15
+ - **HTTP or TCP health checks** — per-service `healthCheck` config: TCP probe (default) or HTTP GET with configurable path and status codes
16
+ - **Reverse proxy config** — generate Traefik, Nginx, or Caddy config from running services. Health-aware: only routes to healthy services
17
+ - **Persistent logs** — every line streamed to `~/.devup/logs/<project>/<svc>.log` with rotation on each launch
18
+ - **CI-ready** — `--dry-run` prints the boot plan; `--once` boots, waits for readiness, exits `0/1` without a TUI
16
19
  - **Project-agnostic** — works with any Node.js monorepo. Your project defines a `devup.config.ts`, devup does the rest
17
20
  - **npm install management** — automatic dependency installation with hash-based stamps to skip redundant installs
18
- - **Auto-restart with backoff** — crashed services restart automatically with exponential backoff (2s → 4s → 8s), max 3 attempts
19
- - **Port conflict detection** — checks if a port is already in use before starting a service
21
+ - **Auto-restart with backoff** — crashed services restart automatically with exponential backoff (2s → 4s → 8s), max 3 attempts; manual restart resets the counter
22
+ - **Port conflict detection** — checks if a port is already in use before starting a service; also validates lazy `port + 10000` collisions at config-load time
20
23
 
21
24
  ## Quick start
22
25
 
@@ -82,6 +85,25 @@ npx devup
82
85
  | `services` | `ServiceConfig[]` | ✅ | List of services to manage |
83
86
  | `lazy` | `LazyConfig` | | Lazy mode configuration |
84
87
  | `proxy` | `ProxyConfig` | | Reverse proxy config generation |
88
+ | `profiles` | `Record<string, string[]>` | | Named lists of services to boot. Select with `--profile <name>` |
89
+ | `external` | `ExternalService[]` | | Dependencies started **before** phase 0 (databases, queues, etc.) |
90
+
91
+ #### Profiles
92
+
93
+ A profile is a named subset of `services`. Instead of memorising service names for `--services`, you give your common workflows a name:
94
+
95
+ ```typescript
96
+ export default defineConfig({
97
+ // ...
98
+ profiles: {
99
+ 'check-in': ['configurations-api', 'authorization-api', 'app-api', 'check-in-api', 'app-web'],
100
+ 'pickup': ['configurations-api', 'pickup-api', 'pickup-drivers-web'],
101
+ 'frontends': ['app-web', 'admin-web', 'staff-web'],
102
+ },
103
+ });
104
+ ```
105
+
106
+ Then `devup --profile check-in` boots that subset. Composable with `--skip`. The validator catches typos at config-load time. Unknown profile names produce a friendly error listing what's available.
85
107
 
86
108
  ### `ServiceConfig`
87
109
 
@@ -95,10 +117,105 @@ npx devup
95
117
  | `port` | `number` | ✅ | Port the service listens on. Must be unique |
96
118
  | `phase` | `number` | ✅ | Startup phase (0 = first). Services in the same phase start together; devup waits for all APIs in a phase to be ready before starting the next phase |
97
119
  | `maxMem` | `number` | | Max memory in MB. Injects `--max-old-space-size` for `node` commands, or `NODE_OPTIONS` for `npx` |
98
- | `preBuild` | `string` | | Command to run before starting (e.g., `npm run build`) |
99
- | `watchBuild` | `string` | | Watch command to run alongside the service (e.g., `npx tsup --watch`) |
120
+ | `preBuild` | `string` | | Shell command run **before** the service starts. If it exits non-zero the service is marked `crashed` and skipped. Output is tagged `[build]` in the logs panel |
121
+ | `watchBuild` | `string` | | Shell command spawned **alongside** the service (e.g. `npx tsup --watch`). Killed automatically when the service stops/restarts. Output is tagged `[watch]` in the logs panel |
100
122
  | `nodeArgs` | `string[]` | | Extra Node.js arguments |
101
123
  | `extraEnv` | `Record<string, string>` | | Extra environment variables for this service |
124
+ | `healthCheck` | `HealthCheckConfig` | | Override the readiness check for this service. Default: TCP probe on `port` |
125
+ | `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 |
126
+
127
+ ### `HealthCheckConfig`
128
+
129
+ | Field | Type | Required | Description |
130
+ |---|---|---|---|
131
+ | `type` | `'tcp' \| 'http'` | ✅ | `tcp` (default) just opens a socket; `http` issues an HTTP GET and inspects the status code |
132
+ | `path` | `string` | | HTTP-only request path. Default: `/`. Must start with `/` |
133
+ | `expect` | `number \| number[]` | | HTTP-only acceptable status code(s). Default: any 2xx (200-299) |
134
+ | `host` | `string` | | Override target host for the HTTP check. Default: `127.0.0.1` |
135
+ | `timeoutMs` | `number` | | Per-check socket/request timeout in ms. Default: `2000` |
136
+
137
+ ```typescript
138
+ // Wait for /healthz to return 200 before considering the service up
139
+ { name: 'api', /* ... */, healthCheck: { type: 'http', path: '/healthz' } }
140
+
141
+ // Accept 200 or 204
142
+ { name: 'api', /* ... */, healthCheck: { type: 'http', path: '/health', expect: [200, 204] } }
143
+ ```
144
+
145
+ #### readyPattern
146
+
147
+ A regex matched against each line of the service's stdout/stderr. The first matching line flips the service to `up` immediately, without waiting for the next 3-second health-check poll. The periodic health-check still runs as a fallback. Useful for tools that print recognisable boot lines:
148
+
149
+ ```typescript
150
+ // Vite: "ready in 423 ms"
151
+ { name: 'web', cmd: 'npx', args: ['vite'], readyPattern: 'ready in' }
152
+
153
+ // Angular: "Compiled successfully"
154
+ { name: 'app', cmd: 'npx', args: ['ng', 'serve'], readyPattern: '/compiled successfully/i' }
155
+
156
+ // Fastify: "Server listening at"
157
+ { name: 'api', cmd: 'node', args: ['index.js'], readyPattern: 'server listening' }
158
+ ```
159
+
160
+ Both plain strings and vim-style `/pattern/flags` are accepted. Strings are case-insensitive by default.
161
+
162
+ #### Build hooks: `preBuild` and `watchBuild`
163
+
164
+ For TypeScript services or anything that needs a compile step, two hooks remove the usual `sh -c 'npm run build && (npx tsup --watch &) && node ...'` workaround:
165
+
166
+ ```typescript
167
+ {
168
+ name: 'orders-api',
169
+ cwd: 'orders/api',
170
+ cmd: 'node', args: ['dist/index.js'],
171
+ type: 'api', port: 3031, phase: 1,
172
+ preBuild: 'npm run build', // runs once, must succeed before the service starts
173
+ watchBuild: 'npx tsup --watch', // runs in parallel; killed when the service stops
174
+ }
175
+ ```
176
+
177
+ - **`preBuild`** runs synchronously before the spawn. Non-zero exit marks the service as `crashed` (the spawn is skipped). Output is tagged `[build]` in the logs panel.
178
+ - **`watchBuild`** runs as a sibling process. devup kills it when the service stops, restarts, or the TUI exits. Output is tagged `[watch]`.
179
+
180
+ Both are passed through the platform shell (`sh -c` on Unix, `cmd /c` on Windows), so pipes and `&&` work.
181
+
182
+ ### `ExternalService`
183
+
184
+ External dependencies (databases, message queues, etc.) launched **before** phase 0. devup waits for each `healthCheck` (when set) to pass before starting any service. Typical use: `docker compose up -d`.
185
+
186
+ | Field | Type | Required | Description |
187
+ |---|---|---|---|
188
+ | `name` | `string` | ✅ | Friendly name shown in logs (logs use the prefix `ext:<name>`) |
189
+ | `cmd` | `string` | ✅ | Shell command (passed through `sh -c` / `cmd /c`). Pipes and `&&` work |
190
+ | `cwd` | `string` | | Working directory relative to the project root |
191
+ | `extraEnv` | `Record<string, string>` | | Extra env vars merged on top of the project env |
192
+ | `healthCheck` | `HealthCheckConfig` | | Readiness probe. devup waits for `up` before starting phase 0 |
193
+ | `port` | `number` | when `healthCheck` is set | Port to probe |
194
+ | `startTimeout` | `number` | | Max seconds to wait for healthCheck. Default: `60` |
195
+ | `stopCmd` | `string` | | Shell command run on shutdown (e.g. `docker compose down`) |
196
+
197
+ ```typescript
198
+ export default defineConfig({
199
+ // ...
200
+ external: [
201
+ {
202
+ name: 'mongo',
203
+ cmd: 'docker compose -f docker-compose.dev.yml up -d mongo',
204
+ port: 27017,
205
+ healthCheck: { type: 'tcp' },
206
+ stopCmd: 'docker compose -f docker-compose.dev.yml stop mongo',
207
+ },
208
+ {
209
+ name: 'redis',
210
+ cmd: 'docker compose -f docker-compose.dev.yml up -d redis',
211
+ port: 6379,
212
+ healthCheck: { type: 'tcp' },
213
+ },
214
+ ],
215
+ });
216
+ ```
217
+
218
+ If any external fails its healthCheck within `startTimeout` seconds, devup aborts the boot, runs each external's `stopCmd` (best-effort), and exits.
102
219
 
103
220
  ### `LazyConfig`
104
221
 
@@ -113,7 +230,7 @@ When lazy mode is active (default), services not in `alwaysOn` start a TCP proxy
113
230
 
114
231
  | Field | Type | Required | Description |
115
232
  |---|---|---|---|
116
- | `provider` | `string` | ✅ | Proxy provider name. Currently: `'traefik'` |
233
+ | `provider` | `string` | ✅ | Proxy provider name. One of: `'traefik'`, `'nginx'`, `'caddy'` |
117
234
  | `routes` | `Record<string, string>` | ✅ | Map of service name → subdomain. Empty string = root domain |
118
235
  | `confPath` | `string` | | Path to write the config file. Default: `~/.traefik/traefik_conf.yaml` |
119
236
  | `host` | `string` | | Target host for proxy URLs. Default: auto-detected per platform |
@@ -135,6 +252,7 @@ devup [options]
135
252
  | `--only apis` | Only start API services |
136
253
  | `--only webs` | Only start web services |
137
254
  | `--services api,web,auth` | Start only the named services |
255
+ | `--profile <name>` | Start the services in the named profile (see `profiles` in the config) |
138
256
  | `--skip tasks-api,pickup-api` | Start everything except these |
139
257
  | `--config path/to/config.ts` | Use a custom config file |
140
258
 
@@ -155,7 +273,24 @@ devup [options]
155
273
  | `--proxy-conf /path/to/file` | Override config file path |
156
274
  | `--proxy-tls` | Enable TLS (default) |
157
275
  | `--no-proxy-tls` | Disable TLS |
158
- | `--proxy-entrypoint web` | Override entrypoint name |
276
+ | `--proxy-entrypoint web` | Override entrypoint name (Traefik-only) |
277
+
278
+ ### CI / scripting
279
+
280
+ | Flag | Description |
281
+ |---|---|
282
+ | `--dry-run` | Print the resolved boot plan (phases, commands, proxy YAML) and exit. Doesn't start any process |
283
+ | `--once` | Boot services, wait until every API is healthy, then exit `0` (or `1` on timeout). Skips the TUI — meant for CI smoke tests |
284
+ | `--once-timeout 60` | Max seconds to wait in `--once` mode. Default: `90` |
285
+
286
+ ### Log files
287
+
288
+ | Flag | Description |
289
+ |---|---|
290
+ | `--no-log-file` | Disable persistent log files |
291
+ | `--log-dir /path` | Override log root. Default: `~/.devup/logs/<project>/<service>.log` |
292
+
293
+ devup writes a separate `.log` file per service to disk. Lines are prefixed with an ISO timestamp. On each fresh launch the previous file is rotated to `<service>.log.prev`, so you always have at most two runs of history per service.
159
294
 
160
295
  ## TUI keybindings
161
296
 
@@ -163,10 +298,13 @@ devup [options]
163
298
  |---|---|
164
299
  | `q` / `Ctrl+C` | Quit and stop all services |
165
300
  | `Tab` | Switch focus between Logs and Stats panels |
301
+ | `↑` / `↓` | Scroll the focused panel by 1 line/row |
302
+ | `[` / `]` (or `Ctrl+B` / `Ctrl+F`) | Page up / page down |
303
+ | `Ctrl+A` / `Ctrl+E` | Jump to top / bottom of the focused panel |
166
304
  | `f` | Filter logs by service |
167
305
  | `a` | Show all logs (clear filter) |
168
306
  | `/` | Search in logs |
169
- | `p` | Pause/resume log output |
307
+ | `p` | Pause/resume log output (auto-engaged when you scroll up) |
170
308
  | `t` | Toggle timestamps |
171
309
  | `c` | Clear logs |
172
310
  | `s` | Cycle sort mode (name → memory → errors) |
@@ -174,6 +312,8 @@ devup [options]
174
312
  | `o` | Open a web service in browser |
175
313
  | `T` | Toggle reverse proxy config sync |
176
314
 
315
+ 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).
316
+
177
317
  ## Config file formats
178
318
 
179
319
  devup looks for config files in this order:
@@ -227,7 +367,7 @@ Services listed in `lazy.alwaysOn` skip the proxy and start normally.
227
367
 
228
368
  ## Reverse proxy providers
229
369
 
230
- devup generates dynamic config for reverse proxies. Currently supported:
370
+ devup generates dynamic config for reverse proxies. Three providers are built in: **Traefik**, **Nginx**, and **Caddy**. Only services with `health === 'up'` are included — flapping services are silently dropped from the generated config and re-added when they recover.
231
371
 
232
372
  ### Traefik
233
373
 
@@ -245,7 +385,39 @@ services:
245
385
  devup --proxy --proxy-host 172.17.0.1
246
386
  ```
247
387
 
248
- Adding a new provider (Nginx, Caddy, etc.) requires implementing the `ProxyConfigProvider` interface:
388
+ ### Nginx
389
+
390
+ Generates an Nginx file with one `server { }` block per healthy service. Drop it into `/etc/nginx/conf.d/` (or `include` it from your main config) and reload Nginx — devup rewrites the file in place every 3 seconds.
391
+
392
+ ```typescript
393
+ proxy: {
394
+ provider: 'nginx',
395
+ confPath: '/etc/nginx/conf.d/devup.conf',
396
+ routes: { 'app-web': '', 'api': 'api' },
397
+ }
398
+ ```
399
+
400
+ With `tls: true` (default) each block listens on `:443 ssl` and points to `/etc/nginx/certs/<server_name>.crt` and `.key`. With `tls: false` it listens on `:80`. WebSocket / HTTP-upgrade headers are forwarded by default.
401
+
402
+ > **Note:** Nginx doesn't watch files automatically — you'll need `nginx -s reload` (or `nginx-reload` sidecar) to pick up devup's updates. For a watch-and-reload workflow, prefer Traefik or Caddy.
403
+
404
+ ### Caddy
405
+
406
+ Generates a Caddyfile with one `reverse_proxy` directive per healthy service. Caddy auto-reloads its file on change (`caddy run --watch`), so devup's updates take effect without intervention.
407
+
408
+ ```typescript
409
+ proxy: {
410
+ provider: 'caddy',
411
+ confPath: '/etc/caddy/devup.Caddyfile',
412
+ routes: { 'app-web': '', 'api': 'api' },
413
+ }
414
+ ```
415
+
416
+ With `tls: true` (default) Caddy provisions TLS automatically (Let's Encrypt or local CA). With `tls: false` each site is prefixed with `http://`.
417
+
418
+ ### Adding a custom provider
419
+
420
+ Implement the `ProxyConfigProvider` interface and register it manually before calling `render(<App />)`:
249
421
 
250
422
  ```typescript
251
423
  interface ProxyConfigProvider {
@@ -323,7 +495,7 @@ git clone https://github.com/gachlab/devup.git
323
495
  cd devup
324
496
  npm install
325
497
  npm run build
326
- npm test # 112 tests, node:test native
498
+ npm test # 200 tests, node:test native
327
499
  npm run test:coverage # coverage report
328
500
  ```
329
501
 
@@ -1,9 +1,10 @@
1
- import type { ServiceConfig } from './types.js';
1
+ import type { DevStackConfig, ServiceConfig } from './types.js';
2
2
  export interface CliArgs {
3
3
  configPath?: string;
4
4
  only?: string;
5
5
  skip: string[];
6
6
  services?: string[];
7
+ profile?: string;
7
8
  lazy: boolean;
8
9
  lazyTimeout: number;
9
10
  proxy: boolean;
@@ -11,7 +12,12 @@ export interface CliArgs {
11
12
  proxyConf?: string;
12
13
  proxyTls: boolean;
13
14
  proxyEntrypoint: string;
15
+ dryRun: boolean;
16
+ once: boolean;
17
+ onceTimeout: number;
18
+ logFile: boolean;
19
+ logDir?: string;
14
20
  }
15
21
  export declare function parseCliArgs(argv: string[]): CliArgs;
16
- export declare function filterServices(services: ServiceConfig[], args: CliArgs): ServiceConfig[];
22
+ export declare function filterServices(services: ServiceConfig[], args: CliArgs, config?: Pick<DevStackConfig, 'profiles'>): ServiceConfig[];
17
23
  //# sourceMappingURL=cli.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/config/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACzB;AAID,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAgCpD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,GAAG,aAAa,EAAE,CAoBxF"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/config/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAKD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CA0CpD;AAED,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,aAAa,EAAE,EACzB,IAAI,EAAE,OAAO,EACb,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,UAAU,CAAC,GACxC,aAAa,EAAE,CA6BjB"}
@@ -11,6 +11,24 @@ export interface ServiceConfig {
11
11
  watchBuild?: string;
12
12
  nodeArgs?: string[];
13
13
  extraEnv?: Record<string, string>;
14
+ healthCheck?: HealthCheckConfig;
15
+ /** Case-insensitive regex. When a line of the service's stdout/stderr matches,
16
+ * the service is immediately marked as `up` without waiting for the next
17
+ * health-check poll. Speeds up phase transitions on cold boots.
18
+ * Examples: '/ready in \\d+ ms/' (Vite), '/compiled successfully/' (Angular). */
19
+ readyPattern?: string;
20
+ }
21
+ export interface HealthCheckConfig {
22
+ /** 'tcp' (default) checks that the port accepts connections. 'http' makes an HTTP GET. */
23
+ type: 'tcp' | 'http';
24
+ /** HTTP-only: request path. Default: '/' */
25
+ path?: string;
26
+ /** HTTP-only: acceptable status code(s). Default: 200-299 */
27
+ expect?: number | number[];
28
+ /** Override host for the HTTP check. Default: 127.0.0.1 */
29
+ host?: string;
30
+ /** Per-check socket timeout in ms. Default: 2000 */
31
+ timeoutMs?: number;
14
32
  }
15
33
  export interface LazyConfig {
16
34
  alwaysOn: string[];
@@ -24,6 +42,24 @@ export interface ProxyConfig {
24
42
  tls?: boolean;
25
43
  entrypoint?: string;
26
44
  }
45
+ export interface ExternalService {
46
+ /** Friendly name (used in logs and the stats panel). Must be unique within `external`. */
47
+ name: string;
48
+ /** Shell command to start. Will be passed through `sh -c` / `cmd /c`. */
49
+ cmd: string;
50
+ /** Optional working directory (relative to the project root). */
51
+ cwd?: string;
52
+ /** Extra env vars merged on top of the project env. */
53
+ extraEnv?: Record<string, string>;
54
+ /** Optional readiness probe. devup waits for this to return `up` before starting phase 0. */
55
+ healthCheck?: HealthCheckConfig;
56
+ /** Port to probe when `healthCheck` is set. Required for tcp checks. */
57
+ port?: number;
58
+ /** Max seconds to wait for healthCheck to pass before giving up. Default: 60. */
59
+ startTimeout?: number;
60
+ /** Optional shell command run on shutdown (e.g. `docker compose down`). */
61
+ stopCmd?: string;
62
+ }
27
63
  export interface DevStackConfig {
28
64
  name: string;
29
65
  icon?: string;
@@ -32,6 +68,10 @@ export interface DevStackConfig {
32
68
  services: ServiceConfig[];
33
69
  lazy?: LazyConfig;
34
70
  proxy?: ProxyConfig;
71
+ /** Named lists of service names — selectable with --profile <name>. */
72
+ profiles?: Record<string, string[]>;
73
+ /** Optional external dependencies (DBs, queues) started before phase 0. */
74
+ external?: ExternalService[];
35
75
  }
36
76
  export declare function defineConfig(config: DevStackConfig): DevStackConfig;
37
77
  //# sourceMappingURL=types.d.ts.map
@@ -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;CACnC;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,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;CACrB;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAEnE"}
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;CACpB;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;AAEjD,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,CAuErF;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,CAExE"}
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,CAwKrF;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,CAExE"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChG,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChG,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC"}