@monster-orange/den 0.2.8 → 0.4.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 (158) hide show
  1. package/CHANGELOG.md +1731 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +730 -6
  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 +12 -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/agents/default-pack/be-dev.md +20 -0
  22. package/src/shared/agents/default-pack/devops.md +14 -0
  23. package/src/shared/agents/default-pack/fe-dev.md +20 -0
  24. package/src/shared/agents/default-pack/orchestrator.md +16 -0
  25. package/src/shared/agents/default-pack/planner.md +17 -0
  26. package/src/shared/agents/default-pack/reviewer.md +14 -0
  27. package/src/shared/agents/default-pack/ux-designer.md +15 -0
  28. package/src/shared/agents/den-claude.ts +173 -0
  29. package/src/shared/agents/generic.ts +52 -0
  30. package/src/shared/agents/personas.ts +218 -0
  31. package/src/shared/agents/records.ts +118 -0
  32. package/src/shared/agents/runtime.ts +192 -0
  33. package/src/shared/agents/runtimes.ts +47 -0
  34. package/src/shared/auth/headless-login.ts +1 -1
  35. package/src/shared/auth/sudo-vault.ts +93 -31
  36. package/src/shared/auth/token-providers/index.ts +7 -3
  37. package/src/shared/brand.ts +7 -0
  38. package/src/shared/config-error.ts +33 -3
  39. package/src/shared/constants.ts +3 -24
  40. package/src/shared/den-config.ts +257 -5
  41. package/src/shared/edge/config.ts +147 -21
  42. package/src/shared/edge/exec.ts +31 -0
  43. package/src/shared/edge/included.ts +169 -0
  44. package/src/shared/edge/render.ts +69 -14
  45. package/src/shared/edge/runtime.ts +128 -121
  46. package/src/shared/edge/status.ts +68 -8
  47. package/src/shared/edge/tls.ts +328 -0
  48. package/src/shared/edge/types.ts +25 -9
  49. package/src/shared/engine.ts +23 -0
  50. package/src/shared/environment-manager/default-workspace.ts +16 -1
  51. package/src/shared/environment-manager/health.ts +73 -38
  52. package/src/shared/environment-manager/scope.ts +168 -0
  53. package/src/shared/environment-manager/service-runner.ts +789 -43
  54. package/src/shared/environment-manager/up-targets.ts +132 -0
  55. package/src/shared/environment-manager/workspaces.ts +19 -0
  56. package/src/shared/health/rollup.ts +48 -0
  57. package/src/shared/open-url.ts +21 -2
  58. package/src/shared/org-sources.ts +78 -59
  59. package/src/shared/org.ts +53 -5
  60. package/src/shared/plugins/manifest.ts +406 -0
  61. package/src/shared/plugins/store.ts +358 -0
  62. package/src/shared/process/legacy-adopt.ts +285 -0
  63. package/src/shared/process/process-group.ts +202 -0
  64. package/src/shared/process/spawn-streaming.ts +7 -0
  65. package/src/shared/secret-env.ts +70 -0
  66. package/src/shared/secrets/doppler.ts +1 -1
  67. package/src/shared/secrets/index.ts +279 -50
  68. package/src/shared/secrets/none.ts +56 -0
  69. package/src/shared/secrets/strategies.ts +202 -0
  70. package/src/shared/secrets/types.ts +1 -1
  71. package/src/shared/sessions/client.ts +277 -0
  72. package/src/shared/sessions/den-pty.ts +97 -0
  73. package/src/shared/sessions/env.ts +79 -0
  74. package/src/shared/sessions/host.ts +667 -0
  75. package/src/shared/sessions/names.ts +85 -0
  76. package/src/shared/sessions/paths.ts +27 -0
  77. package/src/shared/sessions/protocol.ts +72 -0
  78. package/src/shared/sessions/pty.ts +141 -0
  79. package/src/shared/sessions/ring-buffer.ts +160 -0
  80. package/src/shared/sessions/types.ts +81 -0
  81. package/src/shared/smoke/git-remote.ts +40 -0
  82. package/src/shared/stacks/manifest-commands.ts +1 -1
  83. package/src/shared/stacks/namespace.ts +178 -0
  84. package/src/shared/stacks/owners.ts +303 -0
  85. package/src/shared/stacks/render.ts +8 -3
  86. package/src/shared/stacks/runtime.ts +100 -16
  87. package/src/shared/stacks/status.ts +63 -12
  88. package/src/shared/strategies/registry.ts +89 -0
  89. package/src/shared/theme/contract.ts +538 -0
  90. package/src/shared/types/index.ts +12 -3
  91. package/src/ui/cli/agents-command.ts +812 -0
  92. package/src/ui/cli/create.ts +24 -7
  93. package/src/ui/cli/doctor-command.ts +260 -0
  94. package/src/ui/cli/edge-command.ts +56 -10
  95. package/src/ui/cli/env-probe.ts +266 -0
  96. package/src/ui/cli/flag-parser.ts +18 -5
  97. package/src/ui/cli/health-display.ts +25 -0
  98. package/src/ui/cli/index.ts +580 -60
  99. package/src/ui/cli/notice.ts +20 -0
  100. package/src/ui/cli/plugin-command.ts +849 -0
  101. package/src/ui/cli/sessions-command.ts +718 -0
  102. package/src/ui/cli/stack-command.ts +63 -2
  103. package/src/ui/cli/test-runner.ts +11 -9
  104. package/src/ui/cli/theme-command.ts +136 -0
  105. package/src/ui/cli/web-command.ts +47 -23
  106. package/src/ui/mcp/api.ts +28 -2
  107. package/src/ui/mcp/scope.ts +145 -20
  108. package/src/ui/mcp/server.ts +27 -2
  109. package/src/ui/mcp/tools.ts +94 -12
  110. package/src/ui/server/appearance-prefs.ts +95 -0
  111. package/src/ui/server/audit.ts +35 -0
  112. package/src/ui/server/daemon-reaper.ts +29 -1
  113. package/src/ui/server/engine-stamp.ts +83 -0
  114. package/src/ui/server/github-service.ts +23 -44
  115. package/src/ui/server/graph-page.ts +20 -0
  116. package/src/ui/server/graph-prefs.ts +44 -1
  117. package/src/ui/server/http-errors.ts +293 -0
  118. package/src/ui/server/http-helpers.ts +22 -3
  119. package/src/ui/server/index.ts +539 -51
  120. package/src/ui/server/public/appearance.js +531 -0
  121. package/src/ui/server/public/edge-page.js +248 -0
  122. package/src/ui/server/public/edge.html +251 -0
  123. package/src/ui/server/public/environments.html +65 -83
  124. package/src/ui/server/public/error-page.js +247 -0
  125. package/src/ui/server/public/error.html +260 -0
  126. package/src/ui/server/public/graph-model.js +332 -14
  127. package/src/ui/server/public/graph.css +141 -47
  128. package/src/ui/server/public/graph.js +223 -44
  129. package/src/ui/server/public/header.js +396 -600
  130. package/src/ui/server/public/health-fit.js +248 -0
  131. package/src/ui/server/public/health-rollup.js +128 -0
  132. package/src/ui/server/public/i18n/es.js +287 -0
  133. package/src/ui/server/public/i18n/ja.js +276 -0
  134. package/src/ui/server/public/index.html +139 -438
  135. package/src/ui/server/public/local-dev.html +3468 -4044
  136. package/src/ui/server/public/logs.html +71 -208
  137. package/src/ui/server/public/panel.js +14 -2
  138. package/src/ui/server/public/release.html +175 -389
  139. package/src/ui/server/public/services.html +596 -770
  140. package/src/ui/server/public/settings-page.js +759 -0
  141. package/src/ui/server/public/settings-registry.js +521 -0
  142. package/src/ui/server/public/settings.html +590 -0
  143. package/src/ui/server/public/shell.js +3869 -0
  144. package/src/ui/server/public/topbar.js +1413 -0
  145. package/src/ui/server/public/version-label.js +12 -0
  146. package/src/ui/server/public/workspace-picker.js +74 -34
  147. package/src/ui/server/readonly-routes.ts +37 -3
  148. package/src/ui/server/services-list.ts +94 -17
  149. package/src/ui/server/settings-distribution.ts +74 -0
  150. package/src/ui/server/shell-prefs.ts +192 -0
  151. package/src/ui/server/theme-routes.ts +292 -0
  152. package/src/ui/server/theme-runtime.ts +407 -0
  153. package/src/ui/server/ui-assets.ts +16 -1
  154. package/src/ui/server/ui-language.ts +50 -0
  155. package/src/ui/server/workspace-routes.ts +48 -18
  156. package/themes/den/den-theme.json +72 -0
  157. package/themes/den/favicon.ico +0 -0
  158. package/themes/den/mark.svg +6 -0
