@monster-orange/den 0.0.0 → 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 +165 -1
  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 +99 -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,436 @@
1
+ /**
2
+ * Results Viewer
3
+ * Display test results in CLI
4
+ */
5
+
6
+ import Table from "cli-table3";
7
+ import chalk from "chalk";
8
+ import { resultReader } from "../../shared/results/result-reader.js";
9
+ import type {
10
+ Pillar,
11
+ Environment,
12
+ TestResult,
13
+ } from "../../shared/types/index.js";
14
+
15
+ /** Journeys row metadata extracted from the JSONL row (all optional on the wire). */
16
+ interface JourneyRowMetadata {
17
+ failureKind: string | null;
18
+ frictionCount: number;
19
+ snapshotCount: number;
20
+ snapshotsDir: string | null;
21
+ }
22
+
23
+ export class ResultsViewer {
24
+ /**
25
+ * Display summary of recent results
26
+ */
27
+ async displaySummary(days: number = 7): Promise<void> {
28
+ console.log(chalk.bold(`\n📊 Test Results Summary (Last ${days} days)`));
29
+ console.log(chalk.gray("━".repeat(80)));
30
+
31
+ const pillars: Pillar[] = [
32
+ "synthetic",
33
+ "integration",
34
+ "performance",
35
+ "journeys",
36
+ "security",
37
+ ];
38
+ const environments: Environment[] = ["local", "dev", "sandbox", "staging"];
39
+
40
+ for (const pillar of pillars) {
41
+ console.log(chalk.bold(`\n${this.formatPillarName(pillar)}:`));
42
+
43
+ const table = new Table({
44
+ head: [
45
+ chalk.cyan("Environment"),
46
+ chalk.cyan("Total"),
47
+ chalk.green("Passed"),
48
+ chalk.red("Failed"),
49
+ chalk.yellow("Skipped"),
50
+ chalk.blue("Pass Rate"),
51
+ ],
52
+ style: {
53
+ head: [],
54
+ border: ["gray"],
55
+ },
56
+ });
57
+
58
+ for (const env of environments) {
59
+ const summary = await resultReader.getSummary(pillar, env, days);
60
+
61
+ if (summary.total === 0) {
62
+ continue; // Skip environments with no results
63
+ }
64
+
65
+ table.push([
66
+ this.formatEnvironmentName(env),
67
+ summary.total.toString(),
68
+ chalk.green(summary.passed.toString()),
69
+ summary.failed > 0
70
+ ? chalk.red(summary.failed.toString())
71
+ : chalk.gray("0"),
72
+ summary.skipped > 0
73
+ ? chalk.yellow(summary.skipped.toString())
74
+ : chalk.gray("0"),
75
+ this.formatPassRate(summary.passRate),
76
+ ]);
77
+ }
78
+
79
+ if (table.length === 0) {
80
+ console.log(chalk.gray(" No results found"));
81
+ } else {
82
+ console.log(table.toString());
83
+ }
84
+ }
85
+
86
+ console.log(); // Empty line at end
87
+ }
88
+
89
+ /**
90
+ * Display detailed results for specific pillar/environment
91
+ */
92
+ async displayDetailed(
93
+ pillar: Pillar,
94
+ environment: Environment,
95
+ days: number = 7,
96
+ ): Promise<void> {
97
+ if (pillar === "journeys") {
98
+ return this.displayJourneysDetailed(environment, days);
99
+ }
100
+
101
+ console.log(
102
+ chalk.bold(
103
+ `\n📋 Detailed Results: ${this.formatPillarName(pillar)} / ${this.formatEnvironmentName(environment)}`,
104
+ ),
105
+ );
106
+ console.log(chalk.gray("━".repeat(80)));
107
+
108
+ const dateFrom = new Date();
109
+ dateFrom.setDate(dateFrom.getDate() - days);
110
+
111
+ const results = await resultReader.query({ pillar, environment, dateFrom });
112
+
113
+ if (results.length === 0) {
114
+ console.log(chalk.yellow("No results found"));
115
+ return;
116
+ }
117
+
118
+ const table = new Table({
119
+ head: [
120
+ chalk.cyan("Test"),
121
+ chalk.cyan("Status"),
122
+ chalk.cyan("Duration"),
123
+ chalk.cyan("User"),
124
+ chalk.cyan("Time"),
125
+ ],
126
+ colWidths: [40, 10, 12, 20, 20],
127
+ style: {
128
+ head: [],
129
+ border: ["gray"],
130
+ },
131
+ wordWrap: true,
132
+ });
133
+
134
+ // Show only recent 20 results
135
+ const recentResults = results.slice(0, 20);
136
+
137
+ for (const result of recentResults) {
138
+ table.push([
139
+ this.truncate(result.test, 38),
140
+ this.formatStatus(result.status),
141
+ this.formatDuration(result.duration),
142
+ this.truncate(result.user, 18),
143
+ this.formatTimestamp(result.timestamp),
144
+ ]);
145
+ }
146
+
147
+ console.log(table.toString());
148
+
149
+ if (results.length > 20) {
150
+ console.log(chalk.gray(`\n... and ${results.length - 20} more results`));
151
+ }
152
+
153
+ // Show summary
154
+ const summary = await resultReader.getSummary(pillar, environment, days);
155
+ console.log(chalk.bold("\nSummary:"));
156
+ console.log(` Total: ${summary.total}`);
157
+ console.log(` ${chalk.green("Passed:")} ${summary.passed}`);
158
+ console.log(` ${chalk.red("Failed:")} ${summary.failed}`);
159
+ console.log(` ${chalk.yellow("Skipped:")} ${summary.skipped}`);
160
+ console.log(` Pass Rate: ${this.formatPassRate(summary.passRate)}`);
161
+ console.log();
162
+ }
163
+
164
+ /**
165
+ * Display journeys results — one row per journey run from the JSONL
166
+ * rows, surfacing the deterministic verdict, failure_kind, friction
167
+ * count, and the snapshots directory for the run.
168
+ */
169
+ async displayJourneysDetailed(
170
+ environment: Environment,
171
+ days: number = 7,
172
+ ): Promise<void> {
173
+ console.log(
174
+ chalk.bold(
175
+ `\n🧭 Journey Results: ${this.formatEnvironmentName(environment)}`,
176
+ ),
177
+ );
178
+ console.log(chalk.gray("━".repeat(80)));
179
+
180
+ const dateFrom = new Date();
181
+ dateFrom.setDate(dateFrom.getDate() - days);
182
+
183
+ const results = await resultReader.query({
184
+ pillar: "journeys",
185
+ environment,
186
+ dateFrom,
187
+ });
188
+
189
+ if (results.length === 0) {
190
+ console.log(chalk.yellow("No results found"));
191
+ return;
192
+ }
193
+
194
+ const table = new Table({
195
+ head: [
196
+ chalk.cyan("Journey"),
197
+ chalk.cyan("Verdict"),
198
+ chalk.cyan("Failure kind"),
199
+ chalk.cyan("Friction"),
200
+ chalk.cyan("Snapshots"),
201
+ chalk.cyan("Time"),
202
+ ],
203
+ style: {
204
+ head: [],
205
+ border: ["gray"],
206
+ },
207
+ wordWrap: true,
208
+ });
209
+
210
+ const recentResults = results.slice(0, 20);
211
+ for (const result of recentResults) {
212
+ const meta = this.journeyRowMetadata(result);
213
+ table.push([
214
+ this.truncate(result.test, 38),
215
+ this.formatStatus(result.status),
216
+ meta.failureKind !== null
217
+ ? chalk.red(meta.failureKind)
218
+ : chalk.gray("—"),
219
+ meta.frictionCount > 0
220
+ ? chalk.yellow(meta.frictionCount.toString())
221
+ : chalk.gray("0"),
222
+ meta.snapshotsDir !== null
223
+ ? `${meta.snapshotCount} · ${meta.snapshotsDir}`
224
+ : meta.snapshotCount.toString(),
225
+ this.formatTimestamp(result.timestamp),
226
+ ]);
227
+ }
228
+
229
+ console.log(table.toString());
230
+
231
+ if (results.length > 20) {
232
+ console.log(chalk.gray(`\n... and ${results.length - 20} more results`));
233
+ }
234
+
235
+ const summary = await resultReader.getSummary(
236
+ "journeys",
237
+ environment,
238
+ days,
239
+ );
240
+ console.log(chalk.bold("\nSummary:"));
241
+ console.log(` Total: ${summary.total}`);
242
+ console.log(` ${chalk.green("Passed:")} ${summary.passed}`);
243
+ console.log(` ${chalk.red("Failed:")} ${summary.failed}`);
244
+ console.log(` Pass Rate: ${this.formatPassRate(summary.passRate)}`);
245
+ console.log();
246
+ }
247
+
248
+ /**
249
+ * Extract the journeys-specific metadata block from a JSONL row,
250
+ * tolerating rows written by older den versions (missing fields
251
+ * degrade to zero/absent — never a crash while browsing results).
252
+ */
253
+ private journeyRowMetadata(result: TestResult): JourneyRowMetadata {
254
+ const metadata = result.metadata ?? {};
255
+ const failureKind = metadata.failure_kind;
256
+ const frictionPoints = metadata.friction_points;
257
+ const snapshotCount = metadata.snapshot_count;
258
+ const snapshotsDir = metadata.snapshots_dir;
259
+
260
+ return {
261
+ failureKind: typeof failureKind === "string" ? failureKind : null,
262
+ frictionCount: Array.isArray(frictionPoints) ? frictionPoints.length : 0,
263
+ snapshotCount: typeof snapshotCount === "number" ? snapshotCount : 0,
264
+ snapshotsDir: typeof snapshotsDir === "string" ? snapshotsDir : null,
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Display flaky tests
270
+ */
271
+ async displayFlakyTests(pillar: Pillar, days: number = 30): Promise<void> {
272
+ console.log(
273
+ chalk.bold(`\n⚠️ Flaky Tests: ${this.formatPillarName(pillar)}`),
274
+ );
275
+ console.log(chalk.gray("━".repeat(80)));
276
+
277
+ const flakyTests = await resultReader.getFlakyTests(pillar, days);
278
+
279
+ if (flakyTests.length === 0) {
280
+ console.log(chalk.green("✓ No flaky tests found!"));
281
+ return;
282
+ }
283
+
284
+ const table = new Table({
285
+ head: [
286
+ chalk.cyan("Test"),
287
+ chalk.cyan("Runs"),
288
+ chalk.cyan("Failures"),
289
+ chalk.cyan("Flaky Rate"),
290
+ ],
291
+ colWidths: [50, 10, 12, 15],
292
+ style: {
293
+ head: [],
294
+ border: ["gray"],
295
+ },
296
+ wordWrap: true,
297
+ });
298
+
299
+ for (const flaky of flakyTests) {
300
+ table.push([
301
+ this.truncate(flaky.test, 48),
302
+ flaky.runs.toString(),
303
+ chalk.red(flaky.failures.toString()),
304
+ this.formatFlakyRate(flaky.flakyRate),
305
+ ]);
306
+ }
307
+
308
+ console.log(table.toString());
309
+ console.log();
310
+ }
311
+
312
+ /**
313
+ * Format pillar name for display
314
+ */
315
+ private formatPillarName(pillar: Pillar): string {
316
+ const names: Record<Pillar, string> = {
317
+ synthetic: "🌐 Synthetic",
318
+ integration: "🔗 Integration",
319
+ performance: "⚡ Performance",
320
+ smoke: "🔥 Smoke",
321
+ journeys: "🧭 Journeys",
322
+ security: "🛡 Security",
323
+ suites: "🧪 Suites",
324
+ };
325
+ return names[pillar];
326
+ }
327
+
328
+ /**
329
+ * Format environment name for display
330
+ */
331
+ private formatEnvironmentName(env: Environment): string {
332
+ const names: Record<Environment, string> = {
333
+ local: "🏠 Local",
334
+ dev: "🔧 Dev",
335
+ sandbox: "🏖️ Sandbox",
336
+ staging: "🎭 Staging",
337
+ beta: "🧪 Beta",
338
+ prod: "🚀 Prod",
339
+ production: "🚀 Production",
340
+ };
341
+ return names[env];
342
+ }
343
+
344
+ /**
345
+ * Format test status with color
346
+ */
347
+ private formatStatus(status: "passed" | "failed" | "skipped"): string {
348
+ switch (status) {
349
+ case "passed":
350
+ return chalk.green("✓ Passed");
351
+ case "failed":
352
+ return chalk.red("✗ Failed");
353
+ case "skipped":
354
+ return chalk.yellow("○ Skipped");
355
+ default:
356
+ return status;
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Format duration in human-readable format
362
+ */
363
+ private formatDuration(ms: number): string {
364
+ if (ms < 1000) {
365
+ return `${ms}ms`;
366
+ }
367
+ return `${(ms / 1000).toFixed(2)}s`;
368
+ }
369
+
370
+ /**
371
+ * Format timestamp in relative time
372
+ */
373
+ private formatTimestamp(timestamp: string): string {
374
+ const date = new Date(timestamp);
375
+ const now = new Date();
376
+ const diffMs = now.getTime() - date.getTime();
377
+ const diffMins = Math.floor(diffMs / 60000);
378
+ const diffHours = Math.floor(diffMs / 3600000);
379
+ const diffDays = Math.floor(diffMs / 86400000);
380
+
381
+ if (diffMins < 1) {
382
+ return "just now";
383
+ } else if (diffMins < 60) {
384
+ return `${diffMins}m ago`;
385
+ } else if (diffHours < 24) {
386
+ return `${diffHours}h ago`;
387
+ } else if (diffDays < 7) {
388
+ return `${diffDays}d ago`;
389
+ } else {
390
+ return date.toLocaleDateString();
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Format pass rate with color
396
+ */
397
+ private formatPassRate(rate: number): string {
398
+ const rateStr = `${rate.toFixed(1)}%`;
399
+
400
+ if (rate >= 95) {
401
+ return chalk.green(rateStr);
402
+ } else if (rate >= 80) {
403
+ return chalk.yellow(rateStr);
404
+ } else {
405
+ return chalk.red(rateStr);
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Format flaky rate with color
411
+ */
412
+ private formatFlakyRate(rate: number): string {
413
+ const rateStr = `${rate.toFixed(1)}%`;
414
+
415
+ if (rate >= 50) {
416
+ return chalk.red(rateStr);
417
+ } else if (rate >= 20) {
418
+ return chalk.yellow(rateStr);
419
+ } else {
420
+ return chalk.gray(rateStr);
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Truncate string with ellipsis
426
+ */
427
+ private truncate(str: string, maxLength: number): string {
428
+ if (str.length <= maxLength) {
429
+ return str;
430
+ }
431
+ return str.substring(0, maxLength - 3) + "...";
432
+ }
433
+ }
434
+
435
+ // Export singleton instance
436
+ export const resultsViewer = new ResultsViewer();
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Secrets preflight for `den up` / `restart` / `logs`.
3
+ *
4
+ * Backend-agnostic: shells out to `scripts/setup-secrets.ts --gate`, which
5
+ * dispatches to the configured strategy (Doppler, 1Password or env — see
6
+ * src/shared/secrets). A backend that is not ready only warns unless
7
+ * `secrets.required` is true. Synchronous so the CLI can gate before spawning work.
8
+ */
9
+ import {
10
+ spawnSync,
11
+ type SpawnSyncOptionsWithStringEncoding,
12
+ type SpawnSyncReturns,
13
+ } from "child_process";
14
+ import * as path from "path";
15
+ import { getDistRoot, getEngineRoot } from "../../shared/paths.js";
16
+ import { engineBin } from "../../shared/engine.js";
17
+
18
+ import { product } from "../../shared/product.js";
19
+ type SpawnSyncLike = (
20
+ command: string,
21
+ args: string[],
22
+ options: SpawnSyncOptionsWithStringEncoding,
23
+ ) => SpawnSyncReturns<string>;
24
+
25
+ export interface SecretsPreflightInvocation {
26
+ command: string;
27
+ args: string[];
28
+ cwd: string;
29
+ }
30
+
31
+ export interface SecretsPreflightOptions {
32
+ env?: NodeJS.ProcessEnv;
33
+ root?: string;
34
+ platform?: NodeJS.Platform;
35
+ spawnSyncImpl?: SpawnSyncLike;
36
+ stdio?: SpawnSyncOptionsWithStringEncoding["stdio"];
37
+ }
38
+
39
+ const truthy = (v: string | undefined) => v === "1" || v === "true";
40
+
41
+ export function shouldSkipSecretsPreflight(
42
+ env: NodeJS.ProcessEnv = process.env,
43
+ ): boolean {
44
+ return truthy(env.DEN_SKIP_SECRETS);
45
+ }
46
+
47
+ /**
48
+ * The gate runs Den's own script (engine root) against the distribution
49
+ * (cwd = its root, where the org config lives).
50
+ */
51
+ export function buildSecretsPreflightInvocation(
52
+ root = getDistRoot(),
53
+ platform: NodeJS.Platform = process.platform,
54
+ engine = getEngineRoot(),
55
+ ): SecretsPreflightInvocation {
56
+ return {
57
+ command: engineBin(platform === "win32" ? "tsx.cmd" : "tsx", engine),
58
+ args: [path.join(engine, "scripts", "setup-secrets.ts"), "--gate"],
59
+ cwd: root,
60
+ };
61
+ }
62
+
63
+ export function preflightSecrets(options: SecretsPreflightOptions = {}): void {
64
+ const env = options.env ?? process.env;
65
+ if (shouldSkipSecretsPreflight(env)) return;
66
+
67
+ const invocation = buildSecretsPreflightInvocation(
68
+ options.root,
69
+ options.platform,
70
+ );
71
+ const result = (options.spawnSyncImpl ?? spawnSync)(
72
+ invocation.command,
73
+ invocation.args,
74
+ {
75
+ cwd: invocation.cwd,
76
+ env: { ...process.env, ...env },
77
+ stdio: options.stdio ?? "inherit",
78
+ encoding: "utf-8",
79
+ },
80
+ );
81
+
82
+ if (result.status === 0) return;
83
+
84
+ const detail = result.error ? `\n${result.error.message}` : "";
85
+ throw new Error(
86
+ "Developer secrets are not configured or the secrets backend is not signed in.\n" +
87
+ `Run: make ${product().cli}-setup-secrets\n` +
88
+ `Bypass once: ${product().cli} up --skip-secrets or ${product().cli} restart --skip-secrets` +
89
+ detail,
90
+ );
91
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * `stack ls | up <name…> [--build] | down <name…> | restart <name…> | health <name…> | ps <name> | logs <name> [args]`
3
+ *
4
+ * Runs repos' Docker Compose projects with no host ports, each as its own
5
+ * project (`<stack>-<workspace>`), served by the edge at
6
+ * https://<route>.<stack>.<edge.domain>.
7
+ */
8
+ import chalk from "chalk";
9
+ import { readEdgeConfig } from "../../shared/edge/config.js";
10
+ import { routeUrl } from "../../shared/edge/render.js";
11
+ import { edgeUp } from "../../shared/edge/runtime.js";
12
+ import { readStacks } from "../../shared/stacks/config.js";
13
+ import {
14
+ getFocusedWorkspaceName,
15
+ getWorkspaceByName,
16
+ } from "../../shared/environment-manager/workspaces.js";
17
+ import { stackWorkspace } from "../../shared/stacks/render.js";
18
+ import { projectName, startOrder } from "../../shared/stacks/render.js";
19
+ import {
20
+ stackDown,
21
+ stackHealth,
22
+ stackLogs,
23
+ stackPs,
24
+ stackUp,
25
+ type StackContext,
26
+ } from "../../shared/stacks/runtime.js";
27
+
28
+ function workspaceFlag(args: string[]): {
29
+ fkWs: string;
30
+ ws: string;
31
+ service?: string;
32
+ rest: string[];
33
+ } {
34
+ const rest: string[] = [];
35
+ // The global -w/--workspace is parsed (and checked) before we run and
36
+ // arrives as DEN_WORKSPACE_OVERRIDE.
37
+ const fkWs =
38
+ process.env.DEN_WORKSPACE_OVERRIDE ||
39
+ getFocusedWorkspaceName() ||
40
+ "default";
41
+ let service: string | undefined;
42
+ for (let i = 0; i < args.length; i++) {
43
+ if (args[i] === "--service") service = args[++i];
44
+ else rest.push(args[i]);
45
+ }
46
+ const ws = process.env.DEN_STACK_WORKSPACE || stackWorkspace(fkWs);
47
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(ws))
48
+ throw new Error(
49
+ `workspace "${ws}" must be lowercase letters, digits and dashes`,
50
+ );
51
+ return { fkWs, ws, service, rest };
52
+ }
53
+
54
+ export async function cmdStack(argv: string[]): Promise<void> {
55
+ const { fkWs, ws, service, rest } = workspaceFlag(argv);
56
+ const [sub = "ls", ...args] = rest;
57
+ const edge = readEdgeConfig();
58
+ // Repo manifests come from that den workspace's checkouts.
59
+ const stacks = readStacks(undefined, {
60
+ workspace: getWorkspaceByName(fkWs) ? fkWs : undefined,
61
+ });
62
+ const ctx: StackContext = { edge, stacks, workspace: ws };
63
+ const names = args.filter((a) => !a.startsWith("-"));
64
+ const pick = () => {
65
+ if (!names.length)
66
+ throw new Error(
67
+ `Usage: stack ${sub} <name…> (known: ${Object.keys(stacks).join(", ")})`,
68
+ );
69
+ return names;
70
+ };
71
+
72
+ switch (sub) {
73
+ case "ls":
74
+ for (const s of Object.values(stacks)) {
75
+ console.log(
76
+ chalk.bold(`${s.name}`) +
77
+ chalk.gray(
78
+ ` ${s.repo}${s.dir ? `/${s.dir}` : ""} project ${projectName(s.name, ws)}`,
79
+ ),
80
+ );
81
+ for (const route of Object.keys(s.routes))
82
+ console.log(
83
+ ` ${routeUrl(`${route}.${s.name}`, edge)} → ${s.routes[route]}`,
84
+ );
85
+ if (s.uses.length)
86
+ console.log(chalk.gray(` uses ${s.uses.join(", ")}`));
87
+ }
88
+ return;
89
+ case "up": {
90
+ // Re-render the edge so its cert covers every stack, then start the stacks.
91
+ await edgeUp(edge);
92
+ for (const n of startOrder(pick(), stacks)) {
93
+ console.log(chalk.cyan(`\n▶ ${n} (${projectName(n, ws)})`));
94
+ stackUp(ctx, stacks[n], { build: args.includes("--build") });
95
+ for (const route of Object.keys(stacks[n].routes))
96
+ console.log(chalk.green(` ✓ ${routeUrl(`${route}.${n}`, edge)}`));
97
+ }
98
+ return;
99
+ }
100
+ case "restart":
101
+ await edgeUp(edge);
102
+ for (const n of pick()) {
103
+ console.log(chalk.cyan(`↻ ${n}`));
104
+ stackDown(ctx, stacks[n]);
105
+ stackUp(ctx, stacks[n], { build: args.includes("--build") });
106
+ }
107
+ return;
108
+ case "health": {
109
+ let ok = true;
110
+ for (const n of pick()) {
111
+ const h = stackHealth(ctx, stacks[n], service);
112
+ ok &&= h.ok;
113
+ console.log(
114
+ `${h.ok ? chalk.green("✓") : chalk.red("✗")} ${n} ${h.detail}`,
115
+ );
116
+ }
117
+ process.exitCode = ok ? 0 : 1;
118
+ return;
119
+ }
120
+ case "down":
121
+ for (const n of startOrder(pick(), stacks)
122
+ .reverse()
123
+ .filter((n) => names.includes(n))) {
124
+ console.log(chalk.cyan(`▼ ${n}`));
125
+ stackDown(ctx, stacks[n]);
126
+ }
127
+ return;
128
+ case "ps":
129
+ for (const n of pick())
130
+ console.log(chalk.bold(n) + "\n" + stackPs(ctx, stacks[n]));
131
+ return;
132
+ case "logs":
133
+ process.exitCode = stackLogs(
134
+ ctx,
135
+ stacks[pick()[0]],
136
+ args.filter((a) => a !== names[0]),
137
+ );
138
+ return;
139
+ default:
140
+ throw new Error(
141
+ `Unknown stack command "${sub}". Use: ls | up | down | restart | health | ps | logs`,
142
+ );
143
+ }
144
+ }