@monster-orange/den 0.2.4 → 0.2.6

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 (40) hide show
  1. package/CHANGELOG.md +195 -0
  2. package/docs/distributions.md +158 -16
  3. package/package.json +1 -1
  4. package/src/pillars/security/discover.ts +1 -1
  5. package/src/pillars/smoke/setup-global.ts +2 -1
  6. package/src/scripts/configure-aws-profiles.ts +2 -1
  7. package/src/scripts/register-repos.ts +4 -3
  8. package/src/shared/constants.ts +7 -0
  9. package/src/shared/den-config.ts +130 -12
  10. package/src/shared/engine.ts +18 -0
  11. package/src/shared/environment-manager/default-workspace.ts +229 -87
  12. package/src/shared/environment-manager/health.ts +235 -35
  13. package/src/shared/manifest/placement.ts +1 -1
  14. package/src/shared/manifest/schema.ts +47 -0
  15. package/src/shared/manifest/service-links.ts +1 -1
  16. package/src/shared/manifest/validate-manifest.ts +1 -1
  17. package/src/shared/plural.ts +13 -0
  18. package/src/shared/service-url.ts +2 -5
  19. package/src/shared/stacks/compose-health.ts +242 -0
  20. package/src/shared/stacks/runtime.ts +73 -24
  21. package/src/shared/stacks/status.ts +3 -2
  22. package/src/shared/types/index.ts +13 -1
  23. package/src/ui/cli/create.ts +2 -1
  24. package/src/ui/cli/health-display.ts +1 -0
  25. package/src/ui/cli/index.ts +86 -41
  26. package/src/ui/cli/mcp-connect.ts +36 -5
  27. package/src/ui/cli/server-identity.ts +233 -0
  28. package/src/ui/cli/service-command.ts +136 -12
  29. package/src/ui/cli/spinner.ts +48 -0
  30. package/src/ui/cli/stack-command.ts +40 -12
  31. package/src/ui/cli/web-command.ts +101 -32
  32. package/src/ui/mcp/api.ts +94 -1
  33. package/src/ui/mcp/server.ts +6 -1
  34. package/src/ui/mcp/tools.ts +6 -2
  35. package/src/ui/server/health-row.ts +6 -3
  36. package/src/ui/server/index.ts +120 -40
  37. package/src/ui/server/public/changelog-view.js +16 -0
  38. package/src/ui/server/public/header.js +121 -12
  39. package/src/ui/server/ui-assets.ts +1 -0
  40. package/src/ui/server/workspace-routes.ts +7 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,201 @@ All notable changes to Den are documented here. Each release lists the
4
4
  config and manifest changes a distribution must make to move to it under
5
5
  **Migrations**.
6
6
 