@@ -146,17 +146,26 @@ export function renderDistribution(
146
146
  ...bannerLines(title).map((l) => ` - "${l}"`),
147
147
  "",
148
148
  "# The GitHub org (or user) that owns the repos Den clones and lists.",
149
- "github_org: your-github-org",
149
+ "# Unset, Den lists no GitHub repos (and never calls gh for them). E.g.",
150
+ "# github_org: example-org",
150
151
  "",
151
- "# Secrets backend: env reads plain environment variables; doppler and",
152
- "# 1password are the others.",
152
+ "# Secrets backend: none (this distribution needs no secrets store), or",
153
+ "# env (plain environment variables), doppler or 1password. A backend's",
154
+ "# refs say where Den's own features find their secrets, e.g.",
155
+ `# refs: { sudo: { key: ${prefix}_LOCAL_ADMIN_PWD } }`,
153
156
  "secrets:",
154
- " provider: env",
157
+ " provider: none",
155
158
  "",
156
- "# This instance's HTTPS edge: https://<service>.<domain>.",
159
+ "# This instance's edge: <service>.<domain>, one Traefik for its routes.",
157
160
  "edge:",
158
161
  ` org: ${name}`,
159
162
  ` domain: ${name}.localhost`,
163
+ " # TLS: none serves http only. For https, pick a strategy:",
164
+ " # tls: { strategy: mkcert } # a local CA, per machine",
165
+ " # tls: { strategy: mkcert, ca: ~/path/to/ca } # an existing mkcert CA",
166
+ " # tls: { strategy: files, include: edge/dynamic, mounts: { <host>: <container> } }",
167
+ " # # your own certs, in included Traefik config",
168
+ " tls: none",
160
169
  "",
161
170
  "environments: [local]",
162
171
  "",
@@ -344,13 +353,21 @@ export function runCreate(argv: string[], cwd: string): number {
344
353
  denVersion: runningDenVersion(),
345
354
  });
