@monster-orange/den 0.0.0 → 0.2.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 (255) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +68 -0
  5. package/docs/distributions.md +499 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +101 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +403 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +124 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +757 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/agent-actor.ts +77 -0
  91. package/src/shared/applications.ts +138 -0
  92. package/src/shared/auth/api-token.ts +416 -0
  93. package/src/shared/auth/auth0-helper.ts +284 -0
  94. package/src/shared/auth/headless-login.ts +265 -0
  95. package/src/shared/auth/jwt-helper.ts +186 -0
  96. package/src/shared/auth/sudo-vault.ts +69 -0
  97. package/src/shared/auth/test-accounts.ts +98 -0
  98. package/src/shared/auth/token-injection.ts +184 -0
  99. package/src/shared/auth/token-providers/auth0.ts +80 -0
  100. package/src/shared/auth/token-providers/index.ts +182 -0
  101. package/src/shared/auth/token-refresh.ts +273 -0
  102. package/src/shared/banner.ts +15 -0
  103. package/src/shared/brand.ts +79 -0
  104. package/src/shared/cli-tabs.ts +131 -0
  105. package/src/shared/constants.ts +128 -0
  106. package/src/shared/den-config.ts +688 -0
  107. package/src/shared/dev-metrics/emit.ts +354 -0
  108. package/src/shared/edge/config.ts +229 -0
  109. package/src/shared/edge/render.ts +221 -0
  110. package/src/shared/edge/runtime.ts +268 -0
  111. package/src/shared/edge/status.ts +74 -0
  112. package/src/shared/edge/types.ts +62 -0
  113. package/src/shared/engine.ts +67 -0
  114. package/src/shared/environment/aws-profiles.ts +49 -0
  115. package/src/shared/environment/env-selector.ts +161 -0
  116. package/src/shared/environment/playwright-env.ts +82 -0
  117. package/src/shared/environment-manager/catalog.ts +77 -0
  118. package/src/shared/environment-manager/clone-guard.ts +163 -0
  119. package/src/shared/environment-manager/dag.ts +286 -0
  120. package/src/shared/environment-manager/default-workspace.ts +182 -0
  121. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  122. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  123. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  124. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  125. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  126. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  127. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  128. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  129. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  130. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  131. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  132. package/src/shared/environment-manager/derivation/text.ts +40 -0
  133. package/src/shared/environment-manager/derivation/types.ts +69 -0
  134. package/src/shared/environment-manager/health.ts +635 -0
  135. package/src/shared/environment-manager/index.ts +6 -0
  136. package/src/shared/environment-manager/load-applications.ts +108 -0
  137. package/src/shared/environment-manager/log-tail.ts +213 -0
  138. package/src/shared/environment-manager/make-targets.ts +78 -0
  139. package/src/shared/environment-manager/opctl.ts +335 -0
  140. package/src/shared/environment-manager/readiness.ts +93 -0
  141. package/src/shared/environment-manager/register-local.ts +72 -0
  142. package/src/shared/environment-manager/secrets-check.ts +260 -0
  143. package/src/shared/environment-manager/service-container.ts +47 -0
  144. package/src/shared/environment-manager/service-env.ts +110 -0
  145. package/src/shared/environment-manager/service-runner.ts +1603 -0
  146. package/src/shared/environment-manager/sync-targets.ts +66 -0
  147. package/src/shared/environment-manager/up-plan.ts +326 -0
  148. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  149. package/src/shared/environment-manager/workspaces.ts +860 -0
  150. package/src/shared/manifest/deps.ts +27 -0
  151. package/src/shared/manifest/merge-override.ts +75 -0
  152. package/src/shared/manifest/migrate-manifest.ts +101 -0
  153. package/src/shared/manifest/placement.ts +321 -0
  154. package/src/shared/manifest/read-manifest.ts +278 -0
  155. package/src/shared/manifest/schema.ts +313 -0
  156. package/src/shared/manifest/service-links.ts +93 -0
  157. package/src/shared/manifest/validate-layers.ts +229 -0
  158. package/src/shared/manifest/validate-manifest.ts +200 -0
  159. package/src/shared/manifest/workspace-manifests.ts +92 -0
  160. package/src/shared/open-url.ts +122 -0
  161. package/src/shared/org.ts +44 -0
  162. package/src/shared/paths.ts +190 -0
  163. package/src/shared/perf/optional-fields.ts +105 -0
  164. package/src/shared/perf/report-normalizer.ts +404 -0
  165. package/src/shared/perf/report-paths.ts +19 -0
  166. package/src/shared/perf/report-store.ts +110 -0
  167. package/src/shared/perf/report-types.ts +70 -0
  168. package/src/shared/perf/test-paths.ts +25 -0
  169. package/src/shared/process/log-rotation.ts +147 -0
  170. package/src/shared/process/spawn-streaming.ts +229 -0
  171. package/src/shared/product.ts +92 -0
  172. package/src/shared/profile.ts +141 -0
  173. package/src/shared/redact.ts +107 -0
  174. package/src/shared/repo-kind.ts +32 -0
  175. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  176. package/src/shared/reporting/slack-reporter.ts +340 -0
  177. package/src/shared/results/result-reader.ts +266 -0
  178. package/src/shared/results/result-writer.ts +168 -0
  179. package/src/shared/secrets/doppler.ts +126 -0
  180. package/src/shared/secrets/env.ts +48 -0
  181. package/src/shared/secrets/index.ts +197 -0
  182. package/src/shared/secrets/keys.ts +33 -0
  183. package/src/shared/secrets/onepassword.ts +75 -0
  184. package/src/shared/secrets/types.ts +52 -0
  185. package/src/shared/service-url.ts +119 -0
  186. package/src/shared/smoke/branch-resolver.ts +141 -0
  187. package/src/shared/smoke/git-remote.ts +70 -0
  188. package/src/shared/smoke/repo-cloner.ts +151 -0
  189. package/src/shared/stacks/config.ts +137 -0
  190. package/src/shared/stacks/manifest-commands.ts +118 -0
  191. package/src/shared/stacks/render.ts +253 -0
  192. package/src/shared/stacks/runtime.ts +334 -0
  193. package/src/shared/stacks/status.ts +99 -0
  194. package/src/shared/stacks/types.ts +53 -0
  195. package/src/shared/terminal/tabs.ts +212 -0
  196. package/src/shared/test-discovery/module-scanner.ts +569 -0
  197. package/src/shared/types/index.ts +701 -0
  198. package/src/shared/ui-marker.ts +19 -0
  199. package/src/ui/cli/applications-command.ts +148 -0
  200. package/src/ui/cli/config-command.ts +68 -0
  201. package/src/ui/cli/create-main.ts +9 -0
  202. package/src/ui/cli/create.ts +360 -0
  203. package/src/ui/cli/edge-command.ts +81 -0
  204. package/src/ui/cli/flag-parser.ts +246 -0
  205. package/src/ui/cli/graph-command.ts +570 -0
  206. package/src/ui/cli/health-display.ts +63 -0
  207. package/src/ui/cli/index.ts +3742 -0
  208. package/src/ui/cli/init.ts +855 -0
  209. package/src/ui/cli/invocation-target.ts +175 -0
  210. package/src/ui/cli/logs-command.ts +104 -0
  211. package/src/ui/cli/mcp-main.ts +13 -0
  212. package/src/ui/cli/mcp-stdout-guard.ts +10 -0
  213. package/src/ui/cli/module-selection.ts +31 -0
  214. package/src/ui/cli/prompts.ts +767 -0
  215. package/src/ui/cli/results-viewer.ts +436 -0
  216. package/src/ui/cli/secrets-preflight.ts +91 -0
  217. package/src/ui/cli/stack-command.ts +117 -0
  218. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  219. package/src/ui/cli/test-runner.ts +1319 -0
  220. package/src/ui/cli/web-command.ts +1073 -0
  221. package/src/ui/mcp/api.ts +284 -0
  222. package/src/ui/mcp/guards.ts +141 -0
  223. package/src/ui/mcp/server.ts +147 -0
  224. package/src/ui/mcp/tools.ts +565 -0
  225. package/src/ui/server/audit.ts +71 -0
  226. package/src/ui/server/changelog-parser.ts +105 -0
  227. package/src/ui/server/daemon-reaper.ts +129 -0
  228. package/src/ui/server/editor-manager.ts +301 -0
  229. package/src/ui/server/github-service.ts +223 -0
  230. package/src/ui/server/graph-prefs.ts +89 -0
  231. package/src/ui/server/health-row.ts +15 -0
  232. package/src/ui/server/http-helpers.ts +98 -0
  233. package/src/ui/server/index.ts +3350 -0
  234. package/src/ui/server/log-history.ts +453 -0
  235. package/src/ui/server/perf-report.ts +42 -0
  236. package/src/ui/server/public/environments.html +496 -0
  237. package/src/ui/server/public/favicon.ico +0 -0
  238. package/src/ui/server/public/graph-model.js +416 -0
  239. package/src/ui/server/public/graph.css +976 -0
  240. package/src/ui/server/public/graph.js +986 -0
  241. package/src/ui/server/public/header.js +1373 -0
  242. package/src/ui/server/public/index.html +2595 -0
  243. package/src/ui/server/public/local-dev.html +5344 -0
  244. package/src/ui/server/public/logs.html +1496 -0
  245. package/src/ui/server/public/panel.js +787 -0
  246. package/src/ui/server/public/perf-report.js +359 -0
  247. package/src/ui/server/public/release.html +2111 -0
  248. package/src/ui/server/public/services.html +1103 -0
  249. package/src/ui/server/public/workspace-picker.js +69 -0
  250. package/src/ui/server/readonly-routes.ts +200 -0
  251. package/src/ui/server/release-deploy-session.ts +246 -0
  252. package/src/ui/server/release-service.ts +1125 -0
  253. package/src/ui/server/sudo-store.ts +111 -0
  254. package/src/ui/server/ui-assets.ts +73 -0
  255. package/src/ui/server/workspace-routes.ts +2185 -0
