@monster-orange/den 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +1547 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +507 -4
  5. package/make/den.mk +19 -4
  6. package/make/journeys.mk +2 -1
  7. package/make/local_dev.mk +6 -13
  8. package/make/secrets-1password.mk +18 -0
  9. package/make/test.mk +1 -1
  10. package/package.json +9 -2
  11. package/schemas/den.plugin.v1.json +156 -0
  12. package/schemas/den.theme.v1.json +269 -0
  13. package/scripts/auth-configure.ts +16 -8
  14. package/scripts/heal-config.ts +0 -5
  15. package/scripts/setup-1password.ts +41 -19
  16. package/scripts/setup-secrets.ts +67 -101
  17. package/src/pillars/api/global-setup.ts +1 -1
  18. package/src/pillars/journeys/sandbox.ts +23 -18
  19. package/src/pillars/journeys/tools/browser-session.ts +2 -2
  20. package/src/scripts/setup-default-workspace.ts +2 -2
  21. package/src/shared/auth/headless-login.ts +1 -1
  22. package/src/shared/auth/sudo-vault.ts +93 -31
  23. package/src/shared/auth/token-providers/index.ts +7 -3
  24. package/src/shared/brand.ts +7 -0
  25. package/src/shared/config-error.ts +33 -3
  26. package/src/shared/constants.ts +3 -24
  27. package/src/shared/den-config.ts +141 -5
  28. package/src/shared/edge/config.ts +147 -21
  29. package/src/shared/edge/exec.ts +31 -0
  30. package/src/shared/edge/included.ts +169 -0
  31. package/src/shared/edge/render.ts +69 -14
  32. package/src/shared/edge/runtime.ts +128 -121
  33. package/src/shared/edge/status.ts +68 -8
  34. package/src/shared/edge/tls.ts +328 -0
  35. package/src/shared/edge/types.ts +25 -9
  36. package/src/shared/environment-manager/default-workspace.ts +16 -1
  37. package/src/shared/environment-manager/health.ts +73 -38
  38. package/src/shared/environment-manager/scope.ts +168 -0
  39. package/src/shared/environment-manager/service-runner.ts +789 -43
  40. package/src/shared/environment-manager/up-targets.ts +132 -0
  41. package/src/shared/health/rollup.ts +48 -0
  42. package/src/shared/open-url.ts +21 -2
  43. package/src/shared/org-sources.ts +78 -59
  44. package/src/shared/org.ts +53 -5
  45. package/src/shared/plugins/manifest.ts +406 -0
  46. package/src/shared/plugins/store.ts +358 -0
  47. package/src/shared/process/legacy-adopt.ts +285 -0
  48. package/src/shared/process/process-group.ts +202 -0
  49. package/src/shared/process/spawn-streaming.ts +7 -0
  50. package/src/shared/secrets/doppler.ts +1 -1
  51. package/src/shared/secrets/index.ts +279 -50
  52. package/src/shared/secrets/none.ts +56 -0
  53. package/src/shared/secrets/strategies.ts +202 -0
  54. package/src/shared/secrets/types.ts +1 -1
  55. package/src/shared/smoke/git-remote.ts +40 -0
  56. package/src/shared/stacks/manifest-commands.ts +1 -1
  57. package/src/shared/stacks/namespace.ts +178 -0
  58. package/src/shared/stacks/owners.ts +303 -0
  59. package/src/shared/stacks/render.ts +8 -3
  60. package/src/shared/stacks/runtime.ts +100 -16
  61. package/src/shared/stacks/status.ts +63 -12
  62. package/src/shared/strategies/registry.ts +89 -0
  63. package/src/shared/theme/contract.ts +538 -0
  64. package/src/shared/types/index.ts +12 -3
  65. package/src/ui/cli/create.ts +24 -7
  66. package/src/ui/cli/doctor-command.ts +131 -0
  67. package/src/ui/cli/edge-command.ts +56 -10
  68. package/src/ui/cli/flag-parser.ts +7 -2
  69. package/src/ui/cli/health-display.ts +25 -0
  70. package/src/ui/cli/index.ts +548 -60
  71. package/src/ui/cli/notice.ts +20 -0
  72. package/src/ui/cli/plugin-command.ts +849 -0
  73. package/src/ui/cli/stack-command.ts +63 -2
  74. package/src/ui/cli/test-runner.ts +11 -9
  75. package/src/ui/cli/theme-command.ts +136 -0
  76. package/src/ui/cli/web-command.ts +47 -23
  77. package/src/ui/mcp/api.ts +28 -2
  78. package/src/ui/mcp/scope.ts +22 -1
  79. package/src/ui/mcp/tools.ts +50 -7
  80. package/src/ui/server/appearance-prefs.ts +95 -0
  81. package/src/ui/server/daemon-reaper.ts +29 -1
  82. package/src/ui/server/engine-stamp.ts +83 -0
  83. package/src/ui/server/github-service.ts +23 -44
  84. package/src/ui/server/graph-page.ts +20 -0
  85. package/src/ui/server/graph-prefs.ts +44 -1
  86. package/src/ui/server/http-errors.ts +293 -0
  87. package/src/ui/server/http-helpers.ts +22 -3
  88. package/src/ui/server/index.ts +532 -51
  89. package/src/ui/server/public/appearance.js +531 -0
  90. package/src/ui/server/public/edge-page.js +248 -0
  91. package/src/ui/server/public/edge.html +251 -0
  92. package/src/ui/server/public/environments.html +65 -83
  93. package/src/ui/server/public/error-page.js +247 -0
  94. package/src/ui/server/public/error.html +260 -0
  95. package/src/ui/server/public/graph-model.js +332 -14
  96. package/src/ui/server/public/graph.css +141 -47
  97. package/src/ui/server/public/graph.js +223 -44
  98. package/src/ui/server/public/header.js +396 -600
  99. package/src/ui/server/public/health-fit.js +248 -0
  100. package/src/ui/server/public/health-rollup.js +128 -0
  101. package/src/ui/server/public/i18n/es.js +287 -0
  102. package/src/ui/server/public/i18n/ja.js +276 -0
  103. package/src/ui/server/public/index.html +139 -438
  104. package/src/ui/server/public/local-dev.html +3468 -4044
  105. package/src/ui/server/public/logs.html +71 -208
  106. package/src/ui/server/public/panel.js +14 -2
  107. package/src/ui/server/public/release.html +175 -389
  108. package/src/ui/server/public/services.html +596 -770
  109. package/src/ui/server/public/settings-page.js +759 -0
  110. package/src/ui/server/public/settings-registry.js +521 -0
  111. package/src/ui/server/public/settings.html +590 -0
  112. package/src/ui/server/public/shell.js +3869 -0
  113. package/src/ui/server/public/topbar.js +1413 -0
  114. package/src/ui/server/public/version-label.js +12 -0
  115. package/src/ui/server/public/workspace-picker.js +74 -34
  116. package/src/ui/server/readonly-routes.ts +12 -1
  117. package/src/ui/server/services-list.ts +94 -17
  118. package/src/ui/server/settings-distribution.ts +74 -0
  119. package/src/ui/server/shell-prefs.ts +192 -0
  120. package/src/ui/server/theme-routes.ts +292 -0
  121. package/src/ui/server/theme-runtime.ts +407 -0
  122. package/src/ui/server/ui-assets.ts +16 -1
  123. package/src/ui/server/ui-language.ts +50 -0
  124. package/src/ui/server/workspace-routes.ts +45 -3
  125. package/themes/den/den-theme.json +72 -0
  126. package/themes/den/favicon.ico +0 -0
  127. package/themes/den/mark.svg +6 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `doctor`: every strategy kind in the registry (secrets, tls), the