7
+ ## [0.2.6] - 2026-11-04
8
+
9
+ ### Fixed
10
+
11
+ - **`<cli> web restart` and `web down` act on our server from an older
12
+ Den.** A server from before 0.2.6 (no `root` in `/api/server-info`) run
13
+ from our root is ours, and 0.2.6-dev.0 classified it so, but stopping it
14
+ then looked for our pids by command line only: the entry point of this
15
+ engine, or one under the root. An older Den installed elsewhere, or a
16
+ hand-started server, matched neither, so `web restart` printed "OK ... web
17
+ is already running" and restarted nothing, and `web down` printed "OK ...
18
+ web is not running" while the server stayed up and bound. A server the
19
+ identity check finds ours is now stopped by the pid it reports, when that
20
+ pid listens on the port: `restart` replaces it and `down` frees the port,
21
+ with or without a `web.json` for it. web.json's root is compared after
22
+ resolving symlinks (`/var` and `/private/var` are one root).
23
+ - **The MCP server serves nothing from another distribution's server.**
24
+ It read from whatever answered on its port. Before a tool reads, it now
25
+ checks `/api/server-info` as `web up` does; on another distribution's
26
+ server every tool answers `port <N> is in use by <product> at <root>; not
27
+ this distribution's server` and returns none of its data. A positive
28
+ check is trusted for 30s and made again after a connection error, so a
29
+ server that took over the port is noticed. A server that does not say
30
+ whose it is within 5s is not read from either.
31
+ - **A mistyped `service` flag no longer installs.** `<cli> service install
32
+ --anything` ignored the flag and installed; a `service` subcommand now
33
+ refuses an argument it does not take, before running anything.
34
+ - **`<cli> web up` no longer trusts whatever answers on its port.** With
35
+ `web.port` set to a port where another distribution's Den server ran
36
+ (another root, an older Den), `web up` printed "OK ... web is already
37
+ running" and stopped, and `web down` or `web restart` could SIGTERM that
38
+ server: with a linked engine its command line looks like ours.
39
+ `/api/server-info` now also returns `root` (the distribution root the
40
+ server serves) and `product`. `web up`, `web down` and `web restart`
41
+ compare both with their own, and on another distribution's server exit 1
42
+ with `port <N> is in use by <product> at <root>` and a hint to pick
43
+ another port (`--port`, or `web.port`). A server from before 0.2.6 has
44
+ no `root`: it is ours when it runs from our root (its working
45
+ directory), otherwise `port <N> is in use by another server (Den
46
+ <version>)`; anything else is "a non-Den server". `web down` also no
47
+ longer signals a process running from another root, even when the
48
+ server does not answer. `<cli> mcp connect` refuses to point an agent at
49
+ another distribution's server (and `mcp clients` names it), and `<cli>
50
+ service install` no longer reports "running" when another
51
+ distribution's server answers on the port.
52
+ - **A repeat `<cli> workspace seed` says "already seeded".** Seeding a
53
+ workspace that already holds exactly the blueprint printed "Default
54
+ workspace ready" as if it had seeded again; it now prints `✓ Default
55
+ workspace already seeded — N repos (managed): nothing changed.` and
56
+ writes nothing (multi blueprint: `✓ N workspaces already seeded (a, b)`).
57
+ A seed over a workspace the user changed says it was "reset to the
58
+ blueprint".
59
+ - **Counts read "1 repo", not "1 repos".** CLI output that counts repos,
60
+ workspaces, systems, files, profiles and tools uses one plural helper
61
+ (`src/shared/plural.ts`), also in place of "repo(s)".
62
+ - **Spinners no longer loop forever on a 0-column terminal.** On a pty that
63
+ reports 0 columns (one nobody sized, as agent shells give), ora computed
64
+ an infinite number of lines to clear and never finished. A spinner now
65
+ animates only on a TTY with a known, non-zero width; otherwise it prints
66
+ plain lines (its text on start, then the result).
67
+
68
+ ### Added
69
+
70
+ - **Health type `container`.** `health: { type: container, project: <compose
71
+ project>, service: <compose service> }` checks a compose container by its
72
+ `com.docker.compose.project` / `com.docker.compose.service` labels, or
73
+ `{ type: container, container_name: <name> }` any container by name. It
74
+ is answered from the one `docker ps` per health cycle the compose checks
75
+ already share (its format gains `{{.Names}}`, and it no longer filters to
76
+ compose containers), so N container checks add no docker call, in the
77
+ web health row and in `<cli> status` (22 checks, 17 compose and 5
78
+ container: 1 docker call per cycle). A running container with a
79
+ healthcheck is its health (healthy up, unhealthy down, starting
80
+ pending), one without is up, and no matching container is down. When
81
+ docker gives no listing it says what a compose check says, through the
82
+ same code: a failed `docker ps` is unknown (`ok: null`, the reason in
83
+ `error`) on every cycle, never down; a listing past its budget is
84
+ pending (`pending: true`), down only when that repeats. Of several matches the best counts:
85
+ healthy, then running. `/api/health` rows show it as `check: "container
86
+ <project>/<service>"`. `<cli> validate` requires `project` + `service` or
87
+ `container_name` (not both, not one of the pair alone, no other type's
88
+ keys) and says which. See docs/distributions.md, "Health checks".
89
+ - **`<cli> service install --plan`.** Read-only: prints the label, the
90
+ plist path, the log file, the roots, whose server answers on `web.port`
91
+ (ours, free, or `port <N> is in use by <product> at <root>`) and the
92
+ plist install would write; writes nothing and never calls launchctl.
93
+ Exits 1 when the port is another distribution's. `<cli> service status`
94
+ also names another distribution's server on the port.
95
+
96
+ ### Migrations
97
+
98
+ - None required. Optional: a `type: shell` check that runs `docker ps` to
99
+ find a container (`test -n "$(docker ps -q --filter
100
+ label=com.docker.compose.project=X --filter
101
+ label=com.docker.compose.service=Y)"`) can become `{ type: container,
102
+ project: X, service: Y }`: no process or docker call of its own, and it
103
+ reads the container's healthcheck.
104
+
105
+ ## [0.2.5] - 2026-11-04
106
+
107
+ ### Fixed
108
+
109
+ - **`<cli> web down` (any SIGTERM) shuts the web server down cleanly
110
+ under load.** `<cli> web up` ran the server under the `tsx` CLI, a
111
+ parent process that relays a signal to the server and, if the server has
112
+ not acknowledged it within about 60ms, SIGKILLs it and exits 143. On a
113
+ loaded machine the server missed that window, so it was killed without
114
+ its shutdown. The server now runs as `node --import <tsx loader>`, one
115
+ process that gets the signal itself, as the launchd service already did.
116
+ - **Compose-stack health no longer starts a CLI per check.** Den's own
117
+ check for a compose repo was a `<cli> stack health <stack> --service X`
118
+ process per manifest service, each a full CLI start running its own
119
+ `docker ps`: 17-19 processes per health cycle for three stacks. On a
120
+ loaded machine (load 35-55) they ran past their budget, and two such
121
+ cycles in a row read every stack down. These checks are now answered
122
+ in the web server and in `<cli> status` from one `docker ps` per cycle,
123
+ shared by every check in it (17 checks: 17 CLI starts and 17 docker
124
+ calls per cycle before, 0 and 1 now). The verdict is the one `stack
125
+ health` gives. A distribution's own shell checks still run as shell
126
+ commands, at most max(2, cpus/2) at a time; a queued check's budget
127
+ starts when it starts.
128
+ - **The health row no longer shows healthy repos down on a cold poll.** A
129
+ check that got no answer within its budget was sent as a finished
130
+ `ok: false`: a compose stack's check (`<cli> stack health`, a full CLI
131
+ start per service, 15-20 of them at once) past its 5s budget on a loaded
132
+ machine, or an HTTP check past its 3s timeout. The row painted it red
133
+ and the 8s health cache replayed that to the next page load, until the
134
+ next poll corrected it. Now a check that has not answered is **pending**
135
+ (`ok: null`, `pending: true`, the reason in `error`), never down. Only a
136
+ check that completed and failed is `ok: false`. A timeout is down only
137
+ when it repeats on the next cycle (`timeout (twice in a row)`), so a hung
138
+ service still shows. `<cli> status` agrees: on a timeout it probes once
139
+ more, and is down only when both probes time out. Health results have a
140
+ new `pending` state (`health: "pending"`), which readiness waits on
141
+ rather than skipping.
142
+ - **"Docker state unknown" is not "not running".** When docker does not
143
+ answer, the health row and `<cli> status` report a compose stack as
144
+ unknown (`ok: null`), not down, and so does `/api/stacks`. `<cli> stack
145
+ health` exits 3 when it could not tell (docker did not answer, the stack
146
+ config could not be read), 1 only when a stack is not healthy.
147
+ - **A health stream and a poll at once run each check once.** A stream
148
+ started its own full run next to an in-flight `/api/health` (or another
149
+ stream), doubling the shell checks' processes. It now joins the run in
150
+ flight, replaying what has landed and then following it.
151
+ - The web header's dot for a multi-service repo without an entrypoint
152
+ turned red when none of its checks were up yet, even with none down. It
153
+ is red now only when a check completed and failed.
154
+
155
+ - **The default workspace is seeded only on a genuine fresh setup.** Any
156
+ command that reads workspaces (`status`, `up`, `graph`, `logs`, …) seeded
157
+ `default` from `defaults.workspace.repos` whenever `default` was empty,
158
+ so moving every repo into per-project workspaces and deactivating
159
+ `default` got it refilled with managed entries on the next command, ready
160
+ to clone duplicates of repos you work on locally. Seeding now requires
161
+ that no workspace but `default` exists and that `default` was never
162
+ seeded (no `seeded_repos`). Declining the offer records that too, so it
163
+ is no longer asked again.
164
+ - **No silent seeding without a TTY.** On a fresh setup a command without
165
+ a TTY now prints a one-line hint to run `<cli> workspace seed` and
166
+ changes nothing; on a TTY the offer stays. New: `<cli>
167
+ workspace seed` seeds explicitly (as `<cli> init` in the root does).
168
+ - **Top-up no longer re-adds repos you moved or removed.** Top-up (adding
169
+ repos that joined the blueprint after seeding) never runs once you have
170
+ other workspaces, and skips a repo any workspace holds. Removing a
171
+ blueprint repo from `default` in the web UI records it, so a `default`
172
+ from before `seeded_repos` no longer gets it back on its one-time
173
+ migration.
174
+
175
+ ### Added
176
+
177
+ - **The "What's new" pop-up shows the newest release only** (from
178
+ 0.3.0-dev.16). A "Show all versions (N earlier)" button in its footer
179
+ lists every earlier version in the same pop-up, and "Show latest only"
180
+ collapses it again. It always opens collapsed, at the top: the scroll is
181
+ reset once the pop-up is shown (a reset made while it was hidden did not
182
+ stick, so a reopened pop-up kept its old scroll position). It is an
183
+ accessible modal dialog (`role="dialog"`, `aria-modal`, labelled by its
184
+ title). Opening moves focus to its heading, so a screen reader starts at
185
+ the latest notes. Tab stays inside, and closing returns focus to what had
186
+ it, which is the version pill after the first-visit auto-open. Each
187
+ version is a heading (h4) for heading navigation, and the icon buttons
188
+ have spoken names ("Close", "Copy v0.2.5 as Markdown").
189
+ - **The version pill names the running build** (`v0.2.5-dev.2`), not the
190
+ newest changelog heading, so a prerelease under test says what it is.
191
+ - **Multi-workspace blueprint: `defaults.workspaces`.** `{ <name>: {
192
+ repos, branch?, autostart_off?, focus? } }` in the org config. A fresh
193
+ setup creates each workspace with its repos (managed), activates them
194
+ all and focuses the one with `focus: true` (else the first). Setting
195
+ both `defaults.workspace` and `defaults.workspaces` is an error, as is a
196
+ malformed entry. See docs/distributions.md, "Workspace blueprint".
197
+
198
+ ### Migrations
199
+
200
+ - None required.
201
+
7
202
  ## [0.2.4] - 2026-11-04
