@monster-orange/den 0.1.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/README.md +1 -1
  3. package/bin/den +8 -0
  4. package/docs/distributions.md +283 -12
  5. package/package.json +4 -2
  6. package/src/pillars/journeys/sandbox.ts +2 -5
  7. package/src/pillars/security/discover.ts +17 -1
  8. package/src/pillars/security/run.ts +31 -3
  9. package/src/shared/agent-actor.ts +77 -0
  10. package/src/shared/den-config.ts +122 -0
  11. package/src/shared/edge/status.ts +74 -0
  12. package/src/shared/environment-manager/health.ts +54 -8
  13. package/src/shared/environment-manager/load-applications.ts +31 -1
  14. package/src/shared/environment-manager/log-tail.ts +5 -4
  15. package/src/shared/environment-manager/opctl.ts +2 -1
  16. package/src/shared/environment-manager/service-container.ts +47 -0
  17. package/src/shared/environment-manager/service-runner.ts +19 -2
  18. package/src/shared/environment-manager/sync-targets.ts +66 -0
  19. package/src/shared/environment-manager/up-plan.ts +25 -1
  20. package/src/shared/manifest/merge-override.ts +75 -0
  21. package/src/shared/manifest/placement.ts +333 -0
  22. package/src/shared/manifest/read-manifest.ts +156 -38
  23. package/src/shared/manifest/schema.ts +14 -0
  24. package/src/shared/manifest/validate-layers.ts +229 -0
  25. package/src/shared/manifest/validate-manifest.ts +66 -6
  26. package/src/shared/paths.ts +12 -0
  27. package/src/shared/redact.ts +107 -0
  28. package/src/shared/stacks/manifest-commands.ts +30 -0
  29. package/src/shared/stacks/status.ts +99 -0
  30. package/src/shared/types/index.ts +23 -0
  31. package/src/ui/cli/applications-command.ts +34 -2
  32. package/src/ui/cli/edge-command.ts +9 -19
  33. package/src/ui/cli/graph-command.ts +50 -4
  34. package/src/ui/cli/index.ts +184 -55
  35. package/src/ui/cli/mcp-connect.ts +350 -0
  36. package/src/ui/cli/mcp-main.ts +13 -0
  37. package/src/ui/cli/mcp-stdout-guard.ts +10 -0
  38. package/src/ui/cli/named-target.ts +11 -0
  39. package/src/ui/cli/stack-command.ts +13 -40
  40. package/src/ui/cli/test-runner.ts +8 -1
  41. package/src/ui/mcp/api.ts +286 -0
  42. package/src/ui/mcp/connect.ts +304 -0
  43. package/src/ui/mcp/guards.ts +145 -0
  44. package/src/ui/mcp/server.ts +149 -0
  45. package/src/ui/mcp/tools.ts +644 -0
  46. package/src/ui/server/audit.ts +137 -0
  47. package/src/ui/server/changelog-parser.ts +11 -0
  48. package/src/ui/server/graph-prefs.ts +89 -0
  49. package/src/ui/server/health-row.ts +15 -0
  50. package/src/ui/server/index.ts +256 -61
  51. package/src/ui/server/lifecycle-exec.ts +188 -0
  52. package/src/ui/server/log-history.ts +4 -3
  53. package/src/ui/server/public/environments.html +4 -2
  54. package/src/ui/server/public/graph-model.js +416 -0
  55. package/src/ui/server/public/graph.css +980 -0
  56. package/src/ui/server/public/graph.js +986 -0
  57. package/src/ui/server/public/header.js +7 -5
  58. package/src/ui/server/public/local-dev.html +130 -2191
  59. package/src/ui/server/public/logs.html +11 -4
  60. package/src/ui/server/public/panel.js +790 -0
  61. package/src/ui/server/public/services.html +6 -1
  62. package/src/ui/server/readonly-routes.ts +209 -0
  63. package/src/ui/server/ui-assets.ts +73 -0
  64. package/src/ui/server/workspace-routes.ts +39 -8