3
+ * strategy this distribution selects for it, whether nothing chose it
4
+ * (implicit), and its detect() result with the reason.
5
+ *
6
+ * Read-only: detect() is a PATH check, so doctor never spawns a backend
7
+ * CLI, never prompts, and never prints a secret value. Exit 1 when a
8
+ * strategy that will be used is unavailable or its config is invalid.
9
+ */
10
+ import chalk from "chalk";
11
+ import { readBrand } from "../../shared/brand.js";
12
+ import { hasEdgeBlock, readEdgeConfig } from "../../shared/edge/config.js";
13
+ import { edgeTlsWarnings, tlsStrategyFor } from "../../shared/edge/tls.js";
14
+ import {
15
+ readSecretsConfig,
16
+ secretsStatus,
17
+ } from "../../shared/secrets/index.js";
18
+ import { strategyKinds } from "../../shared/strategies/registry.js";
19
+
20
+ export interface DoctorRow {
21
+ kind: string;
22
+ /** The selected strategy id ("" when the config could not be read). */
23
+ selected: string;
24
+ implicit: boolean;
25
+ available: boolean;
26
+ reason?: string;
27
+ /** True when it will be used and cannot work: doctor exits 1. */
28
+ problem: boolean;
29
+ warnings: string[];
30
+ }
31
+
32
+ type Selector = (env: NodeJS.ProcessEnv) => DoctorRow;
33
+
34
+ const SELECTORS: Record<string, Selector> = {
35
+ secrets(env) {
36
+ const st = secretsStatus(readSecretsConfig(), env);
37
+ return {
38
+ kind: "secrets",
39
+ selected: st.backend,
40
+ implicit: st.implicit,
41
+ available: st.detect.available,
42
+ reason: st.detect.reason,
43
+ // The implicit default is used only when a secret is asked for.
44
+ problem: !st.detect.available && !st.implicit,
45
+ warnings: [],
46
+ };
47
+ },
48
+ tls(env) {
49
+ if (!hasEdgeBlock())
50
+ return {
51
+ kind: "tls",
52
+ selected: "none",
53
+ implicit: false,
54
+ available: true,
55
+ reason: "no edge: block",
56
+ problem: false,
57
+ warnings: [],
58
+ };
59
+ try {
60
+ const cfg = readEdgeConfig();
61
+ const d = tlsStrategyFor(cfg).detect(env);
62
+ return {
63
+ kind: "tls",
64
+ selected: cfg.tls.strategy,
65
+ implicit: cfg.tlsFrom === "implicit",
66
+ available: d.available,
67
+ reason: d.reason,
68
+ problem: !d.available,
69
+ warnings: edgeTlsWarnings(cfg),
70
+ };
71
+ } catch (err) {
72
+ return {
73
+ kind: "tls",
74
+ selected: "",
75
+ implicit: false,
76
+ available: false,
77
+ reason: (err as Error).message,
78
+ problem: true,
79
+ warnings: [],
80
+ };
81
+ }
82
+ },
83
+ };
84
+
85
+ /**
86
+ * One row per registered kind (secrets, then tls, then any other kind in
87
+ * registration order). Never spawns, never throws.
88
+ */
89
+ export function doctorRows(env: NodeJS.ProcessEnv = process.env): DoctorRow[] {
90
+ const kinds = strategyKinds();
91
+ const ordered = [
92
+ ...Object.keys(SELECTORS).filter((k) => kinds.includes(k)),
93
+ ...kinds.filter((k) => !(k in SELECTORS)),
94
+ ];
95
+ return ordered.map(
96
+ (kind) =>
97
+ SELECTORS[kind]?.(env) ?? {
98
+ kind,
99
+ selected: "",
100
+ implicit: false,
101
+ available: false,
102
+ reason: "no selector for this kind",
103
+ problem: false,
104
+ warnings: [],
105
+ },
106
+ );
107
+ }
108
+
109
+ export function formatDoctor(rows: DoctorRow[], title: string): string[] {
110
+ const lines = [chalk.bold(`${title} doctor: strategies`), ""];
111
+ const width = Math.max(...rows.map((r) => r.kind.length));
112
+ for (const r of rows) {
113
+ const sel = r.selected
114
+ ? `${r.selected}${r.implicit ? " (implicit)" : ""}`
115
+ : "(invalid config)";
116
+ const state = r.available
117
+ ? chalk.green("✓ available")
118
+ : (r.problem ? chalk.red : chalk.yellow)("✗ unavailable");
119
+ lines.push(
120
+ ` ${r.kind.padEnd(width)} ${sel.padEnd(20)} ${state}${r.reason ? `: ${r.reason}` : ""}`,
121
+ );
122
+ for (const w of r.warnings) lines.push(chalk.yellow(` ⚠ ${w}`));
123
+ }
124
+ return lines;
125
+ }
126
+
127
+ export function cmdDoctor(_args: string[]): number {
128
+ const rows = doctorRows();
129
+ for (const l of formatDoctor(rows, readBrand().name)) console.log(l);
130
+ return rows.some((r) => r.problem) ? 1 : 0;
131
+ }
@@ -1,7 +1,11 @@
1
1
  /**
2
- * `edge up | down | status | trust | untrust | routes`
2
+ * `edge up [--take-over] | down | status | trust | untrust | routes`
3
3
  *
4
- * This org's HTTPS edge: own CA + cert, routes at `<name>.<edge.domain>`.
4
+ * This org's edge: routes at `<name>.<edge.domain>`, TLS as `edge.tls` says
5
+ * (none, mkcert or files). `trust` / `untrust` exist only under mkcert.
6
+ * `routes` lists Den's own routes, then the routers in the included
7
+ * dynamic files, each with its source file, then the stacks' routers
8
+ * (`stack-*.yml`, 0.3.0-dev.32), each marked "stack" with its file.
5
9
  */
