@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,273 @@
1
+ /**
2
+ * Token Refresh Utility
3
+ * Check a saved browser session before running tests, and refresh it by
4
+ * letting the app's own auth SDK renew it when it has gone stale.
5
+ *
6
+ * "Signed in" means: opening `signedInPath` (environment config
7
+ * `auth.browser.signedInPath`, default "/") ends on the app origin under
8
+ * that path, rather than at the IdP or a login page.
9
+ */
10
+
11
+ import * as fs from "fs/promises";
12
+ import { chromium } from "@playwright/test";
13
+ import type { BrowserLoginConfig } from "../types/index.js";
14
+
15
+ interface StorageState {
16
+ cookies: Array<{
17
+ name: string;
18
+ value: string;
19
+ domain: string;
20
+ path: string;
21
+ expires: number;
22
+ httpOnly: boolean;
23
+ secure: boolean;
24
+ // Playwright's storageState cookie type requires the literal union.
25
+ sameSite: "Strict" | "Lax" | "None";
26
+ }>;
27
+ origins: Array<{
28
+ origin: string;
29
+ localStorage: Array<{
30
+ name: string;
31
+ value: string;
32
+ }>;
33
+ }>;
34
+ }
35
+
36
+ /**
37
+ * Check if auth tokens are expired and refresh if needed
38
+ */
39
+ export async function refreshAuthIfNeeded(
40
+ authFilePath: string,
41
+ baseUrl: string,
42
+ login: BrowserLoginConfig = {},
43
+ ): Promise<boolean> {
44
+ const signedInPath = normalisePath(login.signedInPath ?? "/");
45
+ try {
46
+ // Check if auth file exists
47
+ const fileExists = await fs
48
+ .access(authFilePath)
49
+ .then(() => true)
50
+ .catch(() => false);
51
+ if (!fileExists) {
52
+ console.log(`⚠️ No auth session found at: ${authFilePath}`);
53
+ return false;
54
+ }
55
+
56
+ // Read storage state
57
+ const storageStateContent = await fs.readFile(authFilePath, "utf-8");
58
+ const storageState: StorageState = JSON.parse(storageStateContent);
59
+
60
+ // Check if tokens are still valid
61
+ const isValid = await checkTokenValidity(
62
+ storageState,
63
+ baseUrl,
64
+ signedInPath,
65
+ );
66
+
67
+ if (isValid) {
68
+ console.log("✓ Auth session is still valid");
69
+ return true;
70
+ }
71
+
72
+ console.log("⚠️ Auth session expired, attempting to refresh...");
73
+
74
+ // Try to refresh using the saved session
75
+ const refreshed = await attemptRefresh(
76
+ authFilePath,
77
+ baseUrl,
78
+ storageState,
79
+ signedInPath,
80
+ );
81
+
82
+ if (refreshed) {
83
+ console.log("✓ Auth session refreshed successfully");
84
+ return true;
85
+ } else {
86
+ console.log(
87
+ "❌ Could not refresh auth session - manual re-authentication required",
88
+ );
89
+ return false;
90
+ }
91
+ } catch (error) {
92
+ console.warn(`Token refresh check failed: ${(error as Error).message}`);
93
+ return false;
94
+ }
95
+ }
96
+
97
+ function normalisePath(p: string): string {
98
+ return p.startsWith("/") ? p : `/${p}`;
99
+ }
100
+
101
+ /** True when `href` is on the app origin under `signedInPath`. */
102
+ export function isSignedInUrl(
103
+ href: string,
104
+ baseUrl: string,
105
+ signedInPath: string,
106
+ ): boolean {
107
+ try {
108
+ const url = new URL(href);
109
+ if (url.origin !== new URL(baseUrl).origin) return false;
110
+ const prefix = signedInPath.replace(/\/+$/, "");
111
+ return (
112
+ prefix === "" ||
113
+ url.pathname === prefix ||
114
+ url.pathname.startsWith(`${prefix}/`)
115
+ );
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ /** Wait (briefly) for any IdP redirect to land back on the app origin. */
122
+ async function settleOnApp(
123
+ page: { waitForURL(fn: (url: URL) => boolean, o: object): Promise<void> },
124
+ baseUrl: string,
125
+ ): Promise<void> {
126
+ const appOrigin = new URL(baseUrl).origin;
127
+ try {
128
+ await page.waitForURL((url) => url.origin === appOrigin, {
129
+ timeout: 5000,
130
+ });
131
+ } catch {
132
+ // Timeout is OK - the check below decides
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Check if the current session is still valid
138
+ */
139
+ async function checkTokenValidity(
140
+ storageState: StorageState,
141
+ baseUrl: string,
142
+ signedInPath: string,
143
+ ): Promise<boolean> {
144
+ let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined;
145
+ try {
146
+ // Launch headless browser with saved state
147
+ browser = await chromium.launch({ headless: true });
148
+ const context = await browser.newContext({ storageState });
149
+ const page = await context.newPage();
150
+
151
+ // Try to navigate to a protected route
152
+ await page.goto(baseUrl.replace(/\/$/, "") + signedInPath, {
153
+ waitUntil: "domcontentloaded",
154
+ timeout: 10000,
155
+ });
156
+ await settleOnApp(page, baseUrl);
157
+
158
+ // Still on the protected route (not redirected to a login) = signed in
159
+ return isSignedInUrl(page.url(), baseUrl, signedInPath);
160
+ } catch (error) {
161
+ const errorMsg = (error as Error).message;
162
+ // If the app isn't running, we can't validate - assume tokens might be valid
163
+ if (
164
+ errorMsg.includes("ERR_NAME_NOT_RESOLVED") ||
165
+ errorMsg.includes("ERR_CONNECTION_REFUSED")
166
+ ) {
167
+ console.log(" ℹ️ Cannot reach app - skipping validation");
168
+ return true; // Assume valid, let tests fail naturally if they're not
169
+ }
170
+ console.warn(` Token validity check failed: ${errorMsg}`);
171
+ return false;
172
+ } finally {
173
+ // Close on every path — a thrown goto previously leaked the Chromium process.
174
+ await browser?.close();
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Attempt to refresh the session
180
+ */
181
+ async function attemptRefresh(
182
+ authFilePath: string,
183
+ baseUrl: string,
184
+ oldStorageState: StorageState,
185
+ signedInPath: string,
186
+ ): Promise<boolean> {
187
+ let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined;
188
+ try {
189
+ // Refresh tokens usually live in HTTP-only cookies we can't touch
190
+ // directly. The best approach is to:
191
+ // 1. Load the page with the old session
192
+ // 2. Let the app's auth SDK handle the refresh automatically
193
+ // 3. Save the new session
194
+
195
+ browser = await chromium.launch({ headless: true });
196
+ const context = await browser.newContext({ storageState: oldStorageState });
197
+ const page = await context.newPage();
198
+
199
+ // Navigate to the app - its auth SDK might auto-refresh
200
+ await page.goto(baseUrl, {
201
+ waitUntil: "networkidle",
202
+ timeout: 30000,
203
+ });
204
+ await settleOnApp(page, baseUrl);
205
+
206
+ // Try to access a protected route
207
+ await page.goto(baseUrl.replace(/\/$/, "") + signedInPath, {
208
+ waitUntil: "domcontentloaded",
209
+ timeout: 15000,
210
+ });
211
+ await settleOnApp(page, baseUrl);
212
+
213
+ // Check if we made it to the protected route
214
+ const isAuthenticated = isSignedInUrl(page.url(), baseUrl, signedInPath);
215
+
216
+ if (isAuthenticated) {
217
+ // Save the refreshed session
218
+ await context.storageState({ path: authFilePath });
219
+ return true;
220
+ }
221
+
222
+ return false;
223
+ } catch (error) {
224
+ const errorMsg = (error as Error).message;
225
+ // If the app isn't running, we can't refresh - but this is OK
226
+ if (
227
+ errorMsg.includes("ERR_NAME_NOT_RESOLVED") ||
228
+ errorMsg.includes("ERR_CONNECTION_REFUSED")
229
+ ) {
230
+ console.log(" ℹ️ Cannot reach app - skipping refresh");
231
+ return true; // Assume OK, let tests handle the connection error
232
+ }
233
+ console.warn(` Token refresh attempt failed: ${errorMsg}`);
234
+ return false;
235
+ } finally {
236
+ // Close on every path — a thrown goto previously leaked the Chromium process.
237
+ await browser?.close();
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Get token expiry time in seconds (for manual token inspection)
243
+ */
244
+ function getTokenExpiry(token: string): number | null {
245
+ try {
246
+ // JWT tokens are base64 encoded
247
+ const parts = token.split(".");
248
+ if (parts.length !== 3) {
249
+ return null;
250
+ }
251
+
252
+ // Decode payload
253
+ const payload = JSON.parse(Buffer.from(parts[1], "base64").toString());
254
+ return payload.exp || null;
255
+ } catch {
256
+ return null;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Check if token is expired
262
+ */
263
+ export function isTokenExpired(token: string): boolean {
264
+ const expiry = getTokenExpiry(token);
265
+ if (!expiry) {
266
+ return true; // Assume expired if we can't parse
267
+ }
268
+
269
+ const now = Math.floor(Date.now() / 1000);
270
+ const bufferSeconds = 300; // 5 minute buffer
271
+
272
+ return now >= expiry - bufferSeconds;
273
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The startup banner for the CLI and web server: the org's `brand.banner`
3
+ * lines when configured, else the brand name spelled out. No product's art
4
+ * is built in.
5
+ */
6
+ import { readBrand } from "./brand.js";
7
+
8
+ export const DEN_BANNER_LINES: string[] = (() => {
9
+ const b = readBrand();
10
+ if (b.banner?.length) return b.banner;
11
+ return [` ${b.name.toUpperCase().split("").join(" ")}`];
12
+ })();
13
+
14
+ export const DEN_SUBTITLE = ` ${readBrand().tagline}`;
15
+ export const DEN_DIVIDER = " ──────────────────────────────────";
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Product identity shown to people: the banner, web page titles and the help
3
+ * header. Read from the org config's `brand:` block, defaulting to the
4
+ * profile's name (den.yml). Runtime names (env vars, files, directories) come
5
+ * from the profile, never from the brand.
6
+ *
7
+ * brand:
8
+ * name: Acme Dev # shown in titles and the banner
9
+ * tagline: Acme local dev and E2E harness
10
+ * banner: [ "...", "..." ] # optional ASCII art lines
11
+ */
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import * as yaml from "yaml";
15
+ import { getDistRoot } from "./paths.js";
16
+ import { product } from "./product.js";
17
+
18
+ export interface BrandConfig {
19
+ name: string;
20
+ tagline: string;
21
+ banner?: string[];
22
+ }
23
+
24
+ export function defaultBrand(): BrandConfig {
25
+ return { name: product().title, tagline: "Local dev and E2E harness" };
26
+ }
27
+
28
+ let cached: BrandConfig | null = null;
29
+
30
+ export function readBrand(root: string = getDistRoot()): BrandConfig {
31
+ if (cached) return cached;
32
+ const fallback = defaultBrand();
33
+ let raw: Partial<BrandConfig> = {};
34
+ try {
35
+ const parsed = yaml.parse(
36
+ fs.readFileSync(path.join(root, product().configFile), "utf-8"),
37
+ ) as { brand?: Partial<BrandConfig> } | null;
38
+ raw = parsed?.brand ?? {};
39
+ } catch {
40
+ /* no config — defaults */
41
+ }
42
+ cached = {
43
+ name: raw.name?.trim() || fallback.name,
44
+ tagline: raw.tagline?.trim() || fallback.tagline,
45
+ banner: Array.isArray(raw.banner) ? raw.banner.map(String) : undefined,
46
+ };
47
+ return cached;
48
+ }
49
+
50
+ /** The brand name as a slug: "Acme Dev" → "acme-dev". Names the web route. */
51
+ export function brandSlug(brand: BrandConfig = readBrand()): string {
52
+ const s = brand.name
53
+ .trim()
54
+ .toLowerCase()
55
+ .replace(/[^a-z0-9-]+/g, "-")
56
+ .replace(/^-+|-+$/g, "");
57
+ return s || product().name;
58
+ }
59
+
60
+ export function resetBrandCache(): void {
61
+ cached = null;
62
+ }
63
+
64
+ /** Put the brand name in an HTML page's <title> (pages carry the product title). */
65
+ export function brandHtmlTitle(html: string, brand: BrandConfig): string {
66
+ const title = product().title;
67
+ if (brand.name === title) return html;
68
+ return html.replace(
69
+ /<title>([^<]*)<\/title>/i,
70
+ (_m, t: string) =>
71
+ `<title>${t.split(title).join(escapeHtml(brand.name))}</title>`,
72
+ );
73
+ }
74
+
75
+ function escapeHtml(s: string): string {
76
+ return s.replace(/[&<>"]/g, (c) =>
77
+ c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;",
78
+ );
79
+ }
@@ -0,0 +1,131 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { tabExists, type TerminalApp } from "./terminal/tabs.js";
4
+ import { getStateDir } from "./paths.js";
5
+
6
+ export interface CliTabRecord {
7
+ address: string;
8
+ terminalApp: TerminalApp;
9
+ tabId: string;
10
+ startedAt: Date;
11
+ cwd?: string;
12
+ command?: string;
13
+ }
14
+
15
+ type CliTabRecordJSON = Omit<CliTabRecord, "startedAt"> & {
16
+ startedAt: string;
17
+ };
18
+
19
+ const _tabRegistry = new Map<string, CliTabRecord>();
20
+ let loadedFromPath: string | null = null;
21
+
22
+ function getTabsFile(): string {
23
+ return (
24
+ process.env.DEN_CLI_TABS_FILE ?? path.join(getStateDir(), "cli-tabs.json")
25
+ );
26
+ }
27
+
28
+ function toRecord(json: CliTabRecordJSON): CliTabRecord {
29
+ return {
30
+ ...json,
31
+ startedAt: new Date(json.startedAt),
32
+ };
33
+ }
34
+
35
+ function readTabsFile(filePath: string): Map<string, CliTabRecord> {
36
+ const map = new Map<string, CliTabRecord>();
37
+ try {
38
+ if (!fs.existsSync(filePath)) return map;
39
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as
40
+ | Record<string, CliTabRecordJSON>
41
+ | CliTabRecordJSON[];
42
+ const records = Array.isArray(parsed) ? parsed : Object.values(parsed);
43
+ for (const item of records) {
44
+ if (!item.address || !item.tabId || !item.terminalApp) continue;
45
+ map.set(item.address, toRecord(item));
46
+ }
47
+ } catch {
48
+ // Corrupt sidecar: leave the in-memory map empty.
49
+ }
50
+ return map;
51
+ }
52
+
53
+ function writeTabsFile(filePath: string, map: Map<string, CliTabRecord>): void {
54
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
55
+ const json: Record<string, CliTabRecordJSON> = {};
56
+ for (const [address, record] of map.entries()) {
57
+ json[address] = {
58
+ ...record,
59
+ startedAt: record.startedAt.toISOString(),
60
+ };
61
+ }
62
+
63
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
64
+ fs.writeFileSync(tmp, JSON.stringify(json, null, 2) + "\n", "utf-8");
65
+ fs.renameSync(tmp, filePath);
66
+ }
67
+
68
+ function loadTabs(force = false): void {
69
+ const filePath = getTabsFile();
70
+ if (!force && loadedFromPath === filePath) return;
71
+ _tabRegistry.clear();
72
+ for (const [address, record] of readTabsFile(filePath).entries()) {
73
+ _tabRegistry.set(address, record);
74
+ }
75
+ loadedFromPath = filePath;
76
+ }
77
+
78
+ function reloadForMutation(): string {
79
+ const filePath = getTabsFile();
80
+ loadTabs(true);
81
+ return filePath;
82
+ }
83
+
84
+ export function recordTab(
85
+ address: string,
86
+ info: Omit<CliTabRecord, "address" | "startedAt"> &
87
+ Partial<Pick<CliTabRecord, "startedAt">>,
88
+ ): CliTabRecord {
89
+ const filePath = reloadForMutation();
90
+ const record: CliTabRecord = {
91
+ address,
92
+ ...info,
93
+ startedAt: info.startedAt ?? new Date(),
94
+ };
95
+ _tabRegistry.set(address, record);
96
+ writeTabsFile(filePath, _tabRegistry);
97
+ return record;
98
+ }
99
+
100
+ export function lookupTab(address: string): CliTabRecord | null {
101
+ loadTabs();
102
+ return _tabRegistry.get(address) ?? null;
103
+ }
104
+
105
+ export function forgetTab(address: string): void {
106
+ const filePath = reloadForMutation();
107
+ _tabRegistry.delete(address);
108
+ writeTabsFile(filePath, _tabRegistry);
109
+ }
110
+
111
+ export function listTabs(): CliTabRecord[] {
112
+ loadTabs();
113
+ return Array.from(_tabRegistry.values());
114
+ }
115
+
116
+ export function cleanupStaleTabs(): void {
117
+ const filePath = reloadForMutation();
118
+ let changed = false;
119
+ for (const [address, record] of _tabRegistry.entries()) {
120
+ try {
121
+ if (!tabExists(record.tabId, record.terminalApp)) {
122
+ _tabRegistry.delete(address);
123
+ changed = true;
124
+ }
125
+ } catch {
126
+ // Automation may be denied while a tab still exists. Keep the record so a
127
+ // later `den down` can retry closing it.
128
+ }
129
+ }
130
+ if (changed) writeTabsFile(filePath, _tabRegistry);
131
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Shared constants — single source of truth for values used across modules.
3
+ *
4
+ * If a value appears in 2+ files, it belongs here.
5
+ * If it's specific to one module (e.g. a Playwright config timeout), keep it local.
6
+ */
7
+
8
+ import { envKey } from "./product.js";
9
+ // ── 1Password strategy (only used when secrets.provider: 1password) ──────────────────────────────────────────────────────────────
10
+
11
+ /**
12
+ * Default 1Password account domain used by every `op` invocation across den.
13
+ * Kept in sync with the `OP_ACCOUNT ?= …` default in the Makefile. Scripts that
14
+ * may run outside `make` (setup-1password, auth-configure)
15
+ * set `process.env.OP_ACCOUNT ||= OP_ACCOUNT_DEFAULT` at entry so op picks up
16
+ * the right account even when invoked directly via `npx tsx`.
17
+ */
18
+ export const OP_ACCOUNT_DEFAULT = "example.1password.com";
19
+
20
+ /**
21
+ * Default vault for shared test accounts. Deprecated: the vault is org data —
22
+ * read it with testAccountsVault() (src/shared/auth/test-accounts.ts), which
23
+ * honours `test_accounts.vault` in the org config and falls back to this.
24
+ */
25
+ export const OP_TEST_VAULT = "test-accounts";
26
+
27
+ /** Vault containing per-engineer API keys (1Password's built-in personal vault). */
28
+ export const OP_API_KEYS_VAULT = "Employee";
29
+
30
+ /** Item within OP_API_KEYS_VAULT holding the developer's API keys. */
31
+ export const OP_DEV_KEYS_ITEM = "Developer API Keys";
32
+
33
+ /**
34
+ * Field within OP_DEV_KEYS_ITEM where the user's local admin (sudo) password
35
+ * is optionally stored. Populated when the user checks "Save to 1Password" on
36
+ * the den local-dev unlock banner; read on server startup so subsequent
37
+ * sessions unlock without prompting.
38
+ */
39
+ export const OP_DEN_SUDO_FIELD = `${envKey("LOCAL_ADMIN_PWD")}`;
40
+
41
+ // ── Timeouts ─────────────────────────────────────────────────────────────────
42
+
43
+ /** Health check HTTP/shell ping timeout. */
44
+ export const HEALTH_CHECK_TIMEOUT_MS = 3_000;
45
+ /** Hosted environments (beta, prod) may be scaled to zero; a cold start takes seconds. */
46
+ export const HOSTED_HEALTH_CHECK_TIMEOUT_MS = 10_000;
47
+
48
+ /** Timeout for spawning short-lived CLI subprocesses (open, sudo, git). */
49
+ export const CLI_SPAWN_TIMEOUT_MS = 5_000;
50
+
51
+ /** Timeout for `gh api` calls (branch listing, repo search). */
52
+ export const GH_API_TIMEOUT_MS = 15_000;
53
+
54
+ /** Timeout for 1Password `op` CLI commands.
55
+ * Generous so macOS Touch ID biometric prompts have time to land
56
+ * (≤10s killed the loadtest seed mid-mint when the prompt took >10s). */
57
+ export const OP_CLI_TIMEOUT_MS = 60_000;
58
+
59
+ /** Git remote HEAD check timeout (fast operation, low timeout). */
60
+ export const GIT_REMOTE_TIMEOUT_MS = 2_000;
61
+
62
+ /** Time to wait for a daemon to produce initial output before retrying. */
63
+ export const DAEMON_QUICK_EXIT_MS = 3_000;
64
+
65
+ /** Delay between daemon log stream retries. */
66
+ export const DAEMON_RETRY_DELAY_MS = 5_000;
67
+
68
+ /** Lines of immediate context a live log stream starts with (deeper history
69
+ * is paged on demand via /api/service/history). */
70
+ export const LIVE_TAIL_LINES = 500;
71
+
72
+ /** Window of time to show detached process output before closing the stream. */
73
+ export const DETACH_WINDOW_MS = 5_000;
74
+
75
+ /** Max time to wait for a `depends_on: { wait: true }` dependency to become healthy before starting the dependent. */
76
+ export const READINESS_TIMEOUT_MS = 60_000;
77
+
78
+ /** Poll interval while waiting for a `wait: true` dependency to become healthy. */
79
+ export const READINESS_INTERVAL_MS = 1_000;
80
+
81
+ /** Daemon graceful shutdown timeout. */
82
+ export const DAEMON_SHUTDOWN_GRACE_MS = 5_000;
83
+
84
+ // ── Cache TTLs ───────────────────────────────────────────────────────────────
85
+
86
+ /** TTL for cached GitHub branch listings. */
87
+ export const BRANCH_CACHE_TTL_MS = 60_000;
88
+
89
+ /** TTL for cached GitHub repo/org listings. */
90
+ export const REPO_CACHE_TTL_MS = 5 * 60_000;
91
+
92
+ // ── Log rotation ─────────────────────────────────────────────────────────────
93
+
94
+ /**
95
+ * Size at which a service log is rotated. Service logs are written by DETACHED
96
+ * children holding an inherited O_APPEND fd, so a chatty one grows without
97
+ * bound — a localstack tail was measured at 8.6 GB.
98
+ */
99
+ export const MAX_LOG_BYTES = 128 * 1024 * 1024;
100
+
101
+ /** How much of the tail is preserved into `<name>.log.1` when rotating. */
102
+ export const ROTATED_KEEP_BYTES = 32 * 1024 * 1024;
103
+
104
+ /** How often the server sweeps the log tree for oversized files. */
105
+ export const LOG_SWEEP_INTERVAL_MS = 15 * 60_000;
106
+
107
+ // ── Test file patterns ───────────────────────────────────────────────────────
108
+
109
+ /** File extension patterns per test pillar. */
110
+ export const TEST_FILE_EXTENSIONS: Record<string, string[]> = {
111
+ smoke: [".smoke.ts"],
112
+ synthetic: [".spec.ts", ".test.ts"],
113
+ integration: [".spec.ts", ".test.ts"],
114
+ performance: [".js"],
115
+ };
116
+
117
+ /** Directories to skip when scanning for test modules. */
118
+ export const SKIP_TEST_DIRS = [
119
+ "user-flows",
120
+ "helpers",
121
+ "fixtures",
122
+ "utils",
123
+ ] as const;
124
+
125
+ // ── Server ───────────────────────────────────────────────────────────────────
126
+
127
+ /** Default den web server port. */
128
+ export const DEFAULT_PORT = 7777;