@@ -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
 
@@ -101,6 +102,7 @@ import {
101
102
  getReposRoot,
102
103
  mergeUserConfig,
103
104
  readDenConfig,
105
+ isProductionEnv,
104
106
  readUserConfig,
105
107
  } from "../../shared/den-config.js";
106
108
  import { deriveGraph } from "../../shared/environment-manager/derivation/derive-graph.js";
@@ -120,7 +122,7 @@ import {
120
122
  } from "./applications-command.js";
121
123
  import { loadApplications } from "../../shared/environment-manager/load-applications.js";
122
124
  import { hasRuntime, repoKind } from "../../shared/repo-kind.js";
123
- import type { RepoKind } from "../../shared/types/index.js";
125
+ import type { RepoKind, ValidationResult } from "../../shared/types/index.js";
124
126
  import { localCommandsFor } from "../../shared/stacks/manifest-commands.js";
125
127
  import {
126
128
  buildUpPlan,
@@ -131,7 +133,15 @@ import * as fs from "fs";
131
133
  import { readManifest } from "../../shared/manifest/read-manifest.js";
132
134
  import * as nodePath from "path";
133
135
  import { runInit } from "./init.js";
134
- import { validateManifest } from "../../shared/manifest/validate-manifest.js";
136
+ import {
137
+ validateManifest,
138
+ validateManifestLayers,
139
+ } from "../../shared/manifest/validate-manifest.js";
140
+ import {
141
+ placementRefusal,
142
+ placementSuffix,
143
+ servicePlacement,
144
+ } from "../../shared/manifest/placement.js";
135
145
  import {
136
146
  getDistRoot,
137
147
  getEngineRoot,
@@ -290,6 +300,13 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
290
300
  "This org's HTTPS edge: own CA + cert, routes at <name>.<edge.domain>.",
291
301
  ],
292
302
  },
303
+ mcp: {
304
+ usage: "mcp [--port <web port>]",
305
+ lines: [
306
+ "An MCP server over stdio for agents: a client of the running web server",
307
+ `(\`${product().cli} web up\`). Read tools always; actions only as named in mcp.actions.`,
308
+ ],
309
+ },
293
310
  "local-dev": {
294
311
  usage: "local-dev [on|off]",
295
312
  lines: [
@@ -345,6 +362,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
345
362
  "stack",
346
363
  "apps",
347
364
  "applications",
365
+ "mcp",
348
366
  ]);
349
367
 
350
368
  /** Map a raw command token to a PII-safe, registered telemetry command name. */
@@ -362,6 +380,52 @@ function telemetryReason(error: unknown): string {
362
380
  return name && name !== "Object" ? name : "Error";
363
381
  }
364
382
 
