@monster-orange/den 0.2.5 → 0.2.7
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 +215 -0
- package/bin/den +9 -0
- package/docs/distributions.md +202 -22
- package/package.json +1 -1
- package/service/install.sh +41 -2
- package/src/pillars/security/discover.ts +1 -1
- package/src/pillars/smoke/setup-global.ts +2 -1
- package/src/scripts/configure-aws-profiles.ts +2 -1
- package/src/scripts/register-repos.ts +4 -3
- package/src/shared/den-config.ts +38 -1
- package/src/shared/environment-manager/catalog.ts +9 -2
- package/src/shared/environment-manager/default-workspace.ts +92 -14
- package/src/shared/environment-manager/health.ts +96 -17
- package/src/shared/environment-manager/service-runner.ts +51 -5
- package/src/shared/environment-manager/workspace-manager.ts +2 -1
- package/src/shared/environment-manager/workspace-org.ts +264 -0
- package/src/shared/manifest/placement.ts +1 -1
- package/src/shared/manifest/schema.ts +47 -0
- package/src/shared/manifest/service-links.ts +1 -1
- package/src/shared/manifest/validate-manifest.ts +1 -1
- package/src/shared/plural.ts +13 -0
- package/src/shared/service-url.ts +2 -5
- package/src/shared/stacks/compose-health.ts +106 -14
- package/src/shared/stacks/runtime.ts +2 -0
- package/src/shared/types/index.ts +20 -1
- package/src/ui/cli/clone-plan.ts +243 -0
- package/src/ui/cli/create.ts +2 -1
- package/src/ui/cli/flag-parser.ts +1 -1
- package/src/ui/cli/index.ts +274 -61
- package/src/ui/cli/mcp-connect.ts +36 -5
- package/src/ui/cli/org-command.ts +162 -0
- package/src/ui/cli/results-viewer.ts +5 -5
- package/src/ui/cli/server-identity.ts +233 -0
- package/src/ui/cli/service-command.ts +136 -12
- package/src/ui/cli/spinner.ts +48 -0
- package/src/ui/cli/table.ts +20 -0
- package/src/ui/cli/validate-all.ts +171 -0
- package/src/ui/cli/web-command.ts +76 -25
- package/src/ui/mcp/api.ts +94 -1
- package/src/ui/mcp/server.ts +30 -2
- package/src/ui/mcp/tools.ts +6 -2
- package/src/ui/server/daemon-reaper.ts +22 -4
- package/src/ui/server/health-row.ts +6 -3
- package/src/ui/server/index.ts +63 -18
- package/src/ui/server/release-service.ts +16 -3
- package/src/ui/server/workspace-routes.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,221 @@ 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.7] - 2026-11-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **A workspace belongs to an org** (0.2.7-dev.2, ORG-1a). Its org is
|
|
12
|
+
`org:` on the workspace in workspaces.yml, else `org:` on the blueprint
|
|
13
|
+
workspace (`defaults.workspaces.<n>.org`, the one new blueprint key), else
|
|
14
|
+
the one catalog owner of its repos (case-insensitive; `gh: ""` ignored),
|
|
15
|
+
else `github_org`, the default org. It is computed on read, never written
|
|
16
|
+
to workspaces.yml. A config with no `org:` resolves to `github_org` as
|
|
17
|
+
before, unless all of a workspace's repos have one other owner in the
|
|
18
|
+
catalog.
|
|
19
|
+
- **`<cli> org list`** (0.2.7-dev.2, ORG-1a): each org's label, its
|
|
20
|
+
workspaces on this machine, which is the default, and the org warnings.
|
|
21
|
+
`<cli> workspace list` gains an Org column; `--org <id>` filters it.
|
|
22
|
+
- **Org warnings, never errors** (0.2.7-dev.2, ORG-1a, C1): a workspace
|
|
23
|
+
whose repos span several owners, an `org:` naming an owner Den does not
|
|
24
|
+
know, a repo in another owner than its workspace's org, a workspaces.yml
|
|
25
|
+
`org:` that overrides the blueprint's. Shown by `org list` and
|
|
26
|
+
`workspace list`.
|
|
27
|
+
- **`<cli> setup --plan`** (0.2.7-dev.2, ORG-1a): each repo's org and clone
|
|
28
|
+
URL, and whether it is cloned, then stop. On a fresh machine it plans the
|
|
29
|
+
blueprint's workspaces; once seeded, the focused workspace. `<cli> up
|
|
30
|
+
--plan` prints the same lines for its repos. Nothing is cloned, seeded or
|
|
31
|
+
written.
|
|
32
|
+
- **`GET /api/orgs` entries gain `default` and `workspaces`**, and
|
|
33
|
+
`/api/<cli>/config` gains `default_org` (0.2.7-dev.2, ORG-1a). Every
|
|
34
|
+
existing field, entry and order is unchanged; `?repos=1` still maps each
|
|
35
|
+
repo to its owner string.
|
|
36
|
+
- **`<cli> register --org <id>`** (0.2.7-dev.2, ORG-1a) lists another org's
|
|
37
|
+
repos for a managed clone.
|
|
38
|
+
|
|
39
|
+
- **Bare `<cli> validate` checks every manifest** (0.2.7-dev.1, from a
|
|
40
|
+
consumer). In the distribution root, with no argument: each workspace repo
|
|
41
|
+
once per checkout (own or held, plus any override, then the merge), the
|
|
42
|
+
held manifest when there is no checkout, and held manifests no workspace
|
|
43
|
+
uses. One line per repo, details on failure, exit 1 if any fails, nothing
|
|
44
|
+
cloned. `--held` checks `manifests/` alone, independent of the machine. A
|
|
45
|
+
path argument works as before.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **A repo is released from its own org** (0.2.7-dev.2, REL-1). With no
|
|
50
|
+
`release.org`, a repo with no `gh:` in `release.repos` released from
|
|
51
|
+
`github_org`, so a client org's repo targeted the wrong owner. Its owner
|
|
52
|
+
is now an explicit `release.org` (unchanged: it still wins for every
|
|
53
|
+
repo), else the repo's non-empty catalog owner, else `github_org`.
|
|
54
|
+
- **`register` lists the focused workspace's org** (0.2.7-dev.2, ORG-1a),
|
|
55
|
+
not `github_org` alone, and no longer falls back to the owner of Den's own
|
|
56
|
+
git remote (SEC-0): with no org it says how to set one.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **A second root's server leaves the first root's services alone**
|
|
61
|
+
(0.2.7-dev.3, dev.4). Every root of a product shares one daemons file.
|
|
62
|
+
A server's two startup sweeps (the stale-daemon prune and the health
|
|
63
|
+
reaper) dropped any record whose address did not resolve in its own
|
|
64
|
+
workspaces, so starting a scratch root's server silently forgot another
|
|
65
|
+
root's running dev server; an interactive Ctrl-C stopped every service in
|
|
66
|
+
the file, and deactivating a workspace released any record with the same
|
|
67
|
+
workspace name. Records are now stamped with the root that started them
|
|
68
|
+
and a server lists, stops, releases and sweeps only its own (other roots'
|
|
69
|
+
records stay in the file untouched). An older unstamped record is never
|
|
70
|
+
dropped just for not resolving; it still goes when its containers are
|
|
71
|
+
verifiably gone or two health checks fail. The reaper now logs what it
|
|
72
|
+
removes.
|
|
73
|
+
- **A service the edge has no route for is no longer healthy** (0.2.7-dev.2).
|
|
74
|
+
A health URL served through the edge answered the edge's own
|
|
75
|
+
`404 page not found` when the service had no route, and Den counted any
|
|
76
|
+
404 as up. That exact 404 is now down with "no route (edge 404)", over
|
|
77
|
+
HTTP and HTTPS; a 404 from the service itself is still up.
|
|
78
|
+
- **A client workspace no longer clones from the default org** (0.2.7-dev.2,
|
|
79
|
+
ORG-1a). Seeding, `up` and the web sync gave a repo with no clone URL
|
|
80
|
+
`<github_org>/<repo>`; it is now `<org>/<repo>` with its workspace's org.
|
|
81
|
+
- **`<cli> setup` clones a catalog entry with `gh: ""`** from its
|
|
82
|
+
workspace's org (0.2.7-dev.2), as `up` does, instead of failing on an
|
|
83
|
+
empty slug.
|
|
84
|
+
- **A seed without a terminal no longer resets a workspace that has
|
|
85
|
+
entries** (SEED-0). `<cli> init` and `<cli> workspace seed` from a script
|
|
86
|
+
or `make` replaced every entry of the default (or blueprint) workspaces with
|
|
87
|
+
managed clones, your own dev-managed checkouts included, without asking.
|
|
88
|
+
Without a TTY they now refuse (exit 1, nothing written) and name
|
|
89
|
+
`--reset`; on a TTY they still ask, default No. `init --reset` and
|
|
90
|
+
`workspace seed --reset` reset as before.
|
|
91
|
+
- **A failed seed leaves the workspaces as they were** (0.2.7-dev.1, from a
|
|
92
|
+
consumer). Seeding wrote workspaces.yml, then filled the catalog, which
|
|
93
|
+
fails with no `github_org`: the workspace was reset and nothing could be
|
|
94
|
+
cloned. The catalog comes first now, and the org is needed only for a repo
|
|
95
|
+
with no clone URL in the catalog.
|
|
96
|
+
- **A seed refusal names the command that was run** (`init --reset` or
|
|
97
|
+
`workspace seed --reset`).
|
|
98
|
+
- **A `DEN_ROOT` with no den.yml is a one-line error** (exit 2), not a stack
|
|
99
|
+
trace from deep in module loading.
|
|
100
|
+
- **Piped output carries no ANSI from tables** (`status`, `services`, test
|
|
101
|
+
results). cli-table3 coloured borders and head cells itself, ignoring
|
|
102
|
+
NO_COLOR and a pipe.
|
|
103
|
+
- **`<cli> mcp --call` validates its arguments** against the tool's schema,
|
|
104
|
+
strictly: an unknown or misspelt key, or a bad value, exits 2 with the
|
|
105
|
+
problem and the keys the tool takes, and calls nothing.
|
|
106
|
+
- **The web UI service survives a removed nvm version.** Install prefers a
|
|
107
|
+
stable node of the same major version over a version manager's (or uses
|
|
108
|
+
`DEN_SERVICE_NODE`), and warns when it must keep one. `service status`
|
|
109
|
+
exits 1, naming it, when the installed node no longer exists.
|
|
110
|
+
|
|
111
|
+
- **`<cli> init` in the distribution root seeds even through a symlinked
|
|
112
|
+
path** (`/var` → `/private/var`, a symlinked home). It compared paths
|
|
113
|
+
without resolving links and ran the manifest scaffolder instead.
|
|
114
|
+
|
|
115
|
+
### Migrations
|
|
116
|
+
|
|
117
|
+
- A script that relied on a non-interactive seed resetting a changed
|
|
118
|
+
workspace must pass `--reset`.
|
|
119
|
+
- Optional (ORG-1a): to put a blueprint workspace in an org other than the
|
|
120
|
+
one its catalog owners give, add `org: <id>` to it under
|
|
121
|
+
`defaults.workspaces`. Pin Den 0.2.7 or later first: an older Den rejects
|
|
122
|
+
the key. Nothing else needs changing.
|
|
123
|
+
|
|
124
|
+
## [0.2.6] - 2026-11-04
|
|
125
|
+
|
|
126
|
+
### Fixed
|
|
127
|
+
|
|
128
|
+
- **`<cli> web restart` and `web down` act on our server from an older
|
|
129
|
+
Den.** A server from before 0.2.6 (no `root` in `/api/server-info`) run
|
|
130
|
+
from our root is ours, and 0.2.6-dev.0 classified it so, but stopping it
|
|
131
|
+
then looked for our pids by command line only: the entry point of this
|
|
132
|
+
engine, or one under the root. An older Den installed elsewhere, or a
|
|
133
|
+
hand-started server, matched neither, so `web restart` printed "OK ... web
|
|
134
|
+
is already running" and restarted nothing, and `web down` printed "OK ...
|
|
135
|
+
web is not running" while the server stayed up and bound. A server the
|
|
136
|
+
identity check finds ours is now stopped by the pid it reports, when that
|
|
137
|
+
pid listens on the port: `restart` replaces it and `down` frees the port,
|
|
138
|
+
with or without a `web.json` for it. web.json's root is compared after
|
|
139
|
+
resolving symlinks (`/var` and `/private/var` are one root).
|
|
140
|
+
- **The MCP server serves nothing from another distribution's server.**
|
|
141
|
+
It read from whatever answered on its port. Before a tool reads, it now
|
|
142
|
+
checks `/api/server-info` as `web up` does; on another distribution's
|
|
143
|
+
server every tool answers `port <N> is in use by <product> at <root>; not
|
|
144
|
+
this distribution's server` and returns none of its data. A positive
|
|
145
|
+
check is trusted for 30s and made again after a connection error, so a
|
|
146
|
+
server that took over the port is noticed. A server that does not say
|
|
147
|
+
whose it is within 5s is not read from either.
|
|
148
|
+
- **A mistyped `service` flag no longer installs.** `<cli> service install
|
|
149
|
+
--anything` ignored the flag and installed; a `service` subcommand now
|
|
150
|
+
refuses an argument it does not take, before running anything.
|
|
151
|
+
- **`<cli> web up` no longer trusts whatever answers on its port.** With
|
|
152
|
+
`web.port` set to a port where another distribution's Den server ran
|
|
153
|
+
(another root, an older Den), `web up` printed "OK ... web is already
|
|
154
|
+
running" and stopped, and `web down` or `web restart` could SIGTERM that
|
|
155
|
+
server: with a linked engine its command line looks like ours.
|
|
156
|
+
`/api/server-info` now also returns `root` (the distribution root the
|
|
157
|
+
server serves) and `product`. `web up`, `web down` and `web restart`
|
|
158
|
+
compare both with their own, and on another distribution's server exit 1
|
|
159
|
+
with `port <N> is in use by <product> at <root>` and a hint to pick
|
|
160
|
+
another port (`--port`, or `web.port`). A server from before 0.2.6 has
|
|
161
|
+
no `root`: it is ours when it runs from our root (its working
|
|
162
|
+
directory), otherwise `port <N> is in use by another server (Den
|
|
163
|
+
<version>)`; anything else is "a non-Den server". `web down` also no
|
|
164
|
+
longer signals a process running from another root, even when the
|
|
165
|
+
server does not answer. `<cli> mcp connect` refuses to point an agent at
|
|
166
|
+
another distribution's server (and `mcp clients` names it), and `<cli>
|
|
167
|
+
service install` no longer reports "running" when another
|
|
168
|
+
distribution's server answers on the port.
|
|
169
|
+
- **A repeat `<cli> workspace seed` says "already seeded".** Seeding a
|
|
170
|
+
workspace that already holds exactly the blueprint printed "Default
|
|
171
|
+
workspace ready" as if it had seeded again; it now prints `✓ Default
|
|
172
|
+
workspace already seeded — N repos (managed): nothing changed.` and
|
|
173
|
+
writes nothing (multi blueprint: `✓ N workspaces already seeded (a, b)`).
|
|
174
|
+
A seed over a workspace the user changed says it was "reset to the
|
|
175
|
+
blueprint".
|
|
176
|
+
- **Counts read "1 repo", not "1 repos".** CLI output that counts repos,
|
|
177
|
+
workspaces, systems, files, profiles and tools uses one plural helper
|
|
178
|
+
(`src/shared/plural.ts`), also in place of "repo(s)".
|
|
179
|
+
- **Spinners no longer loop forever on a 0-column terminal.** On a pty that
|
|
180
|
+
reports 0 columns (one nobody sized, as agent shells give), ora computed
|
|
181
|
+
an infinite number of lines to clear and never finished. A spinner now
|
|
182
|
+
animates only on a TTY with a known, non-zero width; otherwise it prints
|
|
183
|
+
plain lines (its text on start, then the result).
|
|
184
|
+
|
|
185
|
+
### Added
|
|
186
|
+
|
|
187
|
+
- **Health type `container`.** `health: { type: container, project: <compose
|
|
188
|
+
project>, service: <compose service> }` checks a compose container by its
|
|
189
|
+
`com.docker.compose.project` / `com.docker.compose.service` labels, or
|
|
190
|
+
`{ type: container, container_name: <name> }` any container by name. It
|
|
191
|
+
is answered from the one `docker ps` per health cycle the compose checks
|
|
192
|
+
already share (its format gains `{{.Names}}`, and it no longer filters to
|
|
193
|
+
compose containers), so N container checks add no docker call, in the
|
|
194
|
+
web health row and in `<cli> status` (22 checks, 17 compose and 5
|
|
195
|
+
container: 1 docker call per cycle). A running container with a
|
|
196
|
+
healthcheck is its health (healthy up, unhealthy down, starting
|
|
197
|
+
pending), one without is up, and no matching container is down. When
|
|
198
|
+
docker gives no listing it says what a compose check says, through the
|
|
199
|
+
same code: a failed `docker ps` is unknown (`ok: null`, the reason in
|
|
200
|
+
`error`) on every cycle, never down; a listing past its budget is
|
|
201
|
+
pending (`pending: true`), down only when that repeats. Of several matches the best counts:
|
|
202
|
+
healthy, then running. `/api/health` rows show it as `check: "container
|
|
203
|
+
<project>/<service>"`. `<cli> validate` requires `project` + `service` or
|
|
204
|
+
`container_name` (not both, not one of the pair alone, no other type's
|
|
205
|
+
keys) and says which. See docs/distributions.md, "Health checks".
|
|
206
|
+
- **`<cli> service install --plan`.** Read-only: prints the label, the
|
|
207
|
+
plist path, the log file, the roots, whose server answers on `web.port`
|
|
208
|
+
(ours, free, or `port <N> is in use by <product> at <root>`) and the
|
|
209
|
+
plist install would write; writes nothing and never calls launchctl.
|
|
210
|
+
Exits 1 when the port is another distribution's. `<cli> service status`
|
|
211
|
+
also names another distribution's server on the port.
|
|
212
|
+
|
|
213
|
+
### Migrations
|
|
214
|
+
|
|
215
|
+
- None required. Optional: a `type: shell` check that runs `docker ps` to
|
|
216
|
+
find a container (`test -n "$(docker ps -q --filter
|
|
217
|
+
label=com.docker.compose.project=X --filter
|
|
218
|
+
label=com.docker.compose.service=Y)"`) can become `{ type: container,
|
|
219
|
+
project: X, service: Y }`: no process or docker call of its own, and it
|
|
220
|
+
reads the container's healthcheck.
|
|
221
|
+
|
|
7
222
|
## [0.2.5] - 2026-11-04
|
|
8
223
|
|
|
9
224
|
### Fixed
|
package/bin/den
CHANGED
|
@@ -49,6 +49,15 @@ if [ "${1:-}" = "mcp" ] && ! case "${2:-}" in connect|disconnect|clients|config)
|
|
|
49
49
|
built="dist/src/ui/cli/mcp-main.js"
|
|
50
50
|
fi
|
|
51
51
|
|
|
52
|
+
# A DEN_ROOT that names no distribution is a usage error, not a crash: say
|
|
53
|
+
# so here, before any module resolves paths from it at import time.
|
|
54
|
+
# (`create` needs no distribution.)
|
|
55
|
+
if [ -n "${DEN_ROOT:-}" ] && [ "$entry" != "src/ui/cli/create-main.ts" ] && [ ! -f "$DEN_ROOT/den.yml" ]; then
|
|
56
|
+
echo "den: DEN_ROOT is set to \"$DEN_ROOT\" but that directory has no den.yml." >&2
|
|
57
|
+
echo " Point it at your distribution's checkout, or unset it." >&2
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
|
|
52
61
|
# Escape hatch: force the compiled build (e.g. to test the prod artifact).
|
|
53
62
|
if [ "${DEN_USE_DIST:-}" = "1" ] && [ -f "$DEN_ENGINE_ROOT/$built" ]; then
|
|
54
63
|
exec node "$DEN_ENGINE_ROOT/$built" "$@"
|
package/docs/distributions.md
CHANGED
|
@@ -104,6 +104,23 @@ naming the file each error is in, then checks the merged result:
|
|
|
104
104
|
ERROR services.api.commands.local.down unknown key "down": not in the base manifest
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
Run with no argument in the distribution root, `<cli> validate` checks every
|
|
108
|
+
manifest the distribution uses, and never clones:
|
|
109
|
+
|
|
110
|
+
- each repo of every workspace, once per checkout path, with the
|
|
111
|
+
workspaces that use it and where its base comes from (`own`, `held`,
|
|
112
|
+
`+ override`);
|
|
113
|
+
- a repo with no checkout on disk falls back to its held manifest, marked
|
|
114
|
+
`held only, no checkout`. With no held manifest either it is a warning, not
|
|
115
|
+
a failure;
|
|
116
|
+
- each held manifest in `manifests/` that no workspace uses.
|
|
117
|
+
|
|
118
|
+
It prints one line per repo, details only for a layer with errors or
|
|
119
|
+
warnings, and exits 1 if any fails. `<cli> validate --held` checks only
|
|
120
|
+
`manifests/`, as if nothing were checked out, so the result does not depend on
|
|
121
|
+
the machine (use it in a CI or upgrade gate). An override whose base is the
|
|
122
|
+
repo's own manifest is noted there, not checked.
|
|
123
|
+
|
|
107
124
|
The web UI's Services cards show the layers each manifest was built from
|
|
108
125
|
(`manifest: repo + override`), and `/api/services` returns them per service as
|
|
109
126
|
`manifestLayers`, e.g. `["held", "override"]`. `DEN_MANIFESTS_DIR` points the
|
|
@@ -126,6 +143,83 @@ is never mistaken for another repo's service. An alias that names more than
|
|
|
126
143
|
one service is an error that lists them (`"worker" is ambiguous in workspace
|
|
127
144
|
"default": one/worker, two/worker`); name one of them.
|
|
128
145
|
|
|
146
|
+
## Health checks
|
|
147
|
+
|
|
148
|
+
A service declares its health per environment under `health`, in one of
|
|
149
|
+
three types:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
services:
|
|
153
|
+
api:
|
|
154
|
+
health:
|
|
155
|
+
local: { url: http://shop-api/healthz } # http (the default type)
|
|
156
|
+
db:
|
|
157
|
+
health:
|
|
158
|
+
local: { type: shell, cmd: pg_isready -h localhost } # exit 0 is up
|
|
159
|
+
cache:
|
|
160
|
+
health:
|
|
161
|
+
local: { type: container, project: shop-main, service: cache }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`label` (optional, any type) is shown in the UI instead of the URL or the
|
|
165
|
+
command. A compose repo with no declared check gets Den's own stack check
|
|
166
|
+
(`<cli> stack health`), answered in-process.
|
|
167
|
+
|
|
168
|
+
**`type: container`** asks whether a container is up, from the one
|
|
169
|
+
`docker ps` listing a health cycle already makes for the compose checks: it
|
|
170
|
+
adds no docker call however many container checks there are, in the web
|
|
171
|
+
health row and in `<cli> status`. It matches a compose container by its
|
|
172
|
+
`com.docker.compose.project` and `com.docker.compose.service` labels
|
|
173
|
+
(`project` + `service`), or any container by name (`container_name`, for one
|
|
174
|
+
compose does not run). One or the other, never both:
|
|
175
|
+
|
|
176
|
+
| What the listing shows | Verdict |
|
|
177
|
+
| ----------------------------------------- | ------------------------------------------------------- |
|
|
178
|
+
| running, healthcheck `healthy` | up |
|
|
179
|
+
| running, healthcheck `unhealthy` | down |
|
|
180
|
+
| running, healthcheck `starting` | pending |
|
|
181
|
+
| running, no healthcheck | up |
|
|
182
|
+
| not running (exited, created, restarting) | down |
|
|
183
|
+
| no matching container | down (`<project>/<service>: no container`) |
|
|
184
|
+
| `docker ps` fails (daemon down) | unknown (`ok: null`, the reason in `error`), never down |
|
|
185
|
+
| `docker ps` past its 5s budget | pending (`ok: null`, `pending: true`) |
|
|
186
|
+
| more than one match | the best one: healthy, then running |
|
|
187
|
+
|
|
188
|
+
When docker gives no listing, a container check says what a compose check
|
|
189
|
+
says, through the same code: docker state unknown is not "not running", so
|
|
190
|
+
a failed `docker ps` is `ok: null` however many cycles it lasts, in the
|
|
191
|
+
health row and in `<cli> status`. Only a listing that runs past its budget
|
|
192
|
+
is `pending`, and like any check with no answer it is down when that
|
|
193
|
+
repeats on the next cycle. A container whose healthcheck is still starting
|
|
194
|
+
stays pending. In
|
|
195
|
+
`/api/health` the row's `check` reads `container <project>/<service>` (or
|
|
196
|
+
`container <name>`).
|
|
197
|
+
|
|
198
|
+
Before, a container check was a shell check that ran docker itself, one
|
|
199
|
+
process and one docker call per check per cycle:
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
health:
|
|
203
|
+
local:
|
|
204
|
+
type: shell
|
|
205
|
+
cmd: test -n "$(docker ps -q --filter label=com.docker.compose.project=shop-main --filter label=com.docker.compose.service=cache)"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
After:
|
|
209
|
+
|
|
210
|
+
```yaml
|
|
211
|
+
health:
|
|
212
|
+
local: { type: container, project: shop-main, service: cache }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
which also reads the container's own healthcheck (the shell form only saw
|
|
216
|
+
that a container existed, running or not).
|
|
217
|
+
|
|
218
|
+
**Validation.** `<cli> validate` rejects a container check with neither
|
|
219
|
+
`project` + `service` nor `container_name`, with only one of `project` and
|
|
220
|
+
`service`, with both forms at once, or with a key of another type (`cmd`,
|
|
221
|
+
`url`), and says which.
|
|
222
|
+
|
|
129
223
|
## Placement
|
|
130
224
|
|
|
131
225
|
Each service says where it runs, per environment: `local` (on this machine,
|
|
@@ -358,8 +452,8 @@ defaults:
|
|
|
358
452
|
focus: true
|
|
359
453
|
```
|
|
360
454
|
|
|
361
|
-
Each entry takes `repos`, `branch` and `
|
|
362
|
-
`defaults.workspace`, plus `focus`. Seeding creates every named workspace
|
|
455
|
+
Each entry takes `repos`, `branch`, `autostart_off` and `org`, the same
|
|
456
|
+
fields as `defaults.workspace`, plus `focus`. Seeding creates every named workspace
|
|
363
457
|
with its repos, activates them all, and focuses the one with `focus: true`
|
|
364
458
|
(else the first). Set `defaults.workspace` or `defaults.workspaces`, not both:
|
|
365
459
|
both at once is an error, as is more than one `focus: true`.
|
|
@@ -375,6 +469,68 @@ Once seeded, a repo that later joins `defaults.workspace.repos` is added to
|
|
|
375
469
|
you removed from it or that another workspace holds. With
|
|
376
470
|
`defaults.workspaces` nothing is topped up.
|
|
377
471
|
|
|
472
|
+
To see what a setup would clone before it clones anything, run
|
|
473
|
+
`acme setup --plan`: each repo with its org and clone URL, and whether it
|
|
474
|
+
is cloned yet. On a fresh machine it plans the blueprint (every workspace
|
|
475
|
+
`acme workspace seed` would create); once seeded, the focused workspace.
|
|
476
|
+
`acme up --plan` lists the same lines for the repos it would start. Both
|
|
477
|
+
are read-only: nothing is cloned, seeded or written.
|
|
478
|
+
|
|
479
|
+
## Orgs
|
|
480
|
+
|
|
481
|
+
A distribution can hold work for more than one org: each workspace belongs
|
|
482
|
+
to one. An org's id is a key of `orgs:` or a GitHub owner, and today an org
|
|
483
|
+
is the GitHub owner of the same name. `github_org` is the default org. A
|
|
484
|
+
distribution that never mentions orgs has one org, the default, and every
|
|
485
|
+
workspace is in it.
|
|
486
|
+
|
|
487
|
+
```yaml
|
|
488
|
+
github_org: acme
|
|
489
|
+
orgs:
|
|
490
|
+
acme: { label: Acme }
|
|
491
|
+
globex: { label: Globex, color: "#0af" }
|
|
492
|
+
defaults:
|
|
493
|
+
workspaces:
|
|
494
|
+
storefront:
|
|
495
|
+
repos: [shop-web, shop]
|
|
496
|
+
client:
|
|
497
|
+
org: globex # optional: the workspace's org
|
|
498
|
+
repos: [globex-web, globex-api]
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
A workspace's org is the first of:
|
|
502
|
+
|
|
503
|
+
1. `org:` on the workspace in `config/local/workspaces.yml` (this machine)
|
|
504
|
+
2. `org:` on the blueprint workspace of the same name (committed)
|
|
505
|
+
3. the one owner of its repos in the catalog (`gh: <owner>/<repo>`; owners
|
|
506
|
+
compare case-insensitively, and a `gh: ""` counts as unknown)
|
|
507
|
+
4. `github_org`
|
|
508
|
+
|
|
509
|
+
It is worked out each time it is read and never written back. A repo with
|
|
510
|
+
no clone URL is cloned from `<org>/<repo>` with its workspace's org: seeding
|
|
511
|
+
the blueprint above gives `globex-web` `gh: globex/globex-web`, not
|
|
512
|
+
`acme/...`. `acme register` picks a managed clone from the focused
|
|
513
|
+
workspace's org, or from `--org <id>`.
|
|
514
|
+
|
|
515
|
+
`acme org list` shows each org's label, its workspaces and which is the
|
|
516
|
+
default. `acme workspace list` has an Org column, and `--org <id>` lists one
|
|
517
|
+
org's workspaces. A workspace whose repos belong to several owners, an
|
|
518
|
+
`org:` naming an owner Den does not know, or a repo in another owner than
|
|
519
|
+
its workspace's org is a warning in both lists, never an error.
|
|
520
|
+
`GET /api/orgs` adds `default` and `workspaces` to each entry, and
|
|
521
|
+
`/api/<cli>/config` adds `default_org`.
|
|
522
|
+
|
|
523
|
+
`org:` in the blueprint needs Den 0.2.7 or later: an older Den rejects the
|
|
524
|
+
unknown key. Pin 0.2.7 before you commit it.
|
|
525
|
+
|
|
526
|
+
## Releases
|
|
527
|
+
|
|
528
|
+
The Release page dispatches each repo's CD workflow on GitHub. A repo's
|
|
529
|
+
owner there is its own `gh: owner/repo` in `release.repos`, else
|
|
530
|
+
`release.org` when it is set, else the repo's owner in the catalog, else
|
|
531
|
+
`github_org`. Leave `release.org` unset when the repos belong to different
|
|
532
|
+
orgs: each then releases from its own.
|
|
533
|
+
|
|
378
534
|
## The bin shim
|
|
379
535
|
|
|
380
536
|
`bin/<cli>` is three lines of sh: it names the distribution root and runs
|
|
@@ -449,6 +605,7 @@ it exits:
|
|
|
449
605
|
|
|
450
606
|
```bash
|
|
451
607
|
./bin/acme service install # install (or refresh) and start it
|
|
608
|
+
./bin/acme service install --plan # what install would do; writes and loads nothing
|
|
452
609
|
./bin/acme service status # launchd's view: state, pid, last exit code
|
|
453
610
|
./bin/acme service print # the LaunchAgent plist it would install; installs nothing
|
|
454
611
|
./bin/acme service uninstall # stop it and remove the LaunchAgent
|
|
@@ -477,6 +634,15 @@ answers empty for a few seconds while it starts). If it has not answered
|
|
|
477
634
|
within 30s, it says `installed but not answering yet: check <logs dir>` and
|
|
478
635
|
exits 1.
|
|
479
636
|
|
|
637
|
+
`install --plan` is read-only: it prints the label, the plist path
|
|
638
|
+
(`~/Library/LaunchAgents/<label>.plist`), the log file, the distribution and
|
|
639
|
+
engine roots, whose server answers on `web.port` (this distribution's, free,
|
|
640
|
+
or `port <N> is in use by <product> at <root>; not this distribution's
|
|
641
|
+
server`), and the plist install would write. It writes nothing, never calls
|
|
642
|
+
launchctl, and exits 1 when the port is another distribution's. Any other
|
|
643
|
+
argument to a `service` subcommand is an error, so a mistyped `--plan` never
|
|
644
|
+
installs. `status` also names another distribution's server on the port.
|
|
645
|
+
|
|
480
646
|
`<cli> service` runs the installer script below, which you can also run
|
|
481
647
|
yourself, from the distribution root:
|
|
482
648
|
|
|
@@ -493,10 +659,17 @@ node_modules/@monster-orange/den/service/install.sh print # the plist only
|
|
|
493
659
|
| `DEN_ROOT` | required in a distribution |
|
|
494
660
|
| `DEN_SERVICE_LABEL` | `local.<distribution dir name>.web` |
|
|
495
661
|
| `DEN_SERVICE_LOGS` | `~/Library/Logs/<distribution dir name>` |
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
662
|
+
| `DEN_SERVICE_NODE` | see below |
|
|
663
|
+
|
|
664
|
+
The service runs the engine's server, from the distribution root, with the
|
|
665
|
+
`node` on your `PATH` at install time. A version manager's node (nvm, fnm,
|
|
666
|
+
volta, asdf, mise) lives under a versioned path that disappears when that
|
|
667
|
+
version is removed, so install prefers `/opt/homebrew/bin/node` or
|
|
668
|
+
`/usr/local/bin/node` when one has the same major version, and otherwise
|
|
669
|
+
keeps the version manager's and warns. `DEN_SERVICE_NODE` names the node
|
|
670
|
+
outright. `service status` exits 1 when the installed node no longer exists.
|
|
671
|
+
Re-run `install` after changing node versions or upgrading Den. Keep the
|
|
672
|
+
label stable: it is how launchd knows the service.
|
|
500
673
|
|
|
501
674
|
## MCP server
|
|
502
675
|
|
|
@@ -509,6 +682,13 @@ set of guards. It never starts the web server; if it is not running, every
|
|
|
509
682
|
tool answers with the command that starts it (`./bin/acme web up`). stdout
|
|
510
683
|
carries only the protocol; diagnostics go to stderr.
|
|
511
684
|
|
|
685
|
+
It reads only from this distribution's server. Before serving a tool it
|
|
686
|
+
checks `/api/server-info` (root and product, or for a server from before
|
|
687
|
+
0.2.6 its working directory), trusts a positive answer for 30s, and checks
|
|
688
|
+
again after a connection error. When another distribution's server answers
|
|
689
|
+
on the port, every tool says `port <N> is in use by <product> at <root>; not
|
|
690
|
+
this distribution's server` and returns none of its data.
|
|
691
|
+
|
|
512
692
|
The web server keeps running the Den it started with until it is restarted,
|
|
513
693
|
so after an upgrade it can be older than the MCP server. When a tool's route
|
|
514
694
|
answers 404, the MCP server reads the web server's version from
|
|
@@ -592,22 +772,22 @@ guard off.
|
|
|
592
772
|
|
|
593
773
|
Read tools (always registered):
|
|
594
774
|
|
|
595
|
-
| Tool | Web API | What
|
|
596
|
-
| ------------------- | ------------------------------------------ |
|
|
597
|
-
| `status` | `/api/health` | Service health; `url` (HTTP check) or `check` (shell); `ok` true up, false down, null not known (`pending: true` when a check has not answered) |
|
|
598
|
-
| `services` | `/api/services` | Services, their mode (managed or dev-managed) and commands
|
|
599
|
-
| `applications` | `/api/applications` | Org > Application > Repo > Services
|
|
600
|
-
| `graph` | `/api/graph` | The derived dependency graph and startup order
|
|
601
|
-
| `workspaces` | `/api/workspaces` | Workspaces, active and focused
|
|
602
|
-
| `environments` | `/api/environments` | The enabled environment names (never their values)
|
|
603
|
-
| `logs_search` | `/api/logs/search` | Search services' full log history
|
|
604
|
-
| `logs_tail` | `/api/service/taillog` | The last lines of a service's log (bounded, no follow)
|
|
605
|
-
| `tests_available` | `/api/modules`, `/api/test-sources` | A pillar's modules and the repos they come from
|
|
606
|
-
| `security_latest` | `/api/security/latest` | The newest security scan per mode
|
|
607
|
-
| `release_status` | `/api/release/runs`, `/api/release/status` | Recent CD runs, and an env's deployed-version table
|
|
608
|
-
| `manifest_validate` | `/api/manifest/validate` | Validate a manifest by path or content
|
|
609
|
-
| `stacks` | `/api/stacks` | What `stack ls/ps/health` show: project, routes, health
|
|
610
|
-
| `edge_status` | `/api/edge` | The edge: up or down, its routes and cert names
|
|
775
|
+
| Tool | Web API | What |
|
|
776
|
+
| ------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
777
|
+
| `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) |
|
|
778
|
+
| `services` | `/api/services` | Services, their mode (managed or dev-managed) and commands |
|
|
779
|
+
| `applications` | `/api/applications` | Org > Application > Repo > Services |
|
|
780
|
+
| `graph` | `/api/graph` | The derived dependency graph and startup order |
|
|
781
|
+
| `workspaces` | `/api/workspaces` | Workspaces, active and focused |
|
|
782
|
+
| `environments` | `/api/environments` | The enabled environment names (never their values) |
|
|
783
|
+
| `logs_search` | `/api/logs/search` | Search services' full log history |
|
|
784
|
+
| `logs_tail` | `/api/service/taillog` | The last lines of a service's log (bounded, no follow) |
|
|
785
|
+
| `tests_available` | `/api/modules`, `/api/test-sources` | A pillar's modules and the repos they come from |
|
|
786
|
+
| `security_latest` | `/api/security/latest` | The newest security scan per mode |
|
|
787
|
+
| `release_status` | `/api/release/runs`, `/api/release/status` | Recent CD runs, and an env's deployed-version table |
|
|
788
|
+
| `manifest_validate` | `/api/manifest/validate` | Validate a manifest by path or content |
|
|
789
|
+
| `stacks` | `/api/stacks` | What `stack ls/ps/health` show: project, routes, health |
|
|
790
|
+
| `edge_status` | `/api/edge` | The edge: up or down, its routes and cert names |
|
|
611
791
|
|
|
612
792
|
Action tools (only those named in `mcp.actions`): `service_up`,
|
|
613
793
|
`service_down`, `service_restart` (through `/api/service/exec`),
|
package/package.json
CHANGED
package/service/install.sh
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
#
|
|
16
16
|
# DEN_SERVICE_LABEL launchd label (default local.<dist dir name>.web)
|
|
17
17
|
# DEN_SERVICE_LOGS log directory (default ~/Library/Logs/<dist dir name>)
|
|
18
|
+
# DEN_SERVICE_NODE the node the agent runs (default: see pick_node)
|
|
18
19
|
# DEN_SERVICE_READY_CHECK=external install does not print "running": the
|
|
19
20
|
# caller (`<cli> service install`) checks the server
|
|
20
21
|
# answers first.
|
|
@@ -47,8 +48,36 @@ find_tsx() {
|
|
|
47
48
|
done
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
# A version manager's node (nvm, fnm, volta, asdf, mise) lives under a
|
|
52
|
+
# versioned path that disappears when that version is removed, and the agent
|
|
53
|
+
# then fails to start with no clear reason. Prefer a stable install of the
|
|
54
|
+
# same major version; otherwise keep it and say so.
|
|
55
|
+
STABLE_NODES="${DEN_SERVICE_STABLE_NODES:-/opt/homebrew/bin/node /usr/local/bin/node}"
|
|
56
|
+
node_major() { "$1" -p "process.versions.node.split('.')[0]" 2>/dev/null; }
|
|
57
|
+
pick_node() {
|
|
58
|
+
if [ -n "${DEN_SERVICE_NODE:-}" ]; then
|
|
59
|
+
[ -x "$DEN_SERVICE_NODE" ] || { echo "✗ DEN_SERVICE_NODE ($DEN_SERVICE_NODE) is not executable" >&2; exit 1; }
|
|
60
|
+
echo "$DEN_SERVICE_NODE"; return
|
|
61
|
+
fi
|
|
62
|
+
local node; node="$(command -v node)" || { echo "✗ node is not on PATH" >&2; exit 1; }
|
|
63
|
+
case "$node" in
|
|
64
|
+
*/.nvm/*|*/fnm/*|*/.fnm/*|*/.volta/*|*/.asdf/*|*/mise/*) ;;
|
|
65
|
+
*) echo "$node"; return ;;
|
|
66
|
+
esac
|
|
67
|
+
local want c; want="$(node_major "$node")"
|
|
68
|
+
for c in $STABLE_NODES; do
|
|
69
|
+
if [ -x "$c" ] && [ "$(node_major "$c")" = "$want" ]; then echo "$c"; return; fi
|
|
70
|
+
done
|
|
71
|
+
# Warn when installing; `print` writes the plist and nothing else.
|
|
72
|
+
if [ "${1:-}" = install ]; then
|
|
73
|
+
echo "⚠ the service will run $node, a version manager's node: it stops starting" >&2
|
|
74
|
+
echo " when that version is removed. Re-run install after switching, or set DEN_SERVICE_NODE." >&2
|
|
75
|
+
fi
|
|
76
|
+
echo "$node"
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
write_plist() { # $1 = output file
|
|
51
|
-
NODE="$(
|
|
80
|
+
NODE="$(pick_node "$MODE")"
|
|
52
81
|
TSX="$(find_tsx)" || { echo "✗ tsx not installed: run npm ci in $DIST" >&2; exit 1; }
|
|
53
82
|
DOCKER_DIR="$(dirname "$(command -v docker || echo /usr/local/bin/docker)")"
|
|
54
83
|
SVC_PATH="$(dirname "$NODE"):$DOCKER_DIR:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
@@ -79,7 +108,8 @@ else:
|
|
|
79
108
|
PY
|
|
80
109
|
}
|
|
81
110
|
|
|
82
|
-
|
|
111
|
+
MODE="${1:-install}"
|
|
112
|
+
case "$MODE" in
|
|
83
113
|
install)
|
|
84
114
|
mkdir -p "$LOGS" "$(dirname "$PLIST")"
|
|
85
115
|
write_plist "$PLIST.new"
|
|
@@ -115,6 +145,15 @@ case "${1:-install}" in
|
|
|
115
145
|
write_plist -
|
|
116
146
|
;;
|
|
117
147
|
status)
|
|
148
|
+
# The node the agent was installed with may be gone (a removed nvm
|
|
149
|
+
# version): launchd then restarts it forever with no useful message.
|
|
150
|
+
if [ -f "$PLIST" ] && command -v plutil >/dev/null 2>&1; then
|
|
151
|
+
svc_node="$(plutil -extract ProgramArguments.0 raw -o - "$PLIST" 2>/dev/null || true)"
|
|
152
|
+
if [ -n "$svc_node" ] && [ ! -x "$svc_node" ]; then
|
|
153
|
+
echo "✗ $LABEL runs $svc_node, which no longer exists: re-run service install" >&2
|
|
154
|
+
exit 1
|
|
155
|
+
fi
|
|
156
|
+
fi
|
|
118
157
|
# launchctl's own "Could not find service" is noise when it isn't loaded.
|
|
119
158
|
if out="$(launchctl print "$DOMAIN/$LABEL" 2>/dev/null)"; then
|
|
120
159
|
printf '%s\n' "$out" | grep -E '^\s*(state|pid|last exit code|runs|spawn type)'
|
|
@@ -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 ||
|
|
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
|
|
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
|
|
58
|
+
` Found ${plural(profiles.length, "profile")}: ${profiles.join(", ")}\n`,
|
|
58
59
|
);
|
|
59
60
|
}
|
|
60
61
|
|