@monster-orange/den 0.0.0-stage → 0.1.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 (231) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -2
  4. package/bin/den +61 -0
  5. package/docs/distributions.md +256 -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 +104 -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 +406 -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 +108 -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 +729 -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/applications.ts +138 -0
  91. package/src/shared/auth/api-token.ts +416 -0
  92. package/src/shared/auth/auth0-helper.ts +284 -0
  93. package/src/shared/auth/headless-login.ts +265 -0
  94. package/src/shared/auth/jwt-helper.ts +186 -0
  95. package/src/shared/auth/sudo-vault.ts +69 -0
  96. package/src/shared/auth/test-accounts.ts +98 -0
  97. package/src/shared/auth/token-injection.ts +184 -0
  98. package/src/shared/auth/token-providers/auth0.ts +80 -0
  99. package/src/shared/auth/token-providers/index.ts +182 -0
  100. package/src/shared/auth/token-refresh.ts +273 -0
  101. package/src/shared/banner.ts +15 -0
  102. package/src/shared/brand.ts +79 -0
  103. package/src/shared/cli-tabs.ts +131 -0
  104. package/src/shared/constants.ts +128 -0
  105. package/src/shared/den-config.ts +566 -0
  106. package/src/shared/dev-metrics/emit.ts +354 -0
  107. package/src/shared/edge/config.ts +229 -0
  108. package/src/shared/edge/render.ts +221 -0
  109. package/src/shared/edge/runtime.ts +268 -0
  110. package/src/shared/edge/types.ts +62 -0
  111. package/src/shared/engine.ts +67 -0
  112. package/src/shared/environment/aws-profiles.ts +49 -0
  113. package/src/shared/environment/env-selector.ts +161 -0
  114. package/src/shared/environment/playwright-env.ts +82 -0
  115. package/src/shared/environment-manager/catalog.ts +77 -0
  116. package/src/shared/environment-manager/clone-guard.ts +163 -0
  117. package/src/shared/environment-manager/dag.ts +286 -0
  118. package/src/shared/environment-manager/default-workspace.ts +182 -0
  119. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  120. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  121. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  122. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  123. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  124. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  125. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  126. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  127. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  128. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  129. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  130. package/src/shared/environment-manager/derivation/text.ts +40 -0
  131. package/src/shared/environment-manager/derivation/types.ts +69 -0
  132. package/src/shared/environment-manager/health.ts +589 -0
  133. package/src/shared/environment-manager/index.ts +6 -0
  134. package/src/shared/environment-manager/load-applications.ts +78 -0
  135. package/src/shared/environment-manager/log-tail.ts +212 -0
  136. package/src/shared/environment-manager/make-targets.ts +78 -0
  137. package/src/shared/environment-manager/opctl.ts +334 -0
  138. package/src/shared/environment-manager/readiness.ts +93 -0
  139. package/src/shared/environment-manager/register-local.ts +72 -0
  140. package/src/shared/environment-manager/secrets-check.ts +260 -0
  141. package/src/shared/environment-manager/service-env.ts +110 -0
  142. package/src/shared/environment-manager/service-runner.ts +1588 -0
  143. package/src/shared/environment-manager/up-plan.ts +302 -0
  144. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  145. package/src/shared/environment-manager/workspaces.ts +860 -0
  146. package/src/shared/manifest/deps.ts +27 -0
  147. package/src/shared/manifest/migrate-manifest.ts +101 -0
  148. package/src/shared/manifest/read-manifest.ts +160 -0
  149. package/src/shared/manifest/schema.ts +299 -0
  150. package/src/shared/manifest/service-links.ts +93 -0
  151. package/src/shared/manifest/validate-manifest.ts +140 -0
  152. package/src/shared/manifest/workspace-manifests.ts +92 -0
  153. package/src/shared/open-url.ts +122 -0
  154. package/src/shared/org.ts +44 -0
  155. package/src/shared/paths.ts +178 -0
  156. package/src/shared/perf/optional-fields.ts +105 -0
  157. package/src/shared/perf/report-normalizer.ts +404 -0
  158. package/src/shared/perf/report-paths.ts +19 -0
  159. package/src/shared/perf/report-store.ts +110 -0
  160. package/src/shared/perf/report-types.ts +70 -0
  161. package/src/shared/perf/test-paths.ts +25 -0
  162. package/src/shared/process/log-rotation.ts +147 -0
  163. package/src/shared/process/spawn-streaming.ts +229 -0
  164. package/src/shared/product.ts +92 -0
  165. package/src/shared/profile.ts +141 -0
  166. package/src/shared/repo-kind.ts +32 -0
  167. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  168. package/src/shared/reporting/slack-reporter.ts +340 -0
  169. package/src/shared/results/result-reader.ts +266 -0
  170. package/src/shared/results/result-writer.ts +168 -0
  171. package/src/shared/secrets/doppler.ts +126 -0
  172. package/src/shared/secrets/env.ts +48 -0
  173. package/src/shared/secrets/index.ts +197 -0
  174. package/src/shared/secrets/keys.ts +33 -0
  175. package/src/shared/secrets/onepassword.ts +75 -0
  176. package/src/shared/secrets/types.ts +52 -0
  177. package/src/shared/service-url.ts +119 -0
  178. package/src/shared/smoke/branch-resolver.ts +141 -0
  179. package/src/shared/smoke/git-remote.ts +70 -0
  180. package/src/shared/smoke/repo-cloner.ts +151 -0
  181. package/src/shared/stacks/config.ts +137 -0
  182. package/src/shared/stacks/manifest-commands.ts +88 -0
  183. package/src/shared/stacks/render.ts +253 -0
  184. package/src/shared/stacks/runtime.ts +334 -0
  185. package/src/shared/stacks/types.ts +53 -0
  186. package/src/shared/terminal/tabs.ts +212 -0
  187. package/src/shared/test-discovery/module-scanner.ts +569 -0
  188. package/src/shared/types/index.ts +678 -0
  189. package/src/shared/ui-marker.ts +19 -0
  190. package/src/ui/cli/applications-command.ts +116 -0
  191. package/src/ui/cli/config-command.ts +68 -0
  192. package/src/ui/cli/create-main.ts +9 -0
  193. package/src/ui/cli/create.ts +360 -0
  194. package/src/ui/cli/edge-command.ts +91 -0
  195. package/src/ui/cli/flag-parser.ts +246 -0
  196. package/src/ui/cli/graph-command.ts +524 -0
  197. package/src/ui/cli/health-display.ts +63 -0
  198. package/src/ui/cli/index.ts +3656 -0
  199. package/src/ui/cli/init.ts +855 -0
  200. package/src/ui/cli/invocation-target.ts +175 -0
  201. package/src/ui/cli/logs-command.ts +104 -0
  202. package/src/ui/cli/module-selection.ts +31 -0
  203. package/src/ui/cli/prompts.ts +767 -0
  204. package/src/ui/cli/results-viewer.ts +436 -0
  205. package/src/ui/cli/secrets-preflight.ts +91 -0
  206. package/src/ui/cli/stack-command.ts +144 -0
  207. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  208. package/src/ui/cli/test-runner.ts +1312 -0
  209. package/src/ui/cli/web-command.ts +1073 -0
  210. package/src/ui/server/changelog-parser.ts +94 -0
  211. package/src/ui/server/daemon-reaper.ts +129 -0
  212. package/src/ui/server/editor-manager.ts +301 -0
  213. package/src/ui/server/github-service.ts +223 -0
  214. package/src/ui/server/http-helpers.ts +98 -0
  215. package/src/ui/server/index.ts +3236 -0
  216. package/src/ui/server/log-history.ts +452 -0
  217. package/src/ui/server/perf-report.ts +42 -0
  218. package/src/ui/server/public/environments.html +494 -0
  219. package/src/ui/server/public/favicon.ico +0 -0
  220. package/src/ui/server/public/header.js +1371 -0
  221. package/src/ui/server/public/index.html +2595 -0
  222. package/src/ui/server/public/local-dev.html +7405 -0
  223. package/src/ui/server/public/logs.html +1489 -0
  224. package/src/ui/server/public/perf-report.js +359 -0
  225. package/src/ui/server/public/release.html +2111 -0
  226. package/src/ui/server/public/services.html +1098 -0
  227. package/src/ui/server/public/workspace-picker.js +69 -0
  228. package/src/ui/server/release-deploy-session.ts +246 -0
  229. package/src/ui/server/release-service.ts +1125 -0
  230. package/src/ui/server/sudo-store.ts +111 -0
  231. package/src/ui/server/workspace-routes.ts +2154 -0