346
355
  const cli = args.cli ?? args.name;
347
- const rel = path.relative(cwd, args.dir) || ".";
356
+ // Relative only when the new directory is under the caller's cwd; a
357
+ // path that climbs out (../../..) is harder to read than the absolute one.
358
+ const rel = path.relative(cwd, args.dir);
359
+ const cdPath =
360
+ rel === ""
361
+ ? "."
362
+ : rel.startsWith("..") || path.isAbsolute(rel)
363
+ ? args.dir
364
+ : rel;
348
365
  console.log(
349
366
  `Created ${args.name} in ${args.dir} (${plural(written.length, "file")}).`,
350
367
  );
351
368
  console.log("");
352
369
  console.log("Next:");
353
- console.log(` cd ${rel}`);
370
+ console.log(` cd ${cdPath}`);
354
371
  console.log(" npm install");
355
372
  console.log(` ./bin/${cli} --help`);
356
373
  return 0;
@@ -0,0 +1,260 @@
1
+ /**
2
+ * `doctor`: every strategy kind in the registry (secrets, tls,
3
+ * session-backend, runtime), the strategy this distribution selects for it, whether
4
+ * nothing chose it (implicit), and its detect() result with the reason.
5
+ *
6
+ * Read-only: detect() is a PATH check (session-backend loads the optional
7
+ * PTY module, which runs nothing; runtime reads a CLI's version from its
8
+ * install, never by running it), so doctor never spawns a backend CLI,
9
+ * never prompts, and never prints a secret value. Exit 1 when a strategy
10
+ * that will be used is unavailable or its config is invalid. When agents
11
+ * are configured, a note says to run `<cli> doctor --probe`.
12
+ *
13
+ * `--probe` (opt-in, AG spec §5a-env) is the one exception to never
14
+ * spawning: it runs a short probe session through the session host for a
15
+ * plain session and each available runtime's agent env (env-probe.ts), and
16
+ * exits 1 when a runtime's scrubEnv name or a credential-shaped variable
17
+ * reaches one. Without the PTY module the probe is unavailable, not failed.
18
+ */
19
+ import chalk from "chalk";
20
+ import { readDenConfig, type DenConfig } from "../../shared/den-config.js";
21
+ import { getStateDir } from "../../shared/paths.js";
22
+ import { product } from "../../shared/product.js";
23
+ import { listRecords } from "../../shared/agents/records.js";
24
+ import { readBrand } from "../../shared/brand.js";
25
+ import { hasEdgeBlock, readEdgeConfig } from "../../shared/edge/config.js";
26
+ import { edgeTlsWarnings, tlsStrategyFor } from "../../shared/edge/tls.js";
27
+ import {
28
+ readSecretsConfig,
29
+ secretsStatus,
30
+ } from "../../shared/secrets/index.js";
31
+ import {
32
+ DEN_PTY_ID,
33
+ SESSION_BACKEND_KIND,
34
+ } from "../../shared/sessions/den-pty.js";
35
+ import {
36
+ getStrategy,
37
+ strategyKinds,
38
+ } from "../../shared/strategies/registry.js";
39
+ import {
40
+ DEFAULT_RUNTIME_ID,
41
+ RUNTIME_KIND,
42
+ } from "../../shared/agents/runtimes.js";
43
+
44
+ export interface DoctorRow {
45
+ kind: string;
46
+ /** The selected strategy id ("" when the config could not be read). */
47
+ selected: string;
48
+ implicit: boolean;
49
+ available: boolean;
50
+ reason?: string;
51
+ /** True when it will be used and cannot work: doctor exits 1. */
52
+ problem: boolean;
53
+ warnings: string[];
54
+ }
55
+
56
+ type Selector = (env: NodeJS.ProcessEnv) => DoctorRow;
57
+
58
+ const SELECTORS: Record<string, Selector> = {
59
+ secrets(env) {
60
+ const st = secretsStatus(readSecretsConfig(), env);
61
+ return {
62
+ kind: "secrets",
63
+ selected: st.backend,
64
+ implicit: st.implicit,
65
+ available: st.detect.available,
66
+ reason: st.detect.reason,
67
+ // The implicit default is used only when a secret is asked for.
68
+ problem: !st.detect.available && !st.implicit,
69
+ warnings: [],
70
+ };
71
+ },
72
+ tls(env) {
73
+ if (!hasEdgeBlock())
74
+ return {
75
+ kind: "tls",
76
+ selected: "none",
77
+ implicit: false,
78
+ available: true,
79
+ reason: "no edge: block",
80
+ problem: false,
81
+ warnings: [],
82
+ };
83
+ try {
84
+ const cfg = readEdgeConfig();
85
+ const d = tlsStrategyFor(cfg).detect(env);
86
+ return {
87
+ kind: "tls",
88
+ selected: cfg.tls.strategy,
89
+ implicit: cfg.tlsFrom === "implicit",
90
+ available: d.available,
91
+ reason: d.reason,
92
+ problem: !d.available,
93
+ warnings: edgeTlsWarnings(cfg),
94
+ };
95
+ } catch (err) {
96
+ return {
97
+ kind: "tls",
98
+ selected: "",
99
+ implicit: false,
100
+ available: false,
101
+ reason: (err as Error).message,
102
+ problem: true,
103
+ warnings: [],
104
+ };
105
+ }
106
+ },
107
+ // Agents' terminals (AG-3a). Only den-pty exists, so it is the implicit
108
+ // choice; unavailable is never a problem (nothing needs a session until
109
+ // one is started, and `sessions start` says why).
110
+ [SESSION_BACKEND_KIND](env) {
111
+ const d = getStrategy(SESSION_BACKEND_KIND, DEN_PTY_ID)?.detect(env) ?? {
112
+ available: false,
113
+ reason: "not registered",
114
+ };
115
+ return {
116
+ kind: SESSION_BACKEND_KIND,
117
+ selected: DEN_PTY_ID,
118
+ implicit: true,
119
+ available: d.available,
120
+ reason: d.reason,
121
+ problem: false,
122
+ warnings: [],
123
+ };
124
+ },
125
+ // Agents' CLIs (AG-2a): the default runtime, chosen per persona or agent,
126
+ // so it is implicit and its absence is never a problem (`agents start`
127
+ // says why it can't run).
128
+ [RUNTIME_KIND](env) {
129
+ const d = getStrategy(RUNTIME_KIND, DEFAULT_RUNTIME_ID)?.detect(env) ?? {
130
+ available: false,
131
+ reason: "not registered",
132
+ };
133
+ return {
134
+ kind: RUNTIME_KIND,
135
+ selected: DEFAULT_RUNTIME_ID,
136
+ implicit: true,
137
+ available: d.available,
138
+ reason: d.reason,
139
+ problem: false,
140
+ warnings: [],
141
+ };
142
+ },
143
+ };
144
+
145
+ /**
146
+ * One row per registered kind (secrets, then tls, then any other kind in
147
+ * registration order). Never spawns, never throws.
148
+ */
149
+ export function doctorRows(env: NodeJS.ProcessEnv = process.env): DoctorRow[] {
150
+ const kinds = strategyKinds();
151
+ const ordered = [
152
+ ...Object.keys(SELECTORS).filter((k) => kinds.includes(k)),
153
+ ...kinds.filter((k) => !(k in SELECTORS)),
154
+ ];
155
+ return ordered.map(
156
+ (kind) =>
157
+ SELECTORS[kind]?.(env) ?? {
158
+ kind,
159
+ selected: "",
160
+ implicit: false,
161
+ available: false,
162
+ reason: "no selector for this kind",
163
+ problem: false,
164
+ warnings: [],
165
+ },
166
+ );
167
+ }
168
+
169
+ export function formatDoctor(rows: DoctorRow[], title: string): string[] {
170
+ const lines = [chalk.bold(`${title} doctor: strategies`), ""];
171
+ const width = Math.max(...rows.map((r) => r.kind.length));
172
+ for (const r of rows) {
173
+ const sel = r.selected
174
+ ? `${r.selected}${r.implicit ? " (implicit)" : ""}`
175
+ : "(invalid config)";
176
+ const state = r.available
177
+ ? chalk.green("✓ available")
178
+ : (r.problem ? chalk.red : chalk.yellow)("✗ unavailable");
179
+ lines.push(
180
+ ` ${r.kind.padEnd(width)} ${sel.padEnd(20)} ${state}${r.reason ? `: ${r.reason}` : ""}`,
181
+ );
182
+ for (const w of r.warnings) lines.push(chalk.yellow(` ⚠ ${w}`));
183
+ }
184
+ return lines;
185
+ }
186
+
187
+ /**
188
+ * Whether agents are configured here: an `agents:` block with anything in
189
+ * it (max_concurrent, persona_packs, an agent id), or an agent record.
190
+ */
191
+ export function agentsConfigured(
192
+ config: DenConfig | null,
193
+ stateDir: string,
194
+ ): boolean {
195
+ const raw = config?.agents as unknown;
196
+ if (raw && typeof raw === "object" && Object.keys(raw).length) return true;
197
+ try {
198
+ return listRecords(stateDir).length > 0;
199
+ } catch {
200
+ return false;
201
+ }
202
+ }
203
+
204
+ /** Informational lines after the rows: never a failure. */
205
+ export function doctorNotes(cli: string, configured: boolean): string[] {
206
+ return configured
207
+ ? [
208
+ "",
209
+ ` ℹ agents are configured or have run here: run \`${cli} doctor --probe\` to verify agent env isolation`,
210
+ ]
211
+ : [];
212
+ }
213
+
214
+ function readConfigOrNull(): DenConfig | null {
215
+ try {
216
+ return readDenConfig();
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ export async function cmdDoctor(args: string[]): Promise<number> {
223
+ const probe = args.includes("--probe");
224
+ const unknown = args.filter((a) => a !== "--probe");
225
+ if (unknown.length) {
226
+ console.error(
227
+ `✗ doctor: unknown argument ${unknown[0]} (doctor [--probe])`,
228
+ );
229
+ return 2;
230
+ }
231
+ const rows = doctorRows();
232
+ for (const l of formatDoctor(rows, readBrand().name)) console.log(l);
233
+ const cli = product().cli;
234
+ if (!probe) {
235
+ for (const l of doctorNotes(
236
+ cli,
237
+ agentsConfigured(readConfigOrNull(), getStateDir()),
238
+ ))
239
+ console.log(l);
240
+ return rows.some((r) => r.problem) ? 1 : 0;
241
+ }
242
+ const { runEnvProbe, formatProbe } = await import("./env-probe.js");
243
+ const { resolveDeps, plainSessionEnv } =
244
+ await import("./sessions-command.js");
245
+ const d = resolveDeps({});
246
+ const results = await runEnvProbe({
247
+ dist: d.dist,
248
+ cli: d.cli,
249
+ env: d.env,
250
+ pty: d.pty,
251
+ connect: d.connect,
252
+ plainEnv: (caller) => plainSessionEnv(caller),
253
+ });
254
+ console.log("");
255
+ console.log(chalk.bold(`${readBrand().name} doctor: session env probe`));
256
+ console.log("");
257
+ for (const l of formatProbe(results)) console.log(l);
258
+ const found = results.some((r) => r.findings.length);
259
+ return rows.some((r) => r.problem) || found ? 1 : 0;
260
+ }
@@ -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
  }