383
+ /** Print one manifest's validation: a status line, then each issue. */
384
+ function printValidation(
385
+ label: string,
386
+ result: ValidationResult,
387
+ layer?: string,
388
+ ): void {
389
+ const tag = layer ? `${layer}, ` : "";
390
+ const plural = (n: number, word: string): string =>
391
+ `${n} ${word}${n === 1 ? "" : "s"}`;
392
+ if (result.valid && result.warnings.length === 0) {
393
+ console.log(
394
+ chalk.green(` ✓ ${label}`) +
395
+ chalk.gray(` (${tag}v${result.version}, no issues)`),
396
+ );
397
+ } else if (result.valid) {
398
+ console.log(
399
+ chalk.yellow(` ⚠ ${label}`) +
400
+ chalk.gray(
401
+ ` (${tag}v${result.version}, ${plural(result.warnings.length, "warning")})`,
402
+ ),
403
+ );
404
+ } else {
405
+ console.log(
406
+ chalk.red(` ✗ ${label}`) +
407
+ chalk.gray(
408
+ ` (${tag}v${result.version}, ${plural(result.errors.length, "error")}${result.warnings.length > 0 ? `, ${plural(result.warnings.length, "warning")}` : ""})`,
409
+ ),
410
+ );
411
+ }
412
+
413
+ for (const err of result.errors) {
414
+ const path = err.path ? chalk.gray(err.path) + " " : "";
415
+ console.log(` ${chalk.red("ERROR")} ${path}${err.message}`);
416
+ if (err.suggestion) {
417
+ console.log(` ${chalk.gray("→ " + err.suggestion)}`);
418
+ }
419
+ }
420
+ for (const warn of result.warnings) {
421
+ const path = warn.path ? chalk.gray(warn.path) + " " : "";
422
+ console.log(` ${chalk.yellow("WARNING")} ${path}${warn.message}`);
423
+ if (warn.suggestion) {
424
+ console.log(` ${chalk.gray("→ " + warn.suggestion)}`);
425
+ }
426
+ }
427
+ }
428
+
365
429
  class DenCLI {
366
430
  private static readonly serviceCommands = new Set([
367
431
  "up",
@@ -631,6 +695,26 @@ class DenCLI {
631
695
  cmdApps(args.slice(1));
632
696
  break;
633
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
+ }
710
+ // bin/den routes `mcp` to its own entry (stdout is the protocol);
711
+ // this covers the CLI run directly from a checkout.
712
+ await import("./mcp-stdout-guard.js");
713
+ const { runMcp } = await import("../mcp/server.js");
714
+ await runMcp(args.slice(1));
715
+ break;
716
+ }
717
+
634
718
  case "--help":
635
719
  case "-h":
636
720
  this.displayHelp();
@@ -1226,7 +1310,15 @@ class DenCLI {
1226
1310
  /** Skip ANY already-healthy node (derived `up` starts only what's down). */
1227
1311
  skipHealthyAll?: boolean;
1228
1312
  },
1229
- opts?: { force?: boolean },
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
+ },
1230
1322
  ): Promise<void> {
1231
1323
  // How long to show startup output before handing control back to the menu
1232
1324
  // for detachable (long-running) commands like `up`.
@@ -1304,6 +1396,30 @@ class DenCLI {
1304
1396
  printPartialState();
1305
1397
  return;
1306
1398
  }
1399
+ // R13: a remote service is never started or stopped from here. Said,
1400
+ // not failed, so a workspace or app up/down carries on past it.
1401
+ const placed = resolveService(address, workspace);
1402
+ const remote = placed
1403
+ ? placementRefusal(
1404
+ address,
1405
+ servicePlacement(
1406
+ readManifest(placed.repoPath),
1407
+ placed.subsystem,
1408
+ "local",
1409
+ ),
1410
+ (e) => isProductionEnv(e),
1411
+ command,
1412
+ )
1413
+ : null;
1414
+ if (remote) {
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
+ }
1421
+ continue;
1422
+ }
1307
1423
  if (command === "up" && !seedAddresses.has(address)) {
1308
1424
  const resolved = resolveService(address, workspace);
1309
1425
  if (resolved) {
@@ -1641,6 +1757,7 @@ class DenCLI {
1641
1757
  plan = false,
1642
1758
  pull = false,
1643
1759
  ): Promise<void> {
1760
+ const named = [seed];
1644
1761
  const full = deriveGraph({ scope: "all" });
1645
1762
  // Same positional closure the web "up with deps" uses: everything the seed
1646
1763
  // sits ON TOP OF, never anything above it. Keeps the two entry points from
@@ -1722,12 +1839,17 @@ class DenCLI {
1722
1839
  );
1723
1840
  return;
1724
1841
  }
1725
- await this.runServicesCommand(order, "up", {
1842
+ await this.runServicesCommand(
1726
1843
  order,
1727
- waitDepsFor: (address) => hardDepsOf(graph, address),
1728
- failFast: true,
1729
- skipHealthyAll: true,
1730
- });
1844
+ "up",
1845
+ {
1846
+ order,
1847
+ waitDepsFor: (address) => hardDepsOf(graph, address),
1848
+ failFast: true,
1849
+ skipHealthyAll: true,
1850
+ },
1851
+ { named },
1852
+ );
1731
1853
  }
1732
1854
 
1733
1855
  /**
@@ -1783,7 +1905,12 @@ class DenCLI {
1783
1905
  console.log(chalk.gray(" (--dry-run: not stopping anything)\n"));
1784
1906
  return;
1785
1907
  }
1786
- await this.runServicesCommand(order, "down", { order }, { force });
1908
+ await this.runServicesCommand(
1909
+ order,
1910
+ "down",
1911
+ { order },
1912
+ { force, named: [seed] },
1913
+ );
1787
1914
  }
1788
1915
 
1789
1916
  /** Workspace repos with no runtime (modules, libraries) → their kind. */
@@ -2301,7 +2428,9 @@ class DenCLI {
2301
2428
  return;
2302
2429
  }
2303
2430
  preflightSecrets();
2304
- await this.runServicesCommand(targets, "up");
2431
+ await this.runServicesCommand(targets, "up", undefined, {
2432
+ named: addresses,
2433
+ });
2305
2434
  }