@@ -0,0 +1,688 @@
1
+ /**
2
+ * Den deployment configuration reader.
3
+ *
4
+ * Two-layer config system:
5
+ * config/den.yml — committed, shared deployment config
6
+ * config/local/user.yml — gitignored, per-engineer overrides
7
+ *
8
+ * The user file can toggle local_dev on/off without touching the shared config.
9
+ */
10
+
11
+ import * as fs from "fs";
12
+ import * as path from "path";
13
+ import * as yaml from "yaml";
14
+ import type { Environment, ReleaseSourceConfig } from "./types/index.js";
15
+ import { getDistRoot } from "./paths.js";
16
+ import type { SecretsConfig } from "./secrets/index.js";
17
+ import type { OrgConfig } from "./org.js";
18
+ import type { TestAccountsConfig } from "./auth/test-accounts.js";
19
+
20
+ import { product } from "./product.js";
21
+ const ROOT = getDistRoot();
22
+ const DEN_CONFIG_PATH = path.join(ROOT, product().configFile);
23
+ const USER_CONFIG_PATH = path.join(ROOT, "config", "local", "user.yml");
24
+
25
+ /**
26
+ * Configuration for the derived dependency graph (`den graph` / derived
27
+ * `den up`). Lets the team tune discovery centrally without touching the
28
+ * target repos. Both fields merge over the built-in defaults in
29
+ * environment-manager/derivation/alias.ts.
30
+ */
31
+ export interface GraphConfig {
32
+ /**
33
+ * Canonical repo-system name → alias tokens that appear in env-var URLs, SDK
34
+ * package names, and EventBridge `source` values. The keys also define the
35
+ * universe of known repo-systems for the graph.
36
+ * e.g. shop: [shop-api, shop-backend, backend, shop.backend]
37
+ */
38
+ aliases?: Record<string, string[]>;
39
+ /**
40
+ * A shared local-infra repo (databases, cloud emulators, …) that backends
41
+ * must wait for. Optional: with none set, no infra edges are derived. Any
42
+ * `signals` regex found in a repo's manifest, stack.yml, Makefile or env
43
+ * files means "this repo uses it"; a bare mention of the repo name is a
44
+ * weaker signal.
45
+ */
46
+ infra?: { repo: string; signals?: string[] };
47
+ /** GitHub orgs whose Go modules (`github.com/<org>/<repo>`) count as SDK imports. */
48
+ sdk_orgs?: string[];
49
+ /** npm scopes (`@scope/<repo>-sdk`) whose packages count as SDK imports. */
50
+ npm_scopes?: string[];
51
+ /**
52
+ * Per-edge overrides keyed "from->to":
53
+ * hard — functional-hard; gates startup (waited on). Injected if discovery missed it.
54
+ * soft — started, not gated.
55
+ * ignore — keep in the graph + start set, but exclude from start ORDER
56
+ * (for tolerant edges discovery can't classify, e.g. a redirect URL).
57
+ * e.g. "files-api->links-api": hard ; "billing-api->shop-web": ignore
58
+ */
59
+ edges?: Record<string, "hard" | "soft" | "ignore">;
60
+ }
61
+
62
+ export interface ApplicationConfig {
63
+ title?: string;
64
+ description?: string;
65
+ /** Who builds it (a team, vendor or person). */
66
+ owner?: string;
67
+ repos?: string[];
68
+ }
69
+
70
+ /**
71
+ * A single deployable repo in the release graph: which CD workflow to dispatch,
72
+ * which wave it belongs to (lower waves deploy first and gate later waves), and
73
+ * the ref it deploys by default.
74
+ */
75
+ export interface ReleaseRepo {
76
+ /** Repo-system name (matches the graph node / catalog key). */
77
+ name: string;
78
+ /** GitHub Actions workflow file to dispatch (e.g. "cd.yml", "fullstack-cd.yml"). */
79
+ workflow: string;
80
+ /**
81
+ * Wave index — every repo in wave N dispatches together (staggered); wave N+1
82
+ * is gated on wave N concluding success. Ports release-all.sh's backend→app
83
+ * wave model, generalised to any number of waves.
84
+ */
85
+ wave: number;
86
+ /** Default ref to deploy (default "main"). */
87
+ ref?: string;
88
+ /** Participate in "Deploy all" by default (default true). */
89
+ enabled?: boolean;
90
+ /** Full GitHub slug when the repo is not in `org` ("Initech/initech-web"). */
91
+ gh?: string;
92
+ /** Per Den environment, where the deployed commit is read (manifest `release:`). */
93
+ sources?: Partial<Record<string, ReleaseSourceConfig>>;
94
+ /** Named entry in `release.profiles` giving this repo's per-env targets. */
95
+ profile?: string;
96
+ /** Per-environment overrides, merged over the profile's. */
97
+ targets?: Record<string, ReleaseTarget>;
98
+ }
99
+
100
+ /**
101
+ * How one den environment maps onto a repo's real CD. Every field is
102
+ * optional; the defaults are the den env name, the repo's `workflow`,
103
+ * and the repo's `ref` (or main).
104
+ */
105
+ export interface ReleaseTarget {
106
+ /** GitHub Environment(s) the CD records deployments under. Several (one per tenant) give one row each. */
107
+ environment?: string | string[];
108
+ /** Workflow file(s) whose deployments count. The first is the one dispatched. */
109
+ workflow?: string | string[];
110
+ /** Branch this environment ships from (what "behind" is measured against). */
111
+ branch?: string;
112
+ /** workflow_dispatch inputs. Unset: `environment=<env>`, as before targets existed. */
113
+ inputs?: Record<string, string>;
114
+ }
115
+
116
+ /** A target with its defaults filled in. */
117
+ export interface ResolvedReleaseTarget {
118
+ environments: string[];
119
+ workflows: string[];
120
+ branch: string;
121
+ inputs: Record<string, string>;
122
+ }
123
+
124
+ const asList = (v: string | string[] | undefined): string[] =>
125
+ v === undefined ? [] : Array.isArray(v) ? v : [v];
126
+
127
+ /** Resolve repo × env to the GitHub environments, workflows and branch to check. */
128
+ export function resolveReleaseTarget(
129
+ repo: ReleaseRepo,
130
+ env: string,
131
+ cfg: Pick<ReleaseConfig, "profiles">,
132
+ ): ResolvedReleaseTarget {
133
+ const fromProfile = repo.profile
134
+ ? cfg.profiles?.[repo.profile]?.[env]
135
+ : undefined;
136
+ const t: ReleaseTarget = { ...fromProfile, ...repo.targets?.[env] };
137
+ const environments = asList(t.environment);
138
+ const workflows = asList(t.workflow);
139
+ return {
140
+ environments: environments.length ? environments : [env],
141
+ workflows: workflows.length ? workflows : [repo.workflow],
142
+ branch: t.branch ?? repo.ref ?? "main",
143
+ inputs:
144
+ t.inputs ??
145
+ (fromProfile || repo.targets?.[env] ? {} : { environment: env }),
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Release/deploy configuration for the web Release tab. Ports the wave model of
151
+ * infra-scripts/release-all.sh into den, made editable via the UX. Layered:
152
+ * committed config/den.yml `release:` is the team base, config/local/user.yml
153
+ * `release:` is a personal override (see readReleaseConfig).
154
+ */
155
+ export interface ReleaseConfig {
156
+ /**
157
+ * Allow dispatching deploys from den. Off unless the team config
158
+ * (config/den.yml) sets `deploy: true`; user.yml and the web page can't
159
+ * turn it on. Off, the Release page is read-only and deploys are refused.
160
+ */
161
+ deploy: boolean;
162
+ /** GitHub org owning the repos. Falls back to `github_org` when omitted. */
163
+ org?: string;
164
+ /** Deployable GitHub environments, in display order (e.g. dev/sandbox/staging/prod). */
165
+ environments: string[];
166
+ /** Seconds to wait between dispatches within a wave (release-all.sh default: 20). */
167
+ staggerSeconds: number;
168
+ /** Seconds between monitor polls while waiting on runs (release-all.sh default: 15). */
169
+ pollSeconds: number;
170
+ /** Deployable repos and their wave assignment. */
171
+ repos: ReleaseRepo[];
172
+ /** Shared per-env targets, by name; a repo opts in with `profile:`. */
173
+ profiles?: Record<string, Record<string, ReleaseTarget>>;
174
+ }
175
+
176
+ export interface DenConfig {
177
+ /** Secrets backend strategy — see src/shared/secrets. */
178
+ secrets?: SecretsConfig;
179
+ /** Environments enabled for this deployment. */
180
+ environments: Environment[];
181
+ /**
182
+ * The environments that count as production (default: prod, production).
183
+ * Production is an attribute of an env, never a name check: Den refuses to
184
+ * start, stop, scan or exec a remote service whose target env is listed.
185
+ */
186
+ production_envs?: string[];
187
+ /**
188
+ * Root directory for local repo discovery, relative to the den directory.
189
+ * Defaults to ".." (the parent of den), which covers the common layout where
190
+ * den and all managed repos are siblings under a shared project root.
191
+ */
192
+ repos_root?: string;
193
+ /** GitHub org slug used to list org repos in the web UI (e.g. "Globex-Corp"). */
194
+ github_org?: string;
195
+ /**
196
+ * Display names (and an optional colour) for the GitHub orgs repos belong
197
+ * to, keyed by GitHub owner. Orgs are worked out from the catalog; this
198
+ * only names them.
199
+ */
200
+ orgs?: Record<string, { label?: string; color?: string }>;
201
+ /** Derived-dependency-graph tuning (aliases + edge overrides). */
202
+ graph?: GraphConfig;
203
+ /**
204
+ * The org this instance serves (Org > Application > Repo > Services).
205
+ * `id` defaults to `edge.org`, `title` to the id. See src/shared/org.ts.
206
+ */
207
+ org?: OrgConfig;
208
+ /**
209
+ * Applications: named sets of repos that ship together. A repo can also
210
+ * name its application in its own `.den.yml` (`application:`). A repo
211
+ * in no application is its own, named after the repo.
212
+ */
213
+ applications?: Record<string, ApplicationConfig>;
214
+ /** Release/deploy configuration for the web Release tab (team base). */
215
+ release?: ReleaseConfig;
216
+ /**
217
+ * Extra environment variables injected into every service command (up/down/
218
+ * restart/setup), so consumer repos receive den-resolved locations
219
+ * regardless of where their checkout lives. Values are templates; the only
220
+ * placeholder today is `{repo:<name>}` → the absolute path den resolves
221
+ * `<name>` to. A value already set in the caller's environment is never
222
+ * overridden; an empty string disables a built-in default. See `service-env.ts`.
223
+ */
224
+ service_env?: Record<string, string>;
225
+ /**
226
+ * Non-local health-URL templates `den init` scaffolds per environment,
227
+ * with `{repo}` substituted (e.g. dev: "https://{repo}.internal.dev.example.dev/
228
+ * healthz", production: "https://{repo}.internal.example.dev/healthz"). These
229
+ * feed the TEST scope — smoke/perf base-URL resolution reads the manifest's
230
+ * `health.<env>.url`. Defaults only: hand-edited URLs in an existing manifest
231
+ * are preserved on re-init.
232
+ */
233
+ env_health_urls?: Record<string, string>;
234
+ /** Security pillar (OWASP ZAP) policy — see readSecurityOwnedHosts. */
235
+ security?: SecurityConfig;
236
+ /**
237
+ * The MCP server (`<cli> mcp`). Without this block agents get read tools
238
+ * only; `actions` opts in to named actions. See readMcpConfig.
239
+ */
240
+ mcp?: McpConfig;
241
+ /**
242
+ * Shared test accounts (email + secrets-backend item per role) the test
243
+ * pillars log in as — see src/shared/auth/test-accounts.ts.
244
+ */
245
+ test_accounts?: TestAccountsConfig;
246
+ /**
247
+ * Out-of-the-box default workspace blueprint. On a fresh clone, den offers
248
+ * to seed the `default` workspace with these repos, all managed. See
249
+ * `default-workspace.ts`.
250
+ */
251
+ defaults?: {
252
+ workspace?: {
253
+ /** Branch managed clones track (default "main"). */
254
+ branch?: string;
255
+ /** Repo-system names to seed, all as managed clones. */
256
+ repos?: string[];
257
+ /** The entry repo the seed's dependency walk starts from (listed first). */
258
+ entry?: string;
259
+ /**
260
+ * Seeded repos that get `autostart: false` stamped on their workspace
261
+ * entry — the team default for systems a fresh clone should not start
262
+ * with workspace up (e.g. billing-api's `make up` opens an interactive
263
+ * Stripe tab). Personal thereafter: toggle per system in the web ⋮ menu.
264
+ */
265
+ autostart_off?: string[];
266
+ };
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Per-engineer preferences stored in config/local/user.yml (gitignored).
272
+ * Only affects settings that should differ between engineers on the same team.
273
+ */
274
+ export interface UserConfig {
275
+ /** Per-engineer secrets override (e.g. provider: env). */
276
+ secrets?: SecretsConfig;
277
+ /**
278
+ * Whether the local dev experience is enabled for this engineer.
279
+ * When false, the Local Dev tab is hidden and all workspace/service APIs
280
+ * are disabled. Defaults to true (local dev on by default).
281
+ */
282
+ local_dev?: boolean;
283
+ /** Whether `den up` / `den restart` should default to terminal tab mode. */
284
+ cli_tabs?: boolean;
285
+ /**
286
+ * Max lines a /logs pane keeps loaded (live tail + paged history).
287
+ * Default 20000. Raise via `den config log_window_lines=50000` when
288
+ * deeper in-pane digs are needed; full-history search is unaffected.
289
+ */
290
+ log_window_lines?: number;
291
+ /**
292
+ * Key of the preferred editor for "open in editor" actions (e.g. "vscode", "cursor").
293
+ * Stored as a key; the server resolves the actual executable path at open time.
294
+ */
295
+ editor?: string;
296
+ /**
297
+ * Key of the preferred terminal app for "open in terminal" actions (e.g. "ghostty", "iterm2").
298
+ * Stored as a key; the server resolves the app name at open time.
299
+ */
300
+ terminal?: string;
301
+ /**
302
+ * Per-language IDE overrides. Key = language slug (e.g. "go"), value = editor key (e.g. "idea").
303
+ * When opening a repo whose stack.yml declares a matching language, den uses this
304
+ * editor instead of the default.
305
+ */
306
+ editor_overrides?: Record<string, string>;
307
+ /**
308
+ * Personal override of the team release config (config/den.yml `release:`).
309
+ * Wave-assignment edits made in the Release tab are saved here so they stay
310
+ * local and never get committed. Merged field-wise over the team base.
311
+ */
312
+ release?: Partial<ReleaseConfig>;
313
+ /**
314
+ * Graph layout preferences (direction, grouping, card density, panel
315
+ * width), set from the Local Dev graph. See src/ui/server/graph-prefs.ts.
316
+ */
317
+ graph?: {
318
+ direction?: string;
319
+ group?: string;
320
+ density?: string;
321
+ panelWidth?: number;
322
+ };
323
+ }
324
+
325
+ const ALL_ENVIRONMENTS: Environment[] = [
326
+ "local",
327
+ "dev",
328
+ "sandbox",
329
+ "staging",
330
+ "beta",
331
+ "prod",
332
+ "production",
333
+ ];
334
+
335
+ /** Den's built-in environment names (used when the org config lists none). */
336
+ export function builtinEnvironments(): Environment[] {
337
+ return [...ALL_ENVIRONMENTS];
338
+ }
339
+
340
+ const DEFAULT_CONFIG: DenConfig = {
341
+ environments: ALL_ENVIRONMENTS,
342
+ };
343
+
344
+ /** Read the per-engineer user config (config/local/user.yml). Returns {} if absent. */
345
+ export function readUserConfig(): UserConfig {
346
+ try {
347
+ const raw = fs.readFileSync(USER_CONFIG_PATH, "utf-8");
348
+ const parsed = yaml.parse(raw) as UserConfig | null;
349
+ return parsed ?? {};
350
+ } catch (err: unknown) {
351
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return {};
352
+ throw err;
353
+ }
354
+ }
355
+
356
+ /** Write the per-engineer user config to config/local/user.yml. */
357
+ export function writeUserConfig(cfg: UserConfig): void {
358
+ fs.mkdirSync(path.dirname(USER_CONFIG_PATH), { recursive: true });
359
+ const lines = [
360
+ "# Per-engineer preferences — gitignored, never committed",
361
+ yaml.stringify(cfg).trimEnd(),
362
+ ];
363
+ fs.writeFileSync(USER_CONFIG_PATH, lines.join("\n") + "\n", "utf-8");
364
+ }
365
+
366
+ /** Merge a partial patch into config/local/user.yml without clobbering fields. */
367
+ export function mergeUserConfig(patch: Partial<UserConfig>): UserConfig {
368
+ const current = readUserConfig();
369
+ const next: UserConfig = { ...current };
370
+
371
+ for (const [key, value] of Object.entries(patch) as Array<
372
+ [keyof UserConfig, UserConfig[keyof UserConfig] | undefined]
373
+ >) {
374
+ if (value === undefined) {
375
+ delete next[key];
376
+ } else {
377
+ next[key] = value as never;
378
+ }
379
+ }
380
+
381
+ writeUserConfig(next);
382
+ return next;
383
+ }
384
+
385
+ /**
386
+ * The GitHub org managed repos clone from. Den has no default: an org that
387
+ * needs one sets `github_org` in its config.
388
+ */
389
+ export function requireGithubOrg(config: DenConfig = readDenConfig()): string {
390
+ const org = config.github_org?.trim();
391
+ if (!org)
392
+ throw new Error(
393
+ `github_org is not set: add \`github_org: <your-org>\` to ${DEN_CONFIG_PATH}`,
394
+ );
395
+ return org;
396
+ }
397
+
398
+ /**
399
+ * Read the effective den config, merging the shared deployment config with
400
+ * per-engineer overrides from config/local/user.yml.
401
+ */
402
+ export function readDenConfig(): DenConfig {
403
+ // Read shared base config
404
+ let base: DenConfig;
405
+ try {
406
+ const raw = fs.readFileSync(DEN_CONFIG_PATH, "utf-8");
407
+ const parsed = yaml.parse(raw) as Partial<DenConfig> | null;
408
+ // `environments` may be malformed (scalar/object) in a hand-edited file —
409
+ // fall back to the default set rather than throwing on .filter below.
410
+ const envs = Array.isArray(parsed?.environments)
411
+ ? parsed.environments
412
+ : ALL_ENVIRONMENTS;
413
+ base = {
414
+ environments: envs.filter((e): e is Environment =>
415
+ ALL_ENVIRONMENTS.includes(e as Environment),
416
+ ),
417
+ production_envs: parsed?.production_envs,
418
+ repos_root: parsed?.repos_root,
419
+ github_org: parsed?.github_org,
420
+ orgs: parsed?.orgs,
421
+ graph: parsed?.graph,
422
+ org: parsed?.org,
423
+ applications: parsed?.applications,
424
+ release: parsed?.release,
425
+ service_env: parsed?.service_env,
426
+ env_health_urls: parsed?.env_health_urls,
427
+ security: parsed?.security,
428
+ mcp: parsed?.mcp,
429
+ test_accounts: parsed?.test_accounts,
430
+ defaults: parsed?.defaults,
431
+ secrets: parsed?.secrets,
432
+ };
433
+ } catch (err: unknown) {
434
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
435
+ base = { ...DEFAULT_CONFIG };
436
+ } else {
437
+ throw err;
438
+ }
439
+ }
440
+
441
+ // Apply per-engineer override: local_dev: false → strip "local" from environments
442
+ const user = readUserConfig();
443
+ if (user.local_dev === false) {
444
+ base.environments = base.environments.filter((e) => e !== "local");
445
+ }
446
+
447
+ return base;
448
+ }
449
+
450
+ /**
451
+ * Absolute path to the root directory where local repos are discovered.
452
+ * Defaults to the parent of the den root directory.
453
+ */
454
+ export function getReposRoot(): string {
455
+ const config = readDenConfig();
456
+ return path.resolve(ROOT, config.repos_root ?? "..");
457
+ }
458
+
459
+ const DEFAULT_PRODUCTION_ENVS = ["prod", "production"];
460
+
461
+ /** The envs the org counts as production (`production_envs`). */
462
+ export function productionEnvs(config: DenConfig = readDenConfig()): string[] {
463
+ const listed = config.production_envs;
464
+ return Array.isArray(listed) &&
465
+ listed.every((e): e is string => typeof e === "string")
466
+ ? listed
467
+ : DEFAULT_PRODUCTION_ENVS;
468
+ }
469
+
470
+ /**
471
+ * Whether the org counts `env` as production. An unreadable org config falls
472
+ * back to the default list rather than letting a guard throw.
473
+ */
474
+ export function isProductionEnv(env: string, config?: DenConfig): boolean {
475
+ let listed: string[];
476
+ try {
477
+ listed = productionEnvs(config ?? readDenConfig());
478
+ } catch {
479
+ listed = DEFAULT_PRODUCTION_ENVS;
480
+ }
481
+ return listed.includes(env);
482
+ }
483
+
484
+ /**
485
+ * The org's environment names, for validating manifests: the config's
486
+ * `environments` (Den's built-in list when unset), always with `local`.
487
+ */
488
+ export function orgEnvironments(config: DenConfig = readDenConfig()): string[] {
489
+ return [...new Set<string>(["local", ...config.environments])];
490
+ }
491
+
492
+ /** True when the `local` environment is enabled for this engineer. */
493
+ export function isLocalEnabled(): boolean {
494
+ return readDenConfig().environments.includes("local");
495
+ }
496
+
497
+ // ── Security config (security pillar) ─────────────────────────────────────────
498
+
499
+ /** `security:` in config/den.yml. */
500
+ export interface SecurityConfig {
501
+ /**
502
+ * Hostnames the security pillar may point ZAP at. An entry matches that exact
503
+ * host or any subdomain of it (a leading "." is ignored). Extend per shell with
504
+ * DEN_SECURITY_EXTRA_HOSTS (comma-separated).
505
+ */
506
+ owned_hosts?: string[];
507
+ /**
508
+ * Also scan, unlisted, every locally run service with an HTTP health URL
509
+ * (unless its manifest says `security.skip`). Default false: only the
510
+ * targets manifests declare under `security:` are scanned.
511
+ */
512
+ auto_targets?: boolean;
513
+ }
514
+
515
+ /**
516
+ * Owned hosts when config/den.yml has no `security.owned_hosts`: none.
517
+ * Scanning is a deliberate per-org choice, so nothing is owned by default.
518
+ */
519
+ export const DEFAULT_SECURITY_OWNED_HOSTS: readonly string[] = [];
520
+
521
+ /**
522
+ * The configured owned-host allowlist (config/den.yml
523
+ * `security.owned_hosts`), lowercased. A missing or malformed value falls back
524
+ * to DEFAULT_SECURITY_OWNED_HOSTS; an explicit empty list means "nothing owned".
525
+ */
526
+ export function readSecurityOwnedHosts(
527
+ config: DenConfig = readDenConfig(),
528
+ ): string[] {
529
+ const hosts = config.security?.owned_hosts;
530
+ const list = Array.isArray(hosts) ? hosts : DEFAULT_SECURITY_OWNED_HOSTS;
531
+ return list
532
+ .filter((h): h is string => typeof h === "string")
533
+ .map((h) => h.trim().toLowerCase())
534
+ .filter((h) => h.length > 0);
535
+ }
536
+
537
+ /** Whether `security.auto_targets` is on (only an explicit `true` counts). */
538
+ export function readSecurityAutoTargets(
539
+ config: DenConfig = readDenConfig(),
540
+ ): boolean {
541
+ return config.security?.auto_targets === true;
542
+ }
543
+
544
+ // ── MCP config (`<cli> mcp`) ───────────────────────────────────────────────────
545
+
546
+ /** The action tools an org can opt in to. Read tools are always on. */
547
+ export const MCP_ACTIONS = [
548
+ "service_up",
549
+ "service_down",
550
+ "service_restart",
551
+ "workspace_sync",
552
+ "run_tests",
553
+ ] as const;
554
+ export type McpAction = (typeof MCP_ACTIONS)[number];
555
+
556
+ /** `mcp:` in the org config. */
557
+ export interface McpConfig {
558
+ /** Action tools to register; none when absent. */
559
+ actions?: string[];
560
+ /** Environments `run_tests` may target (default [local]; production never). */
561
+ run_tests?: { envs?: string[] };
562
+ /** `remote: true` lets the log tools read non-local sources (e.g. CloudWatch). */
563
+ logs?: { remote?: boolean };
564
+ }
565
+
566
+ /** `mcp:` with its defaults applied. */
567
+ export interface ResolvedMcpConfig {
568
+ actions: McpAction[];
569
+ runTestsEnvs: Environment[];
570
+ logsRemote: boolean;
571
+ }
572
+
573
+ /**
574
+ * The effective MCP config. An unknown action or environment name throws:
575
+ * a typo must not silently turn a guard off (or an action on).
576
+ */
577
+ export function readMcpConfig(
578
+ config: DenConfig = readDenConfig(),
579
+ ): ResolvedMcpConfig {
580
+ const raw = config.mcp ?? {};
581
+ const actions = (Array.isArray(raw.actions) ? raw.actions : []).map((a) => {
582
+ if (!(MCP_ACTIONS as readonly string[]).includes(String(a)))
583
+ throw new Error(
584
+ `mcp.actions: unknown action "${String(a)}" (known: ${MCP_ACTIONS.join(", ")})`,
585
+ );
586
+ return a as McpAction;
587
+ });
588
+ const envList = raw.run_tests?.envs;
589
+ const runTestsEnvs = (Array.isArray(envList) ? envList : ["local"]).map(
590
+ (e) => {
591
+ if (!ALL_ENVIRONMENTS.includes(e as Environment))
592
+ throw new Error(
593
+ `mcp.run_tests.envs: unknown environment "${String(e)}" (known: ${ALL_ENVIRONMENTS.join(", ")})`,
594
+ );
595
+ return e as Environment;
596
+ },
597
+ );
598
+ return {
599
+ actions: [...new Set(actions)],
600
+ runTestsEnvs: [...new Set(runTestsEnvs)],
601
+ logsRemote: raw.logs?.remote === true,
602
+ };
603
+ }
604
+
605
+ // ── Release config (Release tab) ──────────────────────────────────────────────
606
+
607
+ /**
608
+ * Built-in release defaults: no environments or repos, deploys off. The org
609
+ * config's `release:` layers over this, personal edits via
610
+ * config/local/user.yml.
611
+ */
612
+ export const DEFAULT_RELEASE_CONFIG: ReleaseConfig = {
613
+ deploy: false,
614
+ // No built-in environments or repos: each instance lists its own.
615
+ environments: [],
616
+ staggerSeconds: 20,
617
+ pollSeconds: 15,
618
+ repos: [],
619
+ };
620
+
621
+ /** Merge one override layer (den.yml or user.yml) over a base release config. */
622
+ function mergeReleaseLayer(
623
+ base: ReleaseConfig,
624
+ layer?: Partial<ReleaseConfig> | null,
625
+ ): ReleaseConfig {
626
+ const merged: ReleaseConfig = {
627
+ deploy: base.deploy,
628
+ org: layer?.org ?? base.org,
629
+ environments:
630
+ Array.isArray(layer?.environments) && layer.environments.length
631
+ ? layer.environments
632
+ : base.environments,
633
+ staggerSeconds:
634
+ typeof layer?.staggerSeconds === "number"
635
+ ? layer.staggerSeconds
636
+ : base.staggerSeconds,
637
+ pollSeconds:
638
+ typeof layer?.pollSeconds === "number"
639
+ ? layer.pollSeconds
640
+ : base.pollSeconds,
641
+ repos: base.repos.map((r) => ({ ...r })),
642
+ ...(base.profiles || layer?.profiles
643
+ ? { profiles: { ...base.profiles, ...layer?.profiles } }
644
+ : {}),
645
+ };
646
+
647
+ if (Array.isArray(layer?.repos)) {
648
+ // A layer may add repos with only a subset of fields (e.g. a hand-edited
649
+ // user.yml with just name+wave), so treat entries as partial at runtime.
650
+ const patches = layer.repos as Array<
651
+ Partial<ReleaseRepo> & { name: string }
652
+ >;
653
+ const byName = new Map(merged.repos.map((r) => [r.name, r]));
654
+ for (const patch of patches) {
655
+ if (!patch?.name) continue;
656
+ const existing = byName.get(patch.name);
657
+ if (existing) Object.assign(existing, patch);
658
+ else byName.set(patch.name, { workflow: "cd.yml", wave: 0, ...patch });
659
+ }
660
+ merged.repos = [...byName.values()];
661
+ }
662
+ return merged;
663
+ }
664
+
665
+ /**
666
+ * Read the effective release config: built-in defaults ← config/den.yml
667
+ * `release:` (team base) ← config/local/user.yml `release:` (personal override).
668
+ * `org` falls back to the den `github_org` when unset at every layer.
669
+ */
670
+ export function readReleaseConfig(): ReleaseConfig {
671
+ const den = readDenConfig();
672
+ let cfg = mergeReleaseLayer(DEFAULT_RELEASE_CONFIG, den.release);
673
+ // Only the team layer may enable deploys; user.yml can't.
674
+ cfg.deploy = den.release?.deploy === true;
675
+ cfg = mergeReleaseLayer(cfg, readUserConfig().release);
676
+ if (!cfg.org) cfg.org = den.github_org;
677
+ return cfg;
678
+ }
679
+
680
+ /**
681
+ * Persist a personal release override to config/local/user.yml (gitignored).
682
+ * Used by the Release tab's wave editor — team defaults stay in den.yml.
683
+ */
684
+ export function writeUserReleaseConfig(release: Partial<ReleaseConfig>): void {
685
+ const { deploy: _ignored, ...rest } = release;
686
+ void _ignored;
687
+ mergeUserConfig({ release: rest });
688
+ }