8
203
 
9
204
  ### Fixed
@@ -126,6 +126,83 @@ is never mistaken for another repo's service. An alias that names more than
126
126
  one service is an error that lists them (`"worker" is ambiguous in workspace
127
127
  "default": one/worker, two/worker`); name one of them.
128
128
 
129
+ ## Health checks
130
+
131
+ A service declares its health per environment under `health`, in one of
132
+ three types:
133
+
134
+ ```yaml
135
+ services:
136
+ api:
137
+ health:
138
+ local: { url: http://shop-api/healthz } # http (the default type)
139
+ db:
140
+ health:
141
+ local: { type: shell, cmd: pg_isready -h localhost } # exit 0 is up
142
+ cache:
143
+ health:
144
+ local: { type: container, project: shop-main, service: cache }
145
+ ```
146
+
147
+ `label` (optional, any type) is shown in the UI instead of the URL or the
148
+ command. A compose repo with no declared check gets Den's own stack check
149
+ (`<cli> stack health`), answered in-process.
150
+
151
+ **`type: container`** asks whether a container is up, from the one
152
+ `docker ps` listing a health cycle already makes for the compose checks: it
153
+ adds no docker call however many container checks there are, in the web
154
+ health row and in `<cli> status`. It matches a compose container by its
155
+ `com.docker.compose.project` and `com.docker.compose.service` labels
156
+ (`project` + `service`), or any container by name (`container_name`, for one
157
+ compose does not run). One or the other, never both:
158
+
159
+ | What the listing shows | Verdict |
160
+ | ----------------------------------------- | ------------------------------------------------------- |
161
+ | running, healthcheck `healthy` | up |
162
+ | running, healthcheck `unhealthy` | down |
163
+ | running, healthcheck `starting` | pending |
164
+ | running, no healthcheck | up |
165
+ | not running (exited, created, restarting) | down |
166
+ | no matching container | down (`<project>/<service>: no container`) |
167
+ | `docker ps` fails (daemon down) | unknown (`ok: null`, the reason in `error`), never down |
168
+ | `docker ps` past its 5s budget | pending (`ok: null`, `pending: true`) |
169
+ | more than one match | the best one: healthy, then running |
170
+
171
+ When docker gives no listing, a container check says what a compose check
172
+ says, through the same code: docker state unknown is not "not running", so
173
+ a failed `docker ps` is `ok: null` however many cycles it lasts, in the
174
+ health row and in `<cli> status`. Only a listing that runs past its budget
175
+ is `pending`, and like any check with no answer it is down when that
176
+ repeats on the next cycle. A container whose healthcheck is still starting
177
+ stays pending. In
178
+ `/api/health` the row's `check` reads `container <project>/<service>` (or
179
+ `container <name>`).
180
+
181
+ Before, a container check was a shell check that ran docker itself, one
182
+ process and one docker call per check per cycle:
183
+
184
+ ```yaml
185
+ health:
186
+ local:
187
+ type: shell
188
+ cmd: test -n "$(docker ps -q --filter label=com.docker.compose.project=shop-main --filter label=com.docker.compose.service=cache)"
189
+ ```
190
+
191
+ After:
192
+
193
+ ```yaml
194
+ health:
195
+ local: { type: container, project: shop-main, service: cache }
196
+ ```
197
+
198
+ which also reads the container's own healthcheck (the shell form only saw
199
+ that a container existed, running or not).
200
+
201
+ **Validation.** `<cli> validate` rejects a container check with neither
202
+ `project` + `service` nor `container_name`, with only one of `project` and
203
+ `service`, with both forms at once, or with a key of another type (`cmd`,
204
+ `url`), and says which.
205
+
129
206
  ## Placement