@@ -0,0 +1,566 @@
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
+ * Root directory for local repo discovery, relative to the den directory.
183
+ * Defaults to ".." (the parent of den), which covers the common layout where
184
+ * den and all managed repos are siblings under a shared project root.
185
+ */
186
+ repos_root?: string;
187
+ /** GitHub org slug used to list org repos in the web UI (e.g. "Globex-Corp"). */
188
+ github_org?: string;
189
+ /**
190
+ * Display names (and an optional colour) for the GitHub orgs repos belong
191
+ * to, keyed by GitHub owner. Orgs are worked out from the catalog; this
192
+ * only names them.
193
+ */
194
+ orgs?: Record<string, { label?: string; color?: string }>;
195
+ /** Derived-dependency-graph tuning (aliases + edge overrides). */
196
+ graph?: GraphConfig;
197
+ /**
198
+ * The org this instance serves (Org > Application > Repo > Services).
199
+ * `id` defaults to `edge.org`, `title` to the id. See src/shared/org.ts.
200
+ */
201
+ org?: OrgConfig;
202
+ /**
203
+ * Applications: named sets of repos that ship together. A repo can also
204
+ * name its application in its own `.den.yml` (`application:`). A repo
205
+ * in no application is its own, named after the repo.
206
+ */
207
+ applications?: Record<string, ApplicationConfig>;
208
+ /** Release/deploy configuration for the web Release tab (team base). */
209
+ release?: ReleaseConfig;
210
+ /**
211
+ * Extra environment variables injected into every service command (up/down/
212
+ * restart/setup), so consumer repos receive den-resolved locations
213
+ * regardless of where their checkout lives. Values are templates; the only
214
+ * placeholder today is `{repo:<name>}` → the absolute path den resolves
215
+ * `<name>` to. A value already set in the caller's environment is never
216
+ * overridden; an empty string disables a built-in default. See `service-env.ts`.
217
+ */
218
+ service_env?: Record<string, string>;
219
+ /**
220
+ * Non-local health-URL templates `den init` scaffolds per environment,
221
+ * with `{repo}` substituted (e.g. dev: "https://{repo}.internal.dev.example.dev/
222
+ * healthz", production: "https://{repo}.internal.example.dev/healthz"). These
223
+ * feed the TEST scope — smoke/perf base-URL resolution reads the manifest's
224
+ * `health.<env>.url`. Defaults only: hand-edited URLs in an existing manifest
225
+ * are preserved on re-init.
226
+ */
227
+ env_health_urls?: Record<string, string>;
228
+ /** Security pillar (OWASP ZAP) policy — see readSecurityOwnedHosts. */
229
+ security?: SecurityConfig;
230
+ /**
231
+ * Shared test accounts (email + secrets-backend item per role) the test
232
+ * pillars log in as — see src/shared/auth/test-accounts.ts.
233
+ */
234
+ test_accounts?: TestAccountsConfig;
235
+ /**
236
+ * Out-of-the-box default workspace blueprint. On a fresh clone, den offers
237
+ * to seed the `default` workspace with these repos, all managed. See
238
+ * `default-workspace.ts`.
239
+ */
240
+ defaults?: {
241
+ workspace?: {
242
+ /** Branch managed clones track (default "main"). */
243
+ branch?: string;
244
+ /** Repo-system names to seed, all as managed clones. */
245
+ repos?: string[];
246
+ /** The entry repo the seed's dependency walk starts from (listed first). */
247
+ entry?: string;
248
+ /**
249
+ * Seeded repos that get `autostart: false` stamped on their workspace
250
+ * entry — the team default for systems a fresh clone should not start
251
+ * with workspace up (e.g. billing-api's `make up` opens an interactive
252
+ * Stripe tab). Personal thereafter: toggle per system in the web ⋮ menu.
253
+ */
254
+ autostart_off?: string[];
255
+ };
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Per-engineer preferences stored in config/local/user.yml (gitignored).
261
+ * Only affects settings that should differ between engineers on the same team.
262
+ */
263
+ export interface UserConfig {
264
+ /** Per-engineer secrets override (e.g. provider: env). */
265
+ secrets?: SecretsConfig;
266
+ /**
267
+ * Whether the local dev experience is enabled for this engineer.
268
+ * When false, the Local Dev tab is hidden and all workspace/service APIs
269
+ * are disabled. Defaults to true (local dev on by default).
270
+ */
271
+ local_dev?: boolean;
272
+ /** Whether `den up` / `den restart` should default to terminal tab mode. */
273
+ cli_tabs?: boolean;
274
+ /**
275
+ * Max lines a /logs pane keeps loaded (live tail + paged history).
276
+ * Default 20000. Raise via `den config log_window_lines=50000` when
277
+ * deeper in-pane digs are needed; full-history search is unaffected.
278
+ */
279
+ log_window_lines?: number;
280
+ /**
281
+ * Key of the preferred editor for "open in editor" actions (e.g. "vscode", "cursor").
282
+ * Stored as a key; the server resolves the actual executable path at open time.
283
+ */
284
+ editor?: string;
285
+ /**
286
+ * Key of the preferred terminal app for "open in terminal" actions (e.g. "ghostty", "iterm2").
287
+ * Stored as a key; the server resolves the app name at open time.
288
+ */
289
+ terminal?: string;
290
+ /**
291
+ * Per-language IDE overrides. Key = language slug (e.g. "go"), value = editor key (e.g. "idea").
292
+ * When opening a repo whose stack.yml declares a matching language, den uses this
293
+ * editor instead of the default.
294
+ */
295
+ editor_overrides?: Record<string, string>;
296
+ /**
297
+ * Personal override of the team release config (config/den.yml `release:`).
298
+ * Wave-assignment edits made in the Release tab are saved here so they stay
299
+ * local and never get committed. Merged field-wise over the team base.
300
+ */
301
+ release?: Partial<ReleaseConfig>;
302
+ }
303
+
304
+ const ALL_ENVIRONMENTS: Environment[] = [
305
+ "local",
306
+ "dev",
307
+ "sandbox",
308
+ "staging",
309
+ "beta",
310
+ "prod",
311
+ "production",
312
+ ];
313
+
314
+ const DEFAULT_CONFIG: DenConfig = {
315
+ environments: ALL_ENVIRONMENTS,
316
+ };
317
+
318
+ /** Read the per-engineer user config (config/local/user.yml). Returns {} if absent. */
319
+ export function readUserConfig(): UserConfig {
320
+ try {
321
+ const raw = fs.readFileSync(USER_CONFIG_PATH, "utf-8");
322
+ const parsed = yaml.parse(raw) as UserConfig | null;
323
+ return parsed ?? {};
324
+ } catch (err: unknown) {
325
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return {};
326
+ throw err;
327
+ }
328
+ }
329
+
330
+ /** Write the per-engineer user config to config/local/user.yml. */
331
+ export function writeUserConfig(cfg: UserConfig): void {
332
+ fs.mkdirSync(path.dirname(USER_CONFIG_PATH), { recursive: true });
333
+ const lines = [
334
+ "# Per-engineer preferences — gitignored, never committed",
335
+ yaml.stringify(cfg).trimEnd(),
336
+ ];
337
+ fs.writeFileSync(USER_CONFIG_PATH, lines.join("\n") + "\n", "utf-8");
338
+ }
339
+
340
+ /** Merge a partial patch into config/local/user.yml without clobbering fields. */
341
+ export function mergeUserConfig(patch: Partial<UserConfig>): UserConfig {
342
+ const current = readUserConfig();
343
+ const next: UserConfig = { ...current };
344
+
345
+ for (const [key, value] of Object.entries(patch) as Array<
346
+ [keyof UserConfig, UserConfig[keyof UserConfig] | undefined]
347
+ >) {
348
+ if (value === undefined) {
349
+ delete next[key];
350
+ } else {
351
+ next[key] = value as never;
352
+ }
353
+ }
354
+
355
+ writeUserConfig(next);
356
+ return next;
357
+ }
358
+
359
+ /**
360
+ * The GitHub org managed repos clone from. Den has no default: an org that
361
+ * needs one sets `github_org` in its config.
362
+ */
363
+ export function requireGithubOrg(config: DenConfig = readDenConfig()): string {
364
+ const org = config.github_org?.trim();
365
+ if (!org)
366
+ throw new Error(
367
+ `github_org is not set: add \`github_org: <your-org>\` to ${DEN_CONFIG_PATH}`,
368
+ );
369
+ return org;
370
+ }
371
+
372
+ /**
373
+ * Read the effective den config, merging the shared deployment config with
374
+ * per-engineer overrides from config/local/user.yml.
375
+ */
376
+ export function readDenConfig(): DenConfig {
377
+ // Read shared base config
378
+ let base: DenConfig;
379
+ try {
380
+ const raw = fs.readFileSync(DEN_CONFIG_PATH, "utf-8");
381
+ const parsed = yaml.parse(raw) as Partial<DenConfig> | null;
382
+ // `environments` may be malformed (scalar/object) in a hand-edited file —
383
+ // fall back to the default set rather than throwing on .filter below.
384
+ const envs = Array.isArray(parsed?.environments)
385
+ ? parsed.environments
386
+ : ALL_ENVIRONMENTS;
387
+ base = {
388
+ environments: envs.filter((e): e is Environment =>
389
+ ALL_ENVIRONMENTS.includes(e as Environment),
390
+ ),
391
+ repos_root: parsed?.repos_root,
392
+ github_org: parsed?.github_org,
393
+ orgs: parsed?.orgs,
394
+ graph: parsed?.graph,
395
+ org: parsed?.org,
396
+ applications: parsed?.applications,
397
+ release: parsed?.release,
398
+ service_env: parsed?.service_env,
399
+ env_health_urls: parsed?.env_health_urls,
400
+ security: parsed?.security,
401
+ test_accounts: parsed?.test_accounts,
402
+ defaults: parsed?.defaults,
403
+ secrets: parsed?.secrets,
404
+ };
405
+ } catch (err: unknown) {
406
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
407
+ base = { ...DEFAULT_CONFIG };
408
+ } else {
409
+ throw err;
410
+ }
411
+ }
412
+
413
+ // Apply per-engineer override: local_dev: false → strip "local" from environments
414
+ const user = readUserConfig();
415
+ if (user.local_dev === false) {
416
+ base.environments = base.environments.filter((e) => e !== "local");
417
+ }
418
+
419
+ return base;
420
+ }
421
+
422
+ /**
423
+ * Absolute path to the root directory where local repos are discovered.
424
+ * Defaults to the parent of the den root directory.
425
+ */
426
+ export function getReposRoot(): string {
427
+ const config = readDenConfig();
428
+ return path.resolve(ROOT, config.repos_root ?? "..");
429
+ }
430
+
431
+ /** True when the `local` environment is enabled for this engineer. */
432
+ export function isLocalEnabled(): boolean {
433
+ return readDenConfig().environments.includes("local");
434
+ }
435
+
436
+ // ── Security config (security pillar) ─────────────────────────────────────────
437
+
438
+ /** `security:` in config/den.yml. */
439
+ export interface SecurityConfig {
440
+ /**
441
+ * Hostnames the security pillar may point ZAP at. An entry matches that exact
442
+ * host or any subdomain of it (a leading "." is ignored). Extend per shell with
443
+ * DEN_SECURITY_EXTRA_HOSTS (comma-separated).
444
+ */
445
+ owned_hosts?: string[];
446
+ /**
447
+ * Also scan, unlisted, every locally run service with an HTTP health URL
448
+ * (unless its manifest says `security.skip`). Default false: only the
449
+ * targets manifests declare under `security:` are scanned.
450
+ */
451
+ auto_targets?: boolean;
452
+ }
453
+
454
+ /**
455
+ * Owned hosts when config/den.yml has no `security.owned_hosts`: none.
456
+ * Scanning is a deliberate per-org choice, so nothing is owned by default.
457
+ */
458
+ export const DEFAULT_SECURITY_OWNED_HOSTS: readonly string[] = [];
459
+
460
+ /**
461
+ * The configured owned-host allowlist (config/den.yml
462
+ * `security.owned_hosts`), lowercased. A missing or malformed value falls back
463
+ * to DEFAULT_SECURITY_OWNED_HOSTS; an explicit empty list means "nothing owned".
464
+ */
465
+ export function readSecurityOwnedHosts(
466
+ config: DenConfig = readDenConfig(),
467
+ ): string[] {
468
+ const hosts = config.security?.owned_hosts;
469
+ const list = Array.isArray(hosts) ? hosts : DEFAULT_SECURITY_OWNED_HOSTS;
470
+ return list
471
+ .filter((h): h is string => typeof h === "string")
472
+ .map((h) => h.trim().toLowerCase())
473
+ .filter((h) => h.length > 0);
474
+ }
475
+
476
+ /** Whether `security.auto_targets` is on (only an explicit `true` counts). */
477
+ export function readSecurityAutoTargets(
478
+ config: DenConfig = readDenConfig(),
479
+ ): boolean {
480
+ return config.security?.auto_targets === true;
481
+ }
482
+
483
+ // ── Release config (Release tab) ──────────────────────────────────────────────
484
+
485
+ /**
486
+ * Built-in release defaults: no environments or repos, deploys off. The org
487
+ * config's `release:` layers over this, personal edits via
488
+ * config/local/user.yml.
489
+ */
490
+ export const DEFAULT_RELEASE_CONFIG: ReleaseConfig = {
491
+ deploy: false,
492
+ // No built-in environments or repos: each instance lists its own.
493
+ environments: [],
494
+ staggerSeconds: 20,
495
+ pollSeconds: 15,
496
+ repos: [],
497
+ };
498
+
499
+ /** Merge one override layer (den.yml or user.yml) over a base release config. */
500
+ function mergeReleaseLayer(
501
+ base: ReleaseConfig,
502
+ layer?: Partial<ReleaseConfig> | null,
503
+ ): ReleaseConfig {
504
+ const merged: ReleaseConfig = {
505
+ deploy: base.deploy,
506
+ org: layer?.org ?? base.org,
507
+ environments:
508
+ Array.isArray(layer?.environments) && layer.environments.length
509
+ ? layer.environments
510
+ : base.environments,
511
+ staggerSeconds:
512
+ typeof layer?.staggerSeconds === "number"
513
+ ? layer.staggerSeconds
514
+ : base.staggerSeconds,
515
+ pollSeconds:
516
+ typeof layer?.pollSeconds === "number"
517
+ ? layer.pollSeconds
518
+ : base.pollSeconds,
519
+ repos: base.repos.map((r) => ({ ...r })),
520
+ ...(base.profiles || layer?.profiles
521
+ ? { profiles: { ...base.profiles, ...layer?.profiles } }
522
+ : {}),
523
+ };
524
+
525
+ if (Array.isArray(layer?.repos)) {
526
+ // A layer may add repos with only a subset of fields (e.g. a hand-edited
527
+ // user.yml with just name+wave), so treat entries as partial at runtime.
528
+ const patches = layer.repos as Array<
529
+ Partial<ReleaseRepo> & { name: string }
530
+ >;
531
+ const byName = new Map(merged.repos.map((r) => [r.name, r]));
532
+ for (const patch of patches) {
533
+ if (!patch?.name) continue;
534
+ const existing = byName.get(patch.name);
535
+ if (existing) Object.assign(existing, patch);
536
+ else byName.set(patch.name, { workflow: "cd.yml", wave: 0, ...patch });
537
+ }
538
+ merged.repos = [...byName.values()];
539
+ }
540
+ return merged;
541
+ }
542
+
543
+ /**
544
+ * Read the effective release config: built-in defaults ← config/den.yml
545
+ * `release:` (team base) ← config/local/user.yml `release:` (personal override).
546
+ * `org` falls back to the den `github_org` when unset at every layer.
547
+ */
548
+ export function readReleaseConfig(): ReleaseConfig {
549
+ const den = readDenConfig();
550
+ let cfg = mergeReleaseLayer(DEFAULT_RELEASE_CONFIG, den.release);
551
+ // Only the team layer may enable deploys; user.yml can't.
552
+ cfg.deploy = den.release?.deploy === true;
553
+ cfg = mergeReleaseLayer(cfg, readUserConfig().release);
554
+ if (!cfg.org) cfg.org = den.github_org;
555
+ return cfg;
556
+ }
557
+
558
+ /**
559
+ * Persist a personal release override to config/local/user.yml (gitignored).
560
+ * Used by the Release tab's wave editor — team defaults stay in den.yml.
561
+ */
562
+ export function writeUserReleaseConfig(release: Partial<ReleaseConfig>): void {
563
+ const { deploy: _ignored, ...rest } = release;
564
+ void _ignored;
565
+ mergeUserConfig({ release: rest });
566
+ }