6
10
  import chalk from "chalk";
7
11
  import { readBrand } from "../../shared/brand.js";
@@ -15,18 +19,37 @@ import {
15
19
  trustCa,
16
20
  untrustCa,
17
21
  } from "../../shared/edge/runtime.js";
18
- import { edgeStatus } from "../../shared/edge/status.js";
22
+ import {
23
+ includedRouters,
24
+ stackRouters,
25
+ routerUrls,
26
+ type IncludedRouter,
27
+ } from "../../shared/edge/included.js";
28
+ import { edgeRoutes, edgeStatus } from "../../shared/edge/status.js";
29
+ import { tlsYaml } from "../../shared/edge/tls.js";
30
+ import { rejoinRunningStacks } from "../../shared/stacks/status.js";
31
+
32
+ /** An included router's URLs (the hosts its rule names), else its name. */
33
+ function includedUrl(r: IncludedRouter): string {
34
+ return r.hosts.length ? routerUrls(r).join(", ") : r.name;
35
+ }
19
36
 
20
37
  export async function cmdEdge(args: string[]): Promise<void> {
21
38
  const sub = args[0] ?? "status";
22
39
  const cfg = readEdgeConfig();
23
40
  const n = edgeNames(cfg.org);
24
41
  const published = cfg.ports
25
- ? `host :${cfg.ports.http} / :${cfg.ports.https}`
42
+ ? cfg.tls.strategy === "none"
43
+ ? `host :${cfg.ports.http} (http only)`
44
+ : `host :${cfg.ports.http} / :${cfg.ports.https}`
26
45
  : `not published (reach it on Docker network ${n.network})`;
27
46
  switch (sub) {
28
47
  case "up": {
29
- const { certs } = await edgeUp(cfg);
48
+ // --take-over: the only way to replace an edge another state dir
49
+ // owns (0.3.0-dev.40); never implied.
50
+ const { certs } = await edgeUp(cfg, {
51
+ takeOver: args.includes("--take-over"),
52
+ });
30
53
  console.log(
31
54
  chalk.green(
32
55
  `✓ ${n.container} up — ${edgeDomains(cfg)
@@ -37,7 +60,7 @@ export async function cmdEdge(args: string[]): Promise<void> {
37
60
  console.log(` ports ${published}`);
38
61
  if (certs.issued)
39
62
  console.log(` cert issued for ${certs.names.join(", ")}`);
40
- if (caTrusted(cfg) === false)
63
+ if (cfg.tls.strategy === "mkcert" && caTrusted(cfg) === false)
41
64
  console.log(
42
65
  chalk.yellow(
43
66
  " CA not trusted yet — run `edge trust` (asks for your password once)",
@@ -45,6 +68,8 @@ export async function cmdEdge(args: string[]): Promise<void> {
45
68
  );
46
69
  for (const name of ["edge", ...Object.keys(cfg.routes)])
47
70
  console.log(` ${routeUrl(name, cfg)}`);
71
+ // Running stacks' routes stay up: the edge rejoins their networks.
72
+ rejoinRunningStacks();
48
73
  return;
49
74
  }
50
75
  case "down":
@@ -57,10 +82,25 @@ export async function cmdEdge(args: string[]): Promise<void> {
57
82
  case "untrust":
58
83
  process.exitCode = untrustCa(cfg);
59
84
  return;
60
- case "routes":
61
- for (const r of edgeStatus(cfg).routes)
85
+ case "routes": {
86
+ for (const r of edgeRoutes(cfg))
62
87
  console.log(`${r.url.padEnd(52)} → ${r.upstream}`);
88
+ // Routers in the included dynamic files: Traefik serves them too.
89
+ const inc = includedRouters(cfg);
90
+ for (const r of inc.routers)
91
+ console.log(
92
+ `${includedUrl(r).padEnd(52)} → ${r.upstream || "?"} (${r.file})`,
93
+ );
94
+ // Routers the stacks wrote (stack up): neither Den's nor included.
95
+ const stk = stackRouters(cfg);
96
+ for (const r of stk.routers)
97
+ console.log(
98
+ `${includedUrl(r).padEnd(52)} → ${r.upstream || "?"} (stack: ${r.file})`,
99
+ );
100
+ for (const e of [...inc.errors, ...stk.errors])
101
+ console.log(chalk.yellow(`${e.file}: not read (${e.error})`));
63
102
  return;
103
+ }
64
104
  case "status": {
65
105
  const st = edgeStatus(cfg);
66
106
  console.log(chalk.bold(`${readBrand().name} edge — ${st.org}`));
@@ -68,14 +108,20 @@ export async function cmdEdge(args: string[]): Promise<void> {
68
108
  console.log(` ports ${published}`);
69
109
  console.log(` domains ${st.domains.join(", ")} (+ every subdomain)`);
70
110
  console.log(
71
- ` CA ${caDir(cfg)} — ${st.caTrusted === null ? "unknown" : st.caTrusted ? "trusted" : "NOT trusted"}`,
111
+ ` tls ${tlsYaml(cfg.tls).replace(/^tls: /, "")}${cfg.tlsFrom === "implicit" ? " (implicit: edge.tls is not set)" : ""}`,
72
112
  );
113
+ if (cfg.tls.strategy !== "none")
114
+ console.log(
115
+ // Fronted (edge.publish: false): the proxy in front owns
116
+ // browser trust (BOOT-1), so this machine's trust is moot.
117
+ ` CA ${caDir(cfg)} — ${!st.ports ? "not needed: fronted" : st.caTrusted === null ? "unknown" : st.caTrusted ? "trusted" : "NOT trusted"}`,
118
+ );
73
119
  if (st.certIssued) console.log(` cert ${st.certNames.join(", ")}`);
74
120
  return;
75
121
  }
76
122
  default:
77
123
  throw new Error(
78
- `Unknown edge command "${sub}". Use: up | down | status | trust | untrust | routes`,
124
+ `Unknown edge command "${sub}". Use: up [--take-over] | down | status | trust | untrust | routes`,
79
125
  );
80
126
  }
81
127
  }
@@ -12,6 +12,7 @@ export type CommandName =
12
12
  | "services"
13
13
  | "graph"
14
14
  | "register"
15
+ | "relocate"
15
16
  | "use-local"
16
17
  | "use-managed"
17
18
  | "setup"
@@ -63,9 +64,10 @@ const WORKSPACE_READ_COMMANDS = new Set([
63
64
 
64
65
  /**
65
66
  * Commands that edit one workspace's entries without changing what runs:
66
- * `-w` picks the workspace to write, active or not, and leaves focus alone.
67
+ * `-w` picks the workspace to write, whether or not it is active, and leaves
68
+ * focus alone.
67
69
  */
68
- const WORKSPACE_EDIT_COMMANDS = new Set(["register"]);
70
+ const WORKSPACE_EDIT_COMMANDS = new Set(["register", "relocate"]);
69
71
 
70
72
  const WORKSPACE_COMMANDS = new Set([
71
73
  ...WORKSPACE_MUTATION_COMMANDS,
@@ -196,6 +198,9 @@ const COMMAND_FLAGS: Record<string, readonly string[]> = {
196
198
  logs: [],
197
199
  setup: ["-f", "--force", "--plan", "-w", "--workspace", "--all-active"],
198
200
  sync: [],
201
+ // The only override for an edge another state dir owns; a typo'd one
202
+ // must not be ignored.
203
+ edge: ["--take-over"],
199
204
  };
200
205
 
201
206
  /**
@@ -2,6 +2,7 @@ import chalk from "chalk";
2
2
  import type { ServiceHealthResult } from "../../shared/environment-manager/health.js";
3
3
 
4
4
  import { product } from "../../shared/product.js";
5
+ import type { RepoRollup } from "../../shared/health/rollup.js";
5
6
  type HealthState = ServiceHealthResult["health"];
6
7
  type HealthReasonInput = Pick<ServiceHealthResult, "health" | "healthDetail">;
7
8
 
@@ -62,3 +63,27 @@ export function formatHealthReason(result: HealthReasonInput): string {
62
63
  const reason = healthReasonText(result);
63
64
  return reason ? chalk.gray(reason.slice(0, 48)) : "";
64
65
  }
66
+
67
+ /**
68
+ * A repo row's rolled-up state (rollupRepoHealth), the one the web UI's
69
+ * header and graph show: "● down", "● degraded", "● ok", "● unknown".
70
+ */
71
+ export function formatRollupLabel(r: RepoRollup): string {
72
+ if (r.state === "down") return chalk.red("● down");
73
+ if (r.state === "degraded") return chalk.yellow("● degraded");
74
+ if (r.state === "ok") return chalk.green("● ok");
75
+ return chalk.gray("● unknown");
76
+ }
77
+
78
+ /**
79
+ * Why, for a repo row: the down services ("mailpit down"), and how many
80
+ * services aren't checked ("25 not checked"). Unchecked never means down.
81
+ */
82
+ export function rollupReasonText(r: RepoRollup): string {
83
+ return [
84
+ r.down.length ? `${r.down.join(", ")} down` : "",
85
+ r.unchecked ? `${r.unchecked} not checked` : "",
86
+ ]
87
+ .filter(Boolean)
88
+ .join("; ");
89
+ }