2306
2435
 
2307
2436
  private async cmdServiceDown(args: string[]): Promise<void> {
@@ -2361,7 +2490,10 @@ class DenCLI {
2361
2490
  addresses.length > 0
2362
2491
  ? addresses
2363
2492
  : this.resolveNoArgServiceTargets("down");
2364
- await this.runServicesCommand(targets, "down", undefined, { force });
2493
+ await this.runServicesCommand(targets, "down", undefined, {
2494
+ force,
2495
+ named: addresses,
2496
+ });
2365
2497
  }
2366
2498
 
2367
2499
  private async cmdServiceRestart(argv: string[]): Promise<void> {
@@ -2380,7 +2512,9 @@ class DenCLI {
2380
2512
  filtered.length > 0
2381
2513
  ? filtered
2382
2514
  : this.resolveNoArgServiceTargets("restart");
2383
- await this.runServicesCommand(addresses, "restart");
2515
+ await this.runServicesCommand(addresses, "restart", undefined, {
2516
+ named: filtered,
2517
+ });
2384
2518
  }
2385
2519
 
2386
2520
  /** Service address with a ⏸ marker when the repo is auto-start off. */
@@ -2442,9 +2576,14 @@ class DenCLI {
2442
2576
  for (const r of results) {
2443
2577
  const healthStr = formatHealthLabel(r.health);
2444
2578
  const reasonStr = formatHealthReason(r);
2579
+ const placed = placementSuffix({
2580
+ runs: r.runs ?? "none",
2581
+ target: r.target ?? null,
2582
+ });
2445
2583
 
2446
2584
  table.push([
2447
- this.labelWithAutostart(r.address, noAutostart),
2585
+ this.labelWithAutostart(r.address, noAutostart) +
2586
+ (placed ? chalk.cyan(` ${placed}`) : ""),
2448
2587
  r.mode === "dev-managed"
2449
2588
  ? chalk.blue("dev-managed")
2450
2589
  : chalk.magenta("managed"),
@@ -2606,13 +2745,19 @@ class DenCLI {
2606
2745
  /* ignore */
2607
2746
  }
2608
2747
 
2748
+ const placed = placementSuffix({
2749
+ runs: r.runs ?? "none",
2750
+ target: r.target ?? null,
2751
+ });
2609
2752
  table.push([
2610
- this.labelWithAutostart(r.address, noAutostart),
2753
+ this.labelWithAutostart(r.address, noAutostart) +
2754
+ (placed ? chalk.cyan(` ${placed}`) : ""),
2611
2755
  r.mode === "dev-managed"
2612
2756
  ? chalk.blue("dev-managed")
2613
2757
  : chalk.magenta("managed"),
2614
2758
  healthStr,
2615
- cmds.join(", "),
2759
+ // A remote service's commands are never run from here.
2760
+ placed ? chalk.gray("—") : cmds.join(", "),
2616
2761
  ]);
2617
2762
  }
2618
2763
 
@@ -3415,10 +3560,26 @@ class DenCLI {
3415
3560
  req.on("error", reject);
3416
3561
  });
3417
3562
  } else {
3418
- const filePath = fs.existsSync(target)
3419
- ? fs.lstatSync(target).isDirectory()
3420
- ? nodePath.join(target, `${product().manifestYml}`)
3421
- : target
3563
+ const isDir = fs.existsSync(target) && fs.lstatSync(target).isDirectory();
3564
+ // A repo folder: validate each layer that applies to it (its own or the
3565
+ // held manifest, then manifests/<repo>.override.yml, then the merge).
3566
+ if (isDir) {
3567
+ const layered = validateManifestLayers(nodePath.resolve(target));
3568
+ if (layered.layers.length > 0) {
3569
+ console.log();
3570
+ for (const l of layered.layers)
3571
+ printValidation(
3572
+ chalk.bold(l.file ?? "merged"),
3573
+ l.result,
3574
+ l.layer === "merged" ? "merged" : l.layer,
3575
+ );
3576
+ console.log();
3577
+ if (!layered.valid) process.exit(1);
3578
+ return;
3579
+ }
3580
+ }
3581
+ const filePath = isDir
3582
+ ? nodePath.join(target, `${product().manifestYml}`)
3422
3583
  : target;
3423
3584
 
3424
3585
  if (!fs.existsSync(filePath)) {
@@ -3434,42 +3595,7 @@ class DenCLI {
3434
3595
  );
3435
3596
 
3436
3597
  console.log();
3437
- if (result.valid && result.warnings.length === 0) {
3438
- console.log(
3439
- chalk.green(` ✓ ${label}`) +
3440
- chalk.gray(` (v${result.version}, no issues)`),
3441
- );
3442
- } else if (result.valid) {
3443
- console.log(
3444
- chalk.yellow(` ⚠ ${label}`) +
3445
- chalk.gray(
3446
- ` (v${result.version}, ${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"})`,
3447
- ),
3448
- );
3449
- } else {
3450
- console.log(
3451
- chalk.red(` ✗ ${label}`) +
3452
- chalk.gray(
3453
- ` (v${result.version}, ${result.errors.length} error${result.errors.length === 1 ? "" : "s"}${result.warnings.length > 0 ? `, ${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"}` : ""})`,
3454
- ),
3455
- );
3456
- }
3457
-
3458
- for (const err of result.errors) {
3459
- const path = err.path ? chalk.gray(err.path) + " " : "";
3460
- console.log(` ${chalk.red("ERROR")} ${path}${err.message}`);
3461
- if (err.suggestion) {
3462
- console.log(` ${chalk.gray("→ " + err.suggestion)}`);
3463
- }
3464
- }
3465
- for (const warn of result.warnings) {
3466
- const path = warn.path ? chalk.gray(warn.path) + " " : "";
3467
- console.log(` ${chalk.yellow("WARNING")} ${path}${warn.message}`);
3468
- if (warn.suggestion) {
3469
- console.log(` ${chalk.gray("→ " + warn.suggestion)}`);
3470
- }
3471
- }
3472
-
3598
+ printValidation(label, result);
3473
3599
  console.log();
3474
3600
  if (!result.valid) process.exit(1);
3475
3601
  }
@@ -3559,6 +3685,9 @@ class DenCLI {
3559
3685
  console.log(
3560
3686
  " create <name> [dir] Scaffold a new distribution of your own (--title, --cli, --env-prefix)",
3561
3687
  );
3688
+ console.log(
3689
+ " mcp [--port <n>] MCP server (stdio) for agents, over the running web server",
3690
+ );
3562
3691
  console.log();
3563
3692
  console.log(" workspace list List workspaces");
3564
3693
  console.log(