@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,149 @@
1
+ /**
2
+ * Backend-agnostic developer-secrets setup.
3
+ *
4
+ * Dispatches to the strategy selected by `secrets.provider` in
5
+ * config/den.yml (override: DEN_SECRETS_PROVIDER or
6
+ * config/local/user.yml). See src/shared/secrets.
7
+ *
8
+ * Usage:
9
+ * make den-setup-secrets — interactive setup
10
+ * npx tsx scripts/setup-secrets.ts --check — non-interactive; exit 0 when ready
11
+ * npx tsx scripts/setup-secrets.ts --cli — print "<provider> <binary> <formula>"
12
+ * npx tsx scripts/setup-secrets.ts --gate — like --check, but a backend that
13
+ * is not ready only warns unless
14
+ * secrets.required is true
15
+ */
16
+ import { spawnSync } from "child_process";
17
+ import * as path from "path";
18
+ import { fileURLToPath } from "url";
19
+ import chalk from "chalk";
20
+ import {
21
+ getSecretsProvider,
22
+ readSecretsConfig,
23
+ } from "../src/shared/secrets/index.js";
24
+
25
+ import { envKey, product } from "../src/shared/product.js";
26
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
27
+ const check = process.argv.includes("--check");
28
+ const gate = process.argv.includes("--gate");
29
+
30
+ function run(cmd: string, args: string[]): number {
31
+ return spawnSync(cmd, args, { stdio: "inherit", cwd: ROOT }).status ?? 1;
32
+ }
33
+
34
+ function installed(cmd: string): boolean {
35
+ return !spawnSync(cmd, ["--version"], { stdio: "ignore" }).error;
36
+ }
37
+
38
+ const INSTALL: Record<string, string> = {
39
+ doppler: "brew install dopplerhq/cli/doppler",
40
+ op: "brew install 1password-cli",
41
+ };
42
+
43
+ function otherBackends(current: string): string {
44
+ return (
45
+ "Or pick another backend for just you (config/local/user.yml):\n" +
46
+ " secrets:\n" +
47
+ ` provider: env # or 1password / doppler (team default: ${current})\n` +
48
+ ` or for one shell: ${envKey("SECRETS_PROVIDER")}=env make web`
49
+ );
50
+ }
51
+
52
+ async function main(): Promise<number> {
53
+ const provider = getSecretsProvider();
54
+ const cfg = readSecretsConfig();
55
+ const label = `secrets backend: ${provider.name}`;
56
+
57
+ // `--cli`: print "<provider> <binary> <brew formula>" for make setup's
58
+ // install step ("env -" when the backend needs no CLI).
59
+ if (process.argv.includes("--cli")) {
60
+ const bin =
61
+ provider.name === "doppler"
62
+ ? "doppler"
63
+ : provider.name === "1password"
64
+ ? "op"
65
+ : "";
66
+ const formula = bin ? INSTALL[bin].replace(/^brew install /, "") : "";
67
+ console.log(`${provider.name} ${bin || "-"} ${formula || "-"}`);
68
+ return 0;
69
+ }
70
+
71
+ if (check || gate) {
72
+ const r = await provider.preflight();
73
+ if (r.ok) return 0;
74
+ if (gate && !cfg.required) {
75
+ console.error(
76
+ chalk.yellow(
77
+ `⚠️ ${label} is not ready — continuing.\n` +
78
+ " Test logins and the saved sudo password need it; run " +
79
+ `\`make ${product().cli}-setup-secrets\` when you want them.`,
80
+ ),
81
+ );
82
+ return 0;
83
+ }
84
+ console.error(chalk.red(`✗ ${label}\n${r.message ?? ""}`));
85
+ return 1;
86
+ }
87
+
88
+ console.log(
89
+ chalk.bold(`\n${product().title} developer secrets (${label})\n`),
90
+ );
91
+ switch (provider.name) {
92
+ case "doppler": {
93
+ if (!installed("doppler")) {
94
+ console.error(
95
+ chalk.red("✗ The Doppler CLI is not installed.\n") +
96
+ ` Install it: ${INSTALL.doppler}\n then re-run: make ${product().cli}-setup-secrets\n\n ` +
97
+ otherBackends(provider.name),
98
+ );
99
+ return 1;
100
+ }
101
+ if (!(await provider.isReady())) {
102
+ console.log("→ doppler login");
103
+ if (run("doppler", ["login"]) !== 0) return 1;
104
+ }
105
+ const scope: string[] = [];
106
+ if (cfg.doppler?.project) scope.push("--project", cfg.doppler.project);
107
+ if (cfg.doppler?.config) scope.push("--config", cfg.doppler.config);
108
+ console.log(`→ doppler setup ${scope.join(" ")}`.trim());
109
+ if (
110
+ run("doppler", ["setup", "--no-interactive", ...scope]) !== 0 &&
111
+ scope.length === 0
112
+ ) {
113
+ run("doppler", ["setup"]);
114
+ }
115
+ break;
116
+ }
117
+ case "1password":
118
+ if (!installed("op")) {
119
+ console.error(
120
+ chalk.red("✗ The 1Password CLI (op) is not installed.\n") +
121
+ ` Install it: ${INSTALL.op}\n\n ` +
122
+ otherBackends(provider.name),
123
+ );
124
+ return 1;
125
+ }
126
+ return run(path.join(ROOT, "node_modules", ".bin", "tsx"), [
127
+ path.join(ROOT, "scripts", "setup-1password.ts"),
128
+ ]);
129
+ case "env":
130
+ console.log(
131
+ "The env backend reads secrets from the process environment.\n" +
132
+ `Export them yourself (direnv, CI secrets, \`doppler run -- ${product().cli} up\`).`,
133
+ );
134
+ break;
135
+ }
136
+ const r = await provider.preflight();
137
+ console.log(
138
+ r.ok ? chalk.green(`✓ ${label} ready`) : chalk.red(`✗ ${r.message}`),
139
+ );
140
+ return r.ok ? 0 : 1;
141
+ }
142
+
143
+ main().then(
144
+ (code) => process.exit(code),
145
+ (err: unknown) => {
146
+ console.error(err instanceof Error ? err.message : err);
147
+ process.exit(1);
148
+ },
149
+ );
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * View JSONL test results
4
+ * Shows summary, trends, and flaky tests
5
+ */
6
+
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import { fileURLToPath } from "url";
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const RESULTS_DIR = path.join(__dirname, "../results");
13
+
14
+ // Read all JSONL files
15
+ function readResults(pillar, environment, days = 7) {
16
+ const filename = path.join(RESULTS_DIR, `${pillar}-${environment}.jsonl`);
17
+
18
+ if (!fs.existsSync(filename)) {
19
+ return [];
20
+ }
21
+
22
+ const content = fs.readFileSync(filename, "utf8");
23
+ const lines = content
24
+ .trim()
25
+ .split("\n")
26
+ .filter((line) => line);
27
+
28
+ const cutoffDate = new Date();
29
+ cutoffDate.setDate(cutoffDate.getDate() - days);
30
+
31
+ return lines
32
+ .map((line) => JSON.parse(line))
33
+ .filter((result) => new Date(result.timestamp) > cutoffDate);
34
+ }
35
+
36
+ // Calculate summary stats
37
+ function calculateStats(results) {
38
+ const total = results.length;
39
+ const passed = results.filter((r) => r.status === "passed").length;
40
+ const failed = results.filter((r) => r.status === "failed").length;
41
+ const skipped = results.filter((r) => r.status === "skipped").length;
42
+
43
+ const avgDuration = results.reduce((sum, r) => sum + r.duration, 0) / total;
44
+
45
+ return {
46
+ total,
47
+ passed,
48
+ failed,
49
+ skipped,
50
+ avgDuration,
51
+ passRate: ((passed / total) * 100).toFixed(1),
52
+ };
53
+ }
54
+
55
+ // Find flaky tests (tests that sometimes pass, sometimes fail)
56
+ function findFlakyTests(results) {
57
+ const testResults = {};
58
+
59
+ results.forEach((result) => {
60
+ if (!testResults[result.test]) {
61
+ testResults[result.test] = { passed: 0, failed: 0, total: 0 };
62
+ }
63
+ testResults[result.test].total++;
64
+ if (result.status === "passed") testResults[result.test].passed++;
65
+ if (result.status === "failed") testResults[result.test].failed++;
66
+ });
67
+
68
+ return Object.entries(testResults)
69
+ .filter(([test, stats]) => stats.passed > 0 && stats.failed > 0)
70
+ .map(([test, stats]) => ({
71
+ test,
72
+ passRate: ((stats.passed / stats.total) * 100).toFixed(1),
73
+ runs: stats.total,
74
+ }))
75
+ .sort((a, b) => parseFloat(a.passRate) - parseFloat(b.passRate));
76
+ }
77
+
78
+ // Performance trends
79
+ function getPerformanceTrends(results, testName) {
80
+ return results
81
+ .filter((r) => r.test === testName)
82
+ .slice(-10) // Last 10 runs
83
+ .map((r) => ({
84
+ timestamp: new Date(r.timestamp).toLocaleString(),
85
+ duration: r.duration,
86
+ status: r.status,
87
+ }));
88
+ }
89
+
90
+ // Main
91
+ const command = process.argv[2] || "summary";
92
+ const pillar = process.argv[3] || "synthetic";
93
+ const environment = process.argv[4] || "local";
94
+
95
+ console.log(`\n📊 Test Results - ${pillar}/${environment}\n`);
96
+
97
+ const results = readResults(pillar, environment);
98
+
99
+ if (results.length === 0) {
100
+ console.log("No results found. Run some tests first!\n");
101
+ process.exit(0);
102
+ }
103
+
104
+ if (command === "summary") {
105
+ const stats = calculateStats(results);
106
+ console.log(`Total Tests: ${stats.total}`);
107
+ console.log(`✅ Passed: ${stats.passed} (${stats.passRate}%)`);
108
+ console.log(`❌ Failed: ${stats.failed}`);
109
+ console.log(`⏭️ Skipped: ${stats.skipped}`);
110
+ console.log(`⏱️ Avg Duration: ${stats.avgDuration.toFixed(0)}ms\n`);
111
+
112
+ // Recent failures
113
+ const recentFailures = results.filter((r) => r.status === "failed").slice(-5);
114
+ if (recentFailures.length > 0) {
115
+ console.log("Recent Failures:");
116
+ recentFailures.forEach((r) => {
117
+ console.log(` • ${r.test}`);
118
+ if (r.error) console.log(` ${r.error.substring(0, 80)}...`);
119
+ });
120
+ console.log();
121
+ }
122
+ }
123
+
124
+ if (command === "flaky") {
125
+ const flaky = findFlakyTests(results);
126
+ if (flaky.length === 0) {
127
+ console.log("✅ No flaky tests detected!\n");
128
+ } else {
129
+ console.log(`⚠️ Found ${flaky.length} flaky tests:\n`);
130
+ flaky.forEach(({ test, passRate, runs }) => {
131
+ console.log(` ${passRate}% pass rate (${runs} runs)`);
132
+ console.log(` ${test}\n`);
133
+ });
134
+ }
135
+ }
136
+
137
+ if (command === "trends") {
138
+ const testName = process.argv[5];
139
+ if (!testName) {
140
+ console.log(
141
+ "Usage: node view-results.js trends <pillar> <environment> <testName>\n",
142
+ );
143
+ process.exit(1);
144
+ }
145
+
146
+ const trends = getPerformanceTrends(results, testName);
147
+ console.log(`Performance trend for: ${testName}\n`);
148
+ trends.forEach(({ timestamp, duration, status }) => {
149
+ const statusIcon = status === "passed" ? "✅" : "❌";
150
+ console.log(`${statusIcon} ${timestamp}: ${duration}ms`);
151
+ });
152
+ console.log();
153
+ }
154
+
155
+ console.log(`📁 Results file: results/${pillar}-${environment}.jsonl`);
156
+ console.log(`📅 Showing last 7 days\n`);
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env bash
2
+ # Run the web UI as a long-lived macOS service (a launchd LaunchAgent):
3
+ # started at login, restarted if it exits, not tied to any terminal.
4
+ #
5
+ # service/install.sh install (or refresh) and start it
6
+ # service/install.sh uninstall stop it and remove the LaunchAgent
7
+ # service/install.sh status launchd's view of it
8
+ # service/install.sh print write the LaunchAgent plist to stdout only
9
+ #
10
+ # Works from Den's checkout and from a distribution, where it lives in
11
+ # node_modules/@monster-orange/den/service/. The distribution root is
12
+ # DEN_ROOT, else this checkout when it holds den.yml. The agent runs the
13
+ # engine's server with the node found on PATH now, from the distribution
14
+ # root; re-run install after changing node versions.
15
+ #
16
+ # DEN_SERVICE_LABEL launchd label (default local.<dist dir name>.web)
17
+ # DEN_SERVICE_LOGS log directory (default ~/Library/Logs/<dist dir name>)
18
+ set -euo pipefail
19
+
20
+ HERE="$(cd "$(dirname "$0")" && pwd)"
21
+ ENGINE="$(cd "$HERE/.." && pwd)"
22
+ if [ -n "${DEN_ROOT:-}" ]; then
23
+ DIST="$(cd "$DEN_ROOT" && pwd)"
24
+ elif [ -f "$ENGINE/den.yml" ]; then
25
+ DIST="$ENGINE"
26
+ else
27
+ echo "✗ set DEN_ROOT to your distribution's root (the directory with den.yml)" >&2
28
+ exit 1
29
+ fi
30
+ [ -f "$DIST/den.yml" ] || { echo "✗ $DIST has no den.yml" >&2; exit 1; }
31
+ NAME="$(basename "$DIST")"
32
+ LABEL="${DEN_SERVICE_LABEL:-local.$NAME.web}"
33
+ LOGS="${DEN_SERVICE_LOGS:-$HOME/Library/Logs/$NAME}"
34
+ PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
35
+ DOMAIN="gui/$(id -u)"
36
+
37
+ # tsx: the nearest node_modules/tsx at or above the engine (hoisted installs).
38
+ find_tsx() {
39
+ local d="$ENGINE"
40
+ while :; do
41
+ if [ -f "$d/node_modules/tsx/package.json" ]; then echo "$d/node_modules/tsx"; return 0; fi
42
+ [ "$d" = "/" ] && return 1
43
+ d="$(dirname "$d")"
44
+ done
45
+ }
46
+
47
+ write_plist() { # $1 = output file
48
+ NODE="$(command -v node)" || { echo "✗ node is not on PATH" >&2; exit 1; }
49
+ TSX="$(find_tsx)" || { echo "✗ tsx not installed: run npm ci in $DIST" >&2; exit 1; }
50
+ DOCKER_DIR="$(dirname "$(command -v docker || echo /usr/local/bin/docker)")"
51
+ SVC_PATH="$(dirname "$NODE"):$DOCKER_DIR:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
52
+ python3 -I - "$1" "$LABEL" "$NODE" "$TSX" "$ENGINE" "$DIST" "$SVC_PATH" "$HOME" "$LOGS" <<'PY'
53
+ import plistlib, sys
54
+ out, label, node, tsx, engine, dist, path, home, logs = sys.argv[1:]
55
+ data = {
56
+ "Label": label,
57
+ "ProgramArguments": [node, "--import", f"{tsx}/dist/loader.mjs",
58
+ f"{engine}/src/ui/server/index.ts", "--silent"],
59
+ "WorkingDirectory": dist,
60
+ "EnvironmentVariables": {"PATH": path, "HOME": home,
61
+ "DEN_ROOT": dist, "DEN_ENGINE_ROOT": engine},
62
+ "RunAtLoad": True,
63
+ "KeepAlive": True,
64
+ "ThrottleInterval": 10,
65
+ "ProcessType": "Background",
66
+ "StandardOutPath": f"{logs}/web.log",
67
+ "StandardErrorPath": f"{logs}/web.log",
68
+ }
69
+ if out == "-":
70
+ sys.stdout.buffer.write(plistlib.dumps(data))
71
+ else:
72
+ with open(out, "wb") as f:
73
+ plistlib.dump(data, f)
74
+ PY
75
+ }
76
+
77
+ case "${1:-install}" in
78
+ install)
79
+ mkdir -p "$LOGS" "$(dirname "$PLIST")"
80
+ write_plist "$PLIST.new"
81
+ plutil -lint "$PLIST.new" >/dev/null
82
+ launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
83
+ # bootout returns before launchd has let go of the label; a bootstrap
84
+ # straight after fails ("Bootstrap failed: 5") and leaves NO service.
85
+ # Wait for it to go, then retry bootstrap a few times.
86
+ for _ in $(seq 1 50); do
87
+ launchctl print "$DOMAIN/$LABEL" >/dev/null 2>&1 || break
88
+ sleep 0.2
89
+ done
90
+ mv "$PLIST.new" "$PLIST"
91
+ loaded=0
92
+ for _ in 1 2 3 4 5; do
93
+ if launchctl bootstrap "$DOMAIN" "$PLIST" 2>/dev/null; then loaded=1; break; fi
94
+ sleep 1
95
+ done
96
+ [ "$loaded" = 1 ] || { echo "✗ launchctl bootstrap failed for $LABEL (plist: $PLIST)" >&2; exit 1; }
97
+ launchctl kickstart -k "$DOMAIN/$LABEL"
98
+ echo "✓ $LABEL running (logs: $LOGS/web.log)"
99
+ ;;
100
+ uninstall)
101
+ launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
102
+ rm -f "$PLIST"
103
+ echo "✓ $LABEL removed"
104
+ ;;
105
+ print)
106
+ write_plist -
107
+ ;;
108
+ status)
109
+ launchctl print "$DOMAIN/$LABEL" | grep -E '^\s*(state|pid|last exit code|runs)' || echo "$LABEL is not loaded"
110
+ ;;
111
+ *) echo "usage: $0 [install|uninstall|status|print]" >&2; exit 2 ;;
112
+ esac
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Custom Playwright Fixtures for Integration (API) Tests
3
+ *
4
+ * Auth model: Authorization: Bearer <jwt>
5
+ * Tokens are minted by the environment's token provider (auth0, cognito,
6
+ * local-jwt) and cached to disk. See shared/auth/api-token.ts for details.
7
+ *
8
+ * Prerequisites:
9
+ * - Credentials available: the secrets backend (test_accounts in the org
10
+ * config) or CI ENV vars
11
+ * - For auth0: the "Password" grant type enabled for the app
12
+ * - Social-login accounts cannot be used for API token tests
13
+ */
14
+
15
+ import { test as base, request } from "@playwright/test";
16
+ import { envSelector } from "../../../shared/environment/env-selector.js";
17
+ import { getApiToken } from "../../../shared/auth/api-token.js";
18
+ import { openApiValidator } from "./openapi-validator.js";
19
+ import type {
20
+ Environment,
21
+ EnvironmentConfig,
22
+ } from "../../../shared/types/index.js";
23
+ import type { APIRequestContext } from "@playwright/test";
24
+
25
+ type CustomFixtures = {
26
+ environment: Environment;
27
+ envConfig: EnvironmentConfig;
28
+ /** Unauthenticated API context — for testing public endpoints or 401 behavior */
29
+ apiContext: APIRequestContext;
30
+ /** Admin JWT bearer token */
31
+ adminToken: string;
32
+ /** User JWT bearer token */
33
+ userToken: string;
34
+ /** Authenticated API context using admin token (Bearer) */
35
+ adminContext: APIRequestContext;
36
+ /** Authenticated API context using user token (Bearer) */
37
+ userContext: APIRequestContext;
38
+ /** Alias for adminContext — backward compatibility */
39
+ authenticatedContext: APIRequestContext;
40
+ /** Alias for adminToken — backward compatibility */
41
+ accessToken: string;
42
+ validator: typeof openApiValidator;
43
+ };
44
+
45
+ export const test = base.extend<CustomFixtures>({
46
+ // eslint-disable-next-line no-empty-pattern
47
+ environment: async ({}, use) => {
48
+ const env = (process.env.E2E_ENVIRONMENT as Environment) || "local";
49
+ await use(env);
50
+ },
51
+
52
+ envConfig: async ({ environment }, use) => {
53
+ const config = await envSelector.loadEnvironment(environment);
54
+ await use(config);
55
+ },
56
+
57
+ // ── JWT token fixtures ────────────────────────────────────────────────────
58
+
59
+ adminToken: async ({ environment }, use) => {
60
+ const token = await getApiToken("admin", environment);
61
+ await use(token);
62
+ },
63
+
64
+ userToken: async ({ environment }, use) => {
65
+ const token = await getApiToken("user", environment);
66
+ await use(token);
67
+ },
68
+
69
+ // Backward-compat alias
70
+ accessToken: async ({ adminToken }, use) => {
71
+ await use(adminToken);
72
+ },
73
+
74
+ // ── Request context fixtures ──────────────────────────────────────────────
75
+
76
+ /** Unauthenticated — use for testing 401 responses or public endpoints */
77
+ apiContext: async ({ envConfig }, use) => {
78
+ const context = await request.newContext({
79
+ baseURL: envConfig.baseUrls.api,
80
+ extraHTTPHeaders: {
81
+ Accept: "application/json",
82
+ "Content-Type": "application/json",
83
+ },
84
+ });
85
+ await use(context);
86
+ await context.dispose();
87
+ },
88
+
89
+ /** Admin-authenticated API context — use for admin-only endpoints */
90
+ adminContext: async ({ envConfig, adminToken }, use) => {
91
+ const context = await request.newContext({
92
+ baseURL: envConfig.baseUrls.api,
93
+ extraHTTPHeaders: {
94
+ Accept: "application/json",
95
+ "Content-Type": "application/json",
96
+ Authorization: `Bearer ${adminToken}`,
97
+ },
98
+ });
99
+ await use(context);
100
+ await context.dispose();
101
+ },
102
+
103
+ /** User-authenticated API context — use for user-scoped endpoints */
104
+ userContext: async ({ envConfig, userToken }, use) => {
105
+ const context = await request.newContext({
106
+ baseURL: envConfig.baseUrls.api,
107
+ extraHTTPHeaders: {
108
+ Accept: "application/json",
109
+ "Content-Type": "application/json",
110
+ Authorization: `Bearer ${userToken}`,
111
+ },
112
+ });
113
+ await use(context);
114
+ await context.dispose();
115
+ },
116
+
117
+ // Backward-compat alias
118
+ authenticatedContext: async ({ adminContext }, use) => {
119
+ await use(adminContext);
120
+ },
121
+
122
+ // eslint-disable-next-line no-empty-pattern
123
+ validator: async ({}, use) => {
124
+ await use(openApiValidator);
125
+ },
126
+ });
127
+
128
+ export { expect } from "@playwright/test";
@@ -0,0 +1,111 @@
1
+ /**
2
+ * OpenAPI Schema Validator
3
+ * Validates API responses against OpenAPI schemas
4
+ */
5
+
6
+ import Ajv from "ajv";
7
+ import addFormats from "ajv-formats";
8
+ import { readFile } from "fs/promises";
9
+ import { resolve } from "path";
10
+ import { enginePath } from "../../../shared/engine.js";
11
+
12
+ export class OpenAPIValidator {
13
+ private ajv: Ajv;
14
+ private schemas: Map<string, unknown> = new Map();
15
+
16
+ constructor() {
17
+ this.ajv = new Ajv({
18
+ allErrors: true,
19
+ verbose: true,
20
+ strict: false,
21
+ });
22
+ addFormats(this.ajv);
23
+ }
24
+
25
+ /**
26
+ * Load OpenAPI spec from file
27
+ */
28
+ async loadSpec(specPath: string): Promise<void> {
29
+ // read-only schema load from Den's own files, so worker cwd is irrelevant
30
+ const fullPath = resolve(enginePath("src/pillars/api/schemas"), specPath);
31
+
32
+ try {
33
+ const content = await readFile(fullPath, "utf-8");
34
+ const spec = JSON.parse(content);
35
+
36
+ // Extract schemas from OpenAPI spec
37
+ if (spec.components && spec.components.schemas) {
38
+ for (const [name, schema] of Object.entries(spec.components.schemas)) {
39
+ this.schemas.set(name, schema as object);
40
+ this.ajv.addSchema(schema as object, name);
41
+ }
42
+ }
43
+ } catch (error) {
44
+ throw new Error(
45
+ `Failed to load OpenAPI spec from ${specPath}: ${(error as Error).message}`,
46
+ );
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Validate response data against a schema
52
+ */
53
+ validate(
54
+ schemaName: string,
55
+ data: unknown,
56
+ ): { valid: boolean; errors: string[] } {
57
+ const schema = this.schemas.get(schemaName);
58
+
59
+ if (!schema) {
60
+ return {
61
+ valid: false,
62
+ errors: [
63
+ `Schema "${schemaName}" not found. Available schemas: ${Array.from(this.schemas.keys()).join(", ")}`,
64
+ ],
65
+ };
66
+ }
67
+
68
+ const valid = this.ajv.validate(schema, data);
69
+
70
+ if (!valid && this.ajv.errors) {
71
+ const errors = this.ajv.errors.map((err) => {
72
+ const path = err.instancePath || "root";
73
+ return `${path}: ${err.message}`;
74
+ });
75
+
76
+ return { valid: false, errors };
77
+ }
78
+
79
+ return { valid: true, errors: [] };
80
+ }
81
+
82
+ /**
83
+ * Validate and throw if invalid
84
+ */
85
+ validateOrThrow(schemaName: string, data: unknown): void {
86
+ const result = this.validate(schemaName, data);
87
+
88
+ if (!result.valid) {
89
+ throw new Error(
90
+ `Schema validation failed for "${schemaName}":\n${result.errors.join("\n")}`,
91
+ );
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get list of available schemas
97
+ */
98
+ getAvailableSchemas(): string[] {
99
+ return Array.from(this.schemas.keys());
100
+ }
101
+
102
+ /**
103
+ * Check if schema exists
104
+ */
105
+ hasSchema(schemaName: string): boolean {
106
+ return this.schemas.has(schemaName);
107
+ }
108
+ }
109
+
110
+ // Export singleton instance
111
+ export const openApiValidator = new OpenAPIValidator();