130
207
 
131
208
  Each service says where it runs, per environment: `local` (on this machine,
@@ -327,6 +404,54 @@ brand:
327
404
  Runtime names (env vars, files, directories) always come from `den.yml`,
328
405
  never from the brand.
329
406
 
407
+ ## Workspace blueprint
408
+
409
+ The workspaces a new machine starts with. Read from `defaults:` in the org
410
+ config. Every repo is seeded as a managed clone (`mode: managed`) on the
411
+ blueprint's branch; nothing is cloned until a command needs it. Switch a repo
412
+ to a local checkout afterwards with `acme use-local <repo>`.
413
+
414
+ One workspace, `default`:
415
+
416
+ ```yaml
417
+ defaults:
418
+ workspace:
419
+ branch: main # default: main
420
+ repos: [shop-web, shop, infra-stack]
421
+ autostart_off: [shop] # seeded with autostart: false
422
+ entry: shop-web # where the setup script's dependency walk starts
423
+ ```
424
+
425
+ Several named workspaces:
426
+
427
+ ```yaml
428
+ defaults:
429
+ workspaces:
430
+ storefront:
431
+ repos: [shop-web, shop]
432
+ data:
433
+ repos: [warehouse, infra-stack]
434
+ branch: develop
435
+ focus: true
436
+ ```
437
+
438
+ Each entry takes `repos`, `branch` and `autostart_off`, the same fields as
439
+ `defaults.workspace`, plus `focus`. Seeding creates every named workspace
440
+ with its repos, activates them all, and focuses the one with `focus: true`
441
+ (else the first). Set `defaults.workspace` or `defaults.workspaces`, not both:
442
+ both at once is an error, as is more than one `focus: true`.
443
+
444
+ The blueprint applies only to a genuine fresh setup: no workspace except an
445
+ empty `default` that was never seeded. On a TTY the first command that needs
446
+ workspaces offers to seed. Without a TTY it prints a one-line hint and changes
447
+ nothing. `acme workspace seed` (or `acme init` in the distribution root) seeds
448
+ explicitly, and resets the blueprint's workspaces if they already hold repos.
449
+
450
+ Once seeded, a repo that later joins `defaults.workspace.repos` is added to
451
+ `default`, but only while `default` is your only workspace, and never a repo
452
+ you removed from it or that another workspace holds. With
453
+ `defaults.workspaces` nothing is topped up.
454
+
330
455
  ## The bin shim
331
456
 
332
457
  `bin/<cli>` is three lines of sh: it names the distribution root and runs
@@ -401,6 +526,7 @@ it exits:
401
526
 
402
527
  ```bash
403
528
  ./bin/acme service install # install (or refresh) and start it
529
+ ./bin/acme service install --plan # what install would do; writes and loads nothing
404
530
  ./bin/acme service status # launchd's view: state, pid, last exit code
405
531
  ./bin/acme service print # the LaunchAgent plist it would install; installs nothing
406
532
  ./bin/acme service uninstall # stop it and remove the LaunchAgent
@@ -429,6 +555,15 @@ answers empty for a few seconds while it starts). If it has not answered
429
555
  within 30s, it says `installed but not answering yet: check <logs dir>` and
430
556
  exits 1.
431
557
 
558
+ `install --plan` is read-only: it prints the label, the plist path
559
+ (`~/Library/LaunchAgents/<label>.plist`), the log file, the distribution and
560
+ engine roots, whose server answers on `web.port` (this distribution's, free,
561
+ or `port <N> is in use by <product> at <root>; not this distribution's
562
+ server`), and the plist install would write. It writes nothing, never calls
563
+ launchctl, and exits 1 when the port is another distribution's. Any other
564
+ argument to a `service` subcommand is an error, so a mistyped `--plan` never
565
+ installs. `status` also names another distribution's server on the port.
566
+
432
567
  `<cli> service` runs the installer script below, which you can also run
433
568
  yourself, from the distribution root:
434
569
 
@@ -461,6 +596,13 @@ set of guards. It never starts the web server; if it is not running, every
461
596
  tool answers with the command that starts it (`./bin/acme web up`). stdout
462
597
  carries only the protocol; diagnostics go to stderr.
463
598
 
599
+ It reads only from this distribution's server. Before serving a tool it
600
+ checks `/api/server-info` (root and product, or for a server from before
601
+ 0.2.6 its working directory), trusts a positive answer for 30s, and checks
602
+ again after a connection error. When another distribution's server answers
603
+ on the port, every tool says `port <N> is in use by <product> at <root>; not
604
+ this distribution's server` and returns none of its data.
605
+
464
606
  The web server keeps running the Den it started with until it is restarted,
465
607
  so after an upgrade it can be older than the MCP server. When a tool's route
466
608
  answers 404, the MCP server reads the web server's version from
@@ -544,22 +686,22 @@ guard off.
544
686
 
545
687
  Read tools (always registered):
546
688
 
547
- | Tool | Web API | What |
548
- | ------------------- | ------------------------------------------ | ---------------------------------------------------------- |
549
- | `status` | `/api/health` | Service health; `url` (HTTP check) or `check` (shell) |
550
- | `services` | `/api/services` | Services, their mode (managed or dev-managed) and commands |
551
- | `applications` | `/api/applications` | Org > Application > Repo > Services |
552
- | `graph` | `/api/graph` | The derived dependency graph and startup order |
553
- | `workspaces` | `/api/workspaces` | Workspaces, active and focused |
554
- | `environments` | `/api/environments` | The enabled environment names (never their values) |
555
- | `logs_search` | `/api/logs/search` | Search services' full log history |
556
- | `logs_tail` | `/api/service/taillog` | The last lines of a service's log (bounded, no follow) |
557
- | `tests_available` | `/api/modules`, `/api/test-sources` | A pillar's modules and the repos they come from |
558
- | `security_latest` | `/api/security/latest` | The newest security scan per mode |
559
- | `release_status` | `/api/release/runs`, `/api/release/status` | Recent CD runs, and an env's deployed-version table |
560
- | `manifest_validate` | `/api/manifest/validate` | Validate a manifest by path or content |
561
- | `stacks` | `/api/stacks` | What `stack ls/ps/health` show: project, routes, health |
562
- | `edge_status` | `/api/edge` | The edge: up or down, its routes and cert names |
689
+ | Tool | Web API | What |
690
+ | ------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
691
+ | `status` | `/api/health` | Service health; `url` (HTTP check) or `check` (shell, or `container <project>/<service>`); `ok` true up, false down, null not known (`pending: true` when a check has not answered) |
692
+ | `services` | `/api/services` | Services, their mode (managed or dev-managed) and commands |
693
+ | `applications` | `/api/applications` | Org > Application > Repo > Services |
694
+ | `graph` | `/api/graph` | The derived dependency graph and startup order |
695
+ | `workspaces` | `/api/workspaces` | Workspaces, active and focused |
696
+ | `environments` | `/api/environments` | The enabled environment names (never their values) |
697
+ | `logs_search` | `/api/logs/search` | Search services' full log history |
698
+ | `logs_tail` | `/api/service/taillog` | The last lines of a service's log (bounded, no follow) |
699
+ | `tests_available` | `/api/modules`, `/api/test-sources` | A pillar's modules and the repos they come from |
700
+ | `security_latest` | `/api/security/latest` | The newest security scan per mode |
701
+ | `release_status` | `/api/release/runs`, `/api/release/status` | Recent CD runs, and an env's deployed-version table |
702
+ | `manifest_validate` | `/api/manifest/validate` | Validate a manifest by path or content |
703
+ | `stacks` | `/api/stacks` | What `stack ls/ps/health` show: project, routes, health |
704
+ | `edge_status` | `/api/edge` | The edge: up or down, its routes and cert names |
563
705
 
564
706
  Action tools (only those named in `mcp.actions`): `service_up`,
565
707
  `service_down`, `service_restart` (through `/api/service/exec`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monster-orange/den",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -107,7 +107,7 @@ export function autoSecurityTargets(
107
107
  if (!runsLocally) continue;
108
108
  const hosts: SecurityServiceConfig["hosts"] = {};
109
109
  for (const [env, h] of Object.entries(s.config?.health ?? {})) {
110
- if (!h || h.type === "shell" || !/^https?:\/\//.test(h.url)) continue;
110
+ if (!h || !("url" in h) || !/^https?:\/\//.test(h.url)) continue;
111
111
  hosts[env as keyof typeof hosts] = [new URL(h.url).origin + "/"];
112
112
  }
113
113
  if (Object.keys(hosts).length === 0) continue;
@@ -11,6 +11,7 @@ import { discoverRepos, resolveRepoPath, resolveService } from "./discover.js";
11
11
  import { stageSmokeTree } from "./staging.js";
12
12
  import type { Environment } from "../../shared/types/index.js";
13
13
  import { getDistRoot } from "../../shared/paths.js";
14
+ import { plural } from "../../shared/plural.js";
14
15
 
15
16
  // Den-root anchored, NOT process.cwd() — bulk writer of .smoke-run (see setup.ts).
16
17
  const PROJECT_ROOT = getDistRoot();
@@ -116,7 +117,7 @@ async function main() {
116
117
  );
117
118
 
118
119
  console.log(
119
- `\n Total: ${total} smoke test file(s) staged across ${repos.length} repo(s)\n`,
120
+ `\n Total: ${plural(total, "smoke test file")} staged across ${plural(repos.length, "repo")}\n`,
120
121
  );
121
122
  }
122
123
 
@@ -17,6 +17,7 @@ import type { Environment } from "../shared/types/index.js";
17
17
  import { getDistRoot } from "../shared/paths.js";
18
18
 
19
19
  import { product } from "../shared/product.js";
20
+ import { plural } from "../shared/plural.js";
20
21
  // Den-root anchored: WRITES config/local/aws-profiles.json, and the reader
21
22
  // (shared/environment/aws-profiles.ts) already anchors at getDistRoot().
22
23
  const ROOT = getDistRoot();
@@ -54,7 +55,7 @@ async function main() {
54
55
  );
55
56
  } else {
56
57
  console.log(
57
- ` Found ${profiles.length} profile(s): ${profiles.join(", ")}\n`,
58
+ ` Found ${plural(profiles.length, "profile")}: ${profiles.join(", ")}\n`,
58
59
  );
59
60
  }
60
61
 
@@ -29,6 +29,7 @@ import {
29
29
  } from "../shared/environment-manager/default-workspace.js";
30
30
 
31
31
  import { product } from "../shared/product.js";
32
+ import { plural } from "../shared/plural.js";
32
33
  // Anchored to the den root, NOT process.cwd() — this script WRITES
33
34
  // config/local/* and a cwd-derived root once put den files in another repo.
34
35
  const ROOT = getDistRoot();
@@ -179,7 +180,7 @@ async function legacyScanAndSelect(): Promise<void> {
179
180
  fs.readFileSync(REGISTERED_FILE, "utf-8"),
180
181
  ) as string[];
181
182
  console.log(
182
- ` Current: ${existing.length} repo(s) registered for testing\n`,
183
+ ` Current: ${plural(existing.length, "repo")} registered for testing\n`,
183
184
  );
184
185
  } catch {
185
186
  /* corrupt file — start fresh */
@@ -223,7 +224,7 @@ async function legacyScanAndSelect(): Promise<void> {
223
224
  fs.writeFileSync(REGISTERED_FILE, JSON.stringify(selected, null, 2) + "\n");
224
225
 
225
226
  console.log(
226
- `\n Registered ${selected.length} repo(s) → config/local/registered-repos.json`,
227
+ `\n Registered ${plural(selected.length, "repo")} → config/local/registered-repos.json`,
227
228
  );
228
229
  console.log(" Run `make register-repos` again at any time to update.\n");
229
230
  }
@@ -282,7 +283,7 @@ async function main(): Promise<void> {
282
283
  console.log(
283
284
  " Local Dev is enabled but no workspace exists yet. Your first" +
284
285
  ` \`${product().cli}\` command\n seeds the default workspace with every standard` +
285
- ` system repo, all managed\n out of the box (${plan.repos.length} repos):\n`,
286
+ ` system repo, all managed\n out of the box (${plural(plan.repos.length, "repo")}):\n`,
286
287
  );
287
288
  console.log(` ${plan.repos.join(", ")}\n`);
288
289
  console.log(
@@ -48,6 +48,13 @@ export const HOSTED_HEALTH_CHECK_TIMEOUT_MS = 10_000;
48
48
  /** Timeout for spawning short-lived CLI subprocesses (open, sudo, git). */
49
49
  export const CLI_SPAWN_TIMEOUT_MS = 5_000;
50
50
 
51
+ /**
52
+ * Exit code of `<cli> stack health` when it could not tell (docker did not
53
+ * answer, the stack config could not be read): the check is unknown, not
54
+ * down. Any other non-zero exit means the containers are not healthy.
55
+ */
56
+ export const STACK_HEALTH_UNKNOWN_EXIT = 3;
57
+
51
58
  /** Timeout for `gh api` calls (branch listing, repo search). */
52
59
  export const GH_API_TIMEOUT_MS = 15_000;
53
60