@monster-orange/den 0.2.0 → 0.2.1
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 +67 -0
- package/bin/den +3 -2
- package/docs/distributions.md +37 -9
- package/package.json +1 -1
- package/src/shared/environment-manager/service-runner.ts +4 -2
- package/src/shared/manifest/placement.ts +15 -3
- package/src/ui/cli/index.ts +55 -12
- package/src/ui/cli/mcp-connect.ts +350 -0
- package/src/ui/cli/named-target.ts +11 -0
- package/src/ui/mcp/api.ts +2 -0
- package/src/ui/mcp/connect.ts +304 -0
- package/src/ui/mcp/guards.ts +6 -2
- package/src/ui/mcp/server.ts +2 -0
- package/src/ui/mcp/tools.ts +89 -10
- package/src/ui/server/audit.ts +69 -3
- package/src/ui/server/index.ts +83 -2
- package/src/ui/server/lifecycle-exec.ts +188 -0
- package/src/ui/server/public/graph.css +4 -0
- package/src/ui/server/public/panel.js +6 -3
- package/src/ui/server/readonly-routes.ts +11 -2
- package/src/ui/server/workspace-routes.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,73 @@ 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.1] - 2026-11-04
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **MCP `service_restart` on a service with only `up` and `down`.**
|
|
12
|
+
`/api/service/exec` now runs `up`, `down` and `restart` (env local) through
|
|
13
|
+
the service runner, as `<cli> up|down|restart` do, instead of looking up
|
|
14
|
+
and executing the manifest's command itself. So, for the web UI's panel and
|
|
15
|
+
for MCP alike: `restart` with no declared `restart` is `down` then `up`;
|
|
16
|
+
`up` on a service that is already healthy starts nothing and says
|
|
17
|
+
`already up (healthy)`; `up` attaches to a running shared instance; `down`
|
|
18
|
+
honours workspace ownership. The panel offers Restart whenever a service has
|
|
19
|
+
`up` and `down`.
|
|
20
|
+
- **A detached start says when it is ready.** `/api/service/exec` sends a
|
|
21
|
+
`ready` event once a detached `up` or `restart` is up: its health check
|
|
22
|
+
passes (`checked: true`; for a restart, only after the old instance went
|
|
23
|
+
down or 5s passed), or, with no health check, its launcher is still running
|
|
24
|
+
after 5s (`checked: false`). MCP `service_up` and `service_restart` return
|
|
25
|
+
at `ready` (`"ready": true` in the result) instead of waiting for a
|
|
26
|
+
launcher that runs in the foreground (a dev server) to exit, which ran
|
|
27
|
+
into the 180s timeout and reported a false failure. A restart of a running
|
|
28
|
+
shared instance now says `restarting the running instance`, not
|
|
29
|
+
`already running`, and the log path is printed once. An MCP action's
|
|
30
|
+
`ended` reads `ready` or `handed-off` for a detached start, never
|
|
31
|
+
`stopped`.
|
|
32
|
+
- **MCP actions report failure.** `service_up|down|restart`, `run_tests` and
|
|
33
|
+
`workspace_sync` return `isError` with `"failed": true` in the result when
|
|
34
|
+
the command exits non-zero, a detached service's launcher exits non-zero,
|
|
35
|
+
or the call times out. They used to come back ok with `exitCode: 1` inside.
|
|
36
|
+
- **`<cli> up|down|restart <remote service>` exits 1.** Naming a remote
|
|
37
|
+
service (or its repo) directly is a refused command. A remote service
|
|
38
|
+
reached through a workspace, `-a` or `-A <app>` run is still said and
|
|
39
|
+
skipped, exit 0.
|
|
40
|
+
- **Refusals name the command.** `runs in dev (AWS); nothing to restart here`
|
|
41
|
+
(and `stop`, `start`, `run "<cmd>" on`), not `start` for everything.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **`<cli> mcp connect`: register the MCP server with your agents.**
|
|
46
|
+
`mcp clients` lists the agents installed here (Claude Code, Codex, Cursor,
|
|
47
|
+
VS Code, Claude Desktop, Windsurf, Gemini CLI) and whether each is
|
|
48
|
+
connected. `mcp connect <client>...|--all` registers the distribution's
|
|
49
|
+
server at user scope through each client's own CLI or config file, and
|
|
50
|
+
`--project [dir]` writes a project's config instead. `--dry-run` shows the
|
|
51
|
+
change, `mcp disconnect` removes it, and `mcp config` prints JSON for any
|
|
52
|
+
other client. The registration pins `PATH` to the running node so GUI
|
|
53
|
+
agents can launch it. See "Connecting your agents" in
|
|
54
|
+
`docs/distributions.md`.
|
|
55
|
+
- **Refused attempts are in the activity trail.** `/api/activity` entries may
|
|
56
|
+
carry `outcome: "refused"` and a `reason`: a refused or unresolvable
|
|
57
|
+
`/api/service/exec`, and any MCP action a guard refused before it reached
|
|
58
|
+
the web server. Agents report the latter to `POST /api/activity` (agent
|
|
59
|
+
audit header required; it appends one sanitised entry and nothing else).
|
|
60
|
+
The panel's Activity tab shows them as refused. An attempt that named only
|
|
61
|
+
the repo shows for the repo and for each of its services.
|
|
62
|
+
- **`target.expect` in placement.** `/api/services` and `/api/health` rows'
|
|
63
|
+
`target` now carry `expect` (as declared) when the manifest's target has its
|
|
64
|
+
own `url`.
|
|
65
|
+
|
|
66
|
+
### Migrations
|
|
67
|
+
|
|
68
|
+
- None required. Two additive API changes a contract probe may see:
|
|
69
|
+
`target.expect` on `/api/services` and `/api/health` rows, and
|
|
70
|
+
`outcome`/`reason` on `/api/activity` entries.
|
|
71
|
+
- Scripts that relied on `<cli> restart|down <remote service>` exiting 0
|
|
72
|
+
should expect 1.
|
|
73
|
+
|
|
7
74
|
## [0.2.0] - 2026-11-04
|
|
8
75
|
|
|
9
76
|
### Added
|
package/bin/den
CHANGED
|
@@ -41,8 +41,9 @@ if [ "${1:-}" = "create" ]; then
|
|
|
41
41
|
built="dist/src/ui/cli/create-main.js"
|
|
42
42
|
fi
|
|
43
43
|
# `mcp` speaks the MCP protocol on stdout: its own entry point, so nothing
|
|
44
|
-
# the full CLI prints can reach the protocol stream.
|
|
45
|
-
|
|
44
|
+
# the full CLI prints can reach the protocol stream. Its setup subcommands
|
|
45
|
+
# (connect, disconnect, clients, config) are ordinary CLI commands.
|
|
46
|
+
if [ "${1:-}" = "mcp" ] && ! case "${2:-}" in connect|disconnect|clients|config) true ;; *) false ;; esac; then
|
|
46
47
|
shift
|
|
47
48
|
entry="src/ui/cli/mcp-main.ts"
|
|
48
49
|
built="dist/src/ui/cli/mcp-main.js"
|
package/docs/distributions.md
CHANGED
|
@@ -367,17 +367,45 @@ answers 404, the MCP server reads the web server's version from
|
|
|
367
367
|
`web server is Den 0.1.0; stacks needs >= 0.2.0: run acme web restart`. Any
|
|
368
368
|
other 404 is reported as it is.
|
|
369
369
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
370
|
+
### Connecting your agents
|
|
371
|
+
|
|
372
|
+
`<cli> mcp connect` registers the server with the agents you use, so nobody
|
|
373
|
+
edits an MCP config by hand:
|
|
374
|
+
|
|
375
|
+
```sh
|
|
376
|
+
./bin/acme mcp clients # which agents are installed, which are connected
|
|
377
|
+
./bin/acme mcp connect --all # every installed agent, user scope
|
|
378
|
+
./bin/acme mcp connect claude-code # one (or several) by id
|
|
379
|
+
./bin/acme mcp connect cursor --project ../shop # a project's config instead
|
|
380
|
+
./bin/acme mcp connect --all --dry-run # show what it would write or run
|
|
381
|
+
./bin/acme mcp disconnect cursor
|
|
382
|
+
./bin/acme mcp config # JSON for any other MCP client
|
|
378
383
|
```
|
|
379
384
|
|
|
380
|
-
|
|
385
|
+
| Client | User scope | Project scope (`--project [dir]`) |
|
|
386
|
+
| ---------------- | ------------------------------------- | --------------------------------- |
|
|
387
|
+
| `claude-code` | `claude mcp add --scope user` | `<dir>/.mcp.json` |
|
|
388
|
+
| `codex` | `codex mcp add` | – |
|
|
389
|
+
| `cursor` | `~/.cursor/mcp.json` | `<dir>/.cursor/mcp.json` |
|
|
390
|
+
| `vscode` | `code --add-mcp` | `<dir>/.vscode/mcp.json` |
|
|
391
|
+
| `claude-desktop` | its `claude_desktop_config.json` | – |
|
|
392
|
+
| `windsurf` | `~/.codeium/windsurf/mcp_config.json` | – |
|
|
393
|
+
| `gemini` | `~/.gemini/settings.json` | `<dir>/.gemini/settings.json` |
|
|
394
|
+
|
|
395
|
+
The server is registered under the CLI's name (`acme`) as the distribution's
|
|
396
|
+
`bin/<cli>` by absolute path, with `PATH` set to the directory of the `node`
|
|
397
|
+
that ran `connect` plus the system dirs. GUI agents start without your login
|
|
398
|
+
shell's PATH (so without nvm's node) and would otherwise fail to launch it.
|
|
399
|
+
After upgrading or switching node, run `connect` again. For a JSON config,
|
|
400
|
+
`connect` changes only its own entry, keeps the previous file as `.bak`, and
|
|
401
|
+
refuses to rewrite a file it cannot parse. Connecting again replaces the entry.
|
|
402
|
+
Restart the agent, or start a new session, to load it.
|
|
403
|
+
|
|
404
|
+
Project-scope files hold absolute paths on your machine: commit them only if
|
|
405
|
+
everyone's checkout is in the same place.
|
|
406
|
+
|
|
407
|
+
`--port <n>` (on `mcp` or `mcp connect`) points it at a web server on another
|
|
408
|
+
port.
|
|
381
409
|
|
|
382
410
|
### Config
|
|
383
411
|
|
package/package.json
CHANGED
|
@@ -1063,7 +1063,7 @@ export function runServiceCommand(
|
|
|
1063
1063
|
resolved.address,
|
|
1064
1064
|
servicePlacement(readManifest(repoPath), subsystem, "local"),
|
|
1065
1065
|
(e) => isProductionEnv(e) === true,
|
|
1066
|
-
command
|
|
1066
|
+
command,
|
|
1067
1067
|
);
|
|
1068
1068
|
if (refusal) {
|
|
1069
1069
|
onLine(refusal);
|
|
@@ -1094,7 +1094,9 @@ export function runServiceCommand(
|
|
|
1094
1094
|
workspace,
|
|
1095
1095
|
];
|
|
1096
1096
|
onLine(
|
|
1097
|
-
|
|
1097
|
+
command === "restart"
|
|
1098
|
+
? `restarting the running instance (shared) — owners: [${owners.join(", ")}] at ${resolved.repoPath}`
|
|
1099
|
+
: `already running (shared) — owners: [${owners.join(", ")}] at ${resolved.repoPath}`,
|
|
1098
1100
|
);
|
|
1099
1101
|
// restart still needs to bounce; only short-circuit pure `up`.
|
|
1100
1102
|
if (command === "up") {
|
|
@@ -34,6 +34,8 @@ export interface PlacementTarget {
|
|
|
34
34
|
env: string;
|
|
35
35
|
account?: string;
|
|
36
36
|
url?: string;
|
|
37
|
+
/** The status codes that count as up for `url`, as declared. */
|
|
38
|
+
expect?: number | number[];
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export interface Placement {
|
|
@@ -84,6 +86,7 @@ function targetFor(svc: ServiceManagementConfig, env: string): PlacementTarget {
|
|
|
84
86
|
env: tEnv,
|
|
85
87
|
...(own?.account ? { account: own.account } : {}),
|
|
86
88
|
...(url ? { url } : {}),
|
|
89
|
+
...(own?.url && own.expect !== undefined ? { expect: own.expect } : {}),
|
|
87
90
|
};
|
|
88
91
|
}
|
|
89
92
|
|
|
@@ -186,16 +189,25 @@ export function isProtectedRemote(p: Placement, isProduction: IsProduction) {
|
|
|
186
189
|
return p.runs === "remote" && !!p.target && isProduction(p.target.env);
|
|
187
190
|
}
|
|
188
191
|
|
|
192
|
+
/** How a refusal names the command: up → "start", exec → `run "exec" on`. */
|
|
193
|
+
function refusedVerb(command: string): string {
|
|
194
|
+
if (command === "up" || command === "start") return "start";
|
|
195
|
+
if (command === "down" || command === "stop") return "stop";
|
|
196
|
+
if (command === "restart") return "restart";
|
|
197
|
+
return `run "${command}" on`;
|
|
198
|
+
}
|
|
199
|
+
|
|
189
200
|
/**
|
|
190
|
-
* Why Den must not run
|
|
191
|
-
* remote service is refused ("runs in dev (AWS); nothing to
|
|
201
|
+
* Why Den must not run `command` on `address`, or null. Every command on a
|
|
202
|
+
* remote service is refused ("runs in dev (AWS); nothing to restart here").
|
|
192
203
|
*/
|
|
193
204
|
export function placementRefusal(
|
|
194
205
|
address: string,
|
|
195
206
|
p: Placement,
|
|
196
207
|
isProduction: IsProduction,
|
|
197
|
-
|
|
208
|
+
command = "up",
|
|
198
209
|
): string | null {
|
|
210
|
+
const verb = refusedVerb(command);
|
|
199
211
|
if (p.runs !== "remote") return null;
|
|
200
212
|
const where = p.target
|
|
201
213
|
? `${p.target.env}${p.target.account ? ` (${p.target.account})` : ""}`
|
package/src/ui/cli/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Main entry point for the interactive CLI
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isNamedTarget } from "./named-target.js";
|
|
8
9
|
import chalk from "chalk";
|
|
9
10
|
import oraSpinner from "ora";
|
|
10
11
|
|
|
@@ -695,6 +696,17 @@ class DenCLI {
|
|
|
695
696
|
break;
|
|
696
697
|
|
|
697
698
|
case "mcp": {
|
|
699
|
+
// Setup subcommands: register the server with agents.
|
|
700
|
+
const { MCP_CONNECT_SUBCOMMANDS, runMcpConnect } =
|
|
701
|
+
await import("./mcp-connect.js");
|
|
702
|
+
if (
|
|
703
|
+
(MCP_CONNECT_SUBCOMMANDS as readonly string[]).includes(
|
|
704
|
+
args[1] ?? "",
|
|
705
|
+
)
|
|
706
|
+
) {
|
|
707
|
+
process.exitCode = await runMcpConnect(args.slice(1));
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
698
710
|
// bin/den routes `mcp` to its own entry (stdout is the protocol);
|
|
699
711
|
// this covers the CLI run directly from a checkout.
|
|
700
712
|
await import("./mcp-stdout-guard.js");
|
|
@@ -1298,7 +1310,15 @@ class DenCLI {
|
|
|
1298
1310
|
/** Skip ANY already-healthy node (derived `up` starts only what's down). */
|
|
1299
1311
|
skipHealthyAll?: boolean;
|
|
1300
1312
|
},
|
|
1301
|
-
opts?: {
|
|
1313
|
+
opts?: {
|
|
1314
|
+
force?: boolean;
|
|
1315
|
+
/**
|
|
1316
|
+
* What the user named on the command line (repo or address). A remote
|
|
1317
|
+
* service among them is refused with a non-zero exit; one reached only
|
|
1318
|
+
* as part of a workspace or app run is said and skipped.
|
|
1319
|
+
*/
|
|
1320
|
+
named?: readonly string[];
|
|
1321
|
+
},
|
|
1302
1322
|
): Promise<void> {
|
|
1303
1323
|
// How long to show startup output before handing control back to the menu
|
|
1304
1324
|
// for detachable (long-running) commands like `up`.
|
|
@@ -1388,11 +1408,16 @@ class DenCLI {
|
|
|
1388
1408
|
"local",
|
|
1389
1409
|
),
|
|
1390
1410
|
(e) => isProductionEnv(e),
|
|
1391
|
-
command
|
|
1411
|
+
command,
|
|
1392
1412
|
)
|
|
1393
1413
|
: null;
|
|
1394
1414
|
if (remote) {
|
|
1395
|
-
|
|
1415
|
+
if (isNamedTarget(address, opts?.named ?? [])) {
|
|
1416
|
+
console.log(chalk.red(` ✗ ${remote}`));
|
|
1417
|
+
process.exitCode = 1;
|
|
1418
|
+
} else {
|
|
1419
|
+
console.log(chalk.gray(` • ${remote}`));
|
|
1420
|
+
}
|
|
1396
1421
|
continue;
|
|
1397
1422
|
}
|
|
1398
1423
|
if (command === "up" && !seedAddresses.has(address)) {
|
|
@@ -1732,6 +1757,7 @@ class DenCLI {
|
|
|
1732
1757
|
plan = false,
|
|
1733
1758
|
pull = false,
|
|
1734
1759
|
): Promise<void> {
|
|
1760
|
+
const named = [seed];
|
|
1735
1761
|
const full = deriveGraph({ scope: "all" });
|
|
1736
1762
|
// Same positional closure the web "up with deps" uses: everything the seed
|
|
1737
1763
|
// sits ON TOP OF, never anything above it. Keeps the two entry points from
|
|
@@ -1813,12 +1839,17 @@ class DenCLI {
|
|
|
1813
1839
|
);
|
|
1814
1840
|
return;
|
|
1815
1841
|
}
|
|
1816
|
-
await this.runServicesCommand(
|
|
1842
|
+
await this.runServicesCommand(
|
|
1817
1843
|
order,
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1844
|
+
"up",
|
|
1845
|
+
{
|
|
1846
|
+
order,
|
|
1847
|
+
waitDepsFor: (address) => hardDepsOf(graph, address),
|
|
1848
|
+
failFast: true,
|
|
1849
|
+
skipHealthyAll: true,
|
|
1850
|
+
},
|
|
1851
|
+
{ named },
|
|
1852
|
+
);
|
|
1822
1853
|
}
|
|
1823
1854
|
|
|
1824
1855
|
/**
|
|
@@ -1874,7 +1905,12 @@ class DenCLI {
|
|
|
1874
1905
|
console.log(chalk.gray(" (--dry-run: not stopping anything)\n"));
|
|
1875
1906
|
return;
|
|
1876
1907
|
}
|
|
1877
|
-
await this.runServicesCommand(
|
|
1908
|
+
await this.runServicesCommand(
|
|
1909
|
+
order,
|
|
1910
|
+
"down",
|
|
1911
|
+
{ order },
|
|
1912
|
+
{ force, named: [seed] },
|
|
1913
|
+
);
|
|
1878
1914
|
}
|
|
1879
1915
|
|
|
1880
1916
|
/** Workspace repos with no runtime (modules, libraries) → their kind. */
|
|
@@ -2392,7 +2428,9 @@ class DenCLI {
|
|
|
2392
2428
|
return;
|
|
2393
2429
|
}
|
|
2394
2430
|
preflightSecrets();
|
|
2395
|
-
await this.runServicesCommand(targets, "up"
|
|
2431
|
+
await this.runServicesCommand(targets, "up", undefined, {
|
|
2432
|
+
named: addresses,
|
|
2433
|
+
});
|
|
2396
2434
|
}
|
|
2397
2435
|
|
|
2398
2436
|
private async cmdServiceDown(args: string[]): Promise<void> {
|
|
@@ -2452,7 +2490,10 @@ class DenCLI {
|
|
|
2452
2490
|
addresses.length > 0
|
|
2453
2491
|
? addresses
|
|
2454
2492
|
: this.resolveNoArgServiceTargets("down");
|
|
2455
|
-
await this.runServicesCommand(targets, "down", undefined, {
|
|
2493
|
+
await this.runServicesCommand(targets, "down", undefined, {
|
|
2494
|
+
force,
|
|
2495
|
+
named: addresses,
|
|
2496
|
+
});
|
|
2456
2497
|
}
|
|
2457
2498
|
|
|
2458
2499
|
private async cmdServiceRestart(argv: string[]): Promise<void> {
|
|
@@ -2471,7 +2512,9 @@ class DenCLI {
|
|
|
2471
2512
|
filtered.length > 0
|
|
2472
2513
|
? filtered
|
|
2473
2514
|
: this.resolveNoArgServiceTargets("restart");
|
|
2474
|
-
await this.runServicesCommand(addresses, "restart"
|
|
2515
|
+
await this.runServicesCommand(addresses, "restart", undefined, {
|
|
2516
|
+
named: filtered,
|
|
2517
|
+
});
|
|
2475
2518
|
}
|
|
2476
2519
|
|
|
2477
2520
|
/** Service address with a ⏸ marker when the repo is auto-start off. */
|