@monster-orange/den 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +68 -0
  5. package/docs/distributions.md +499 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +101 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +403 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +124 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +757 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/agent-actor.ts +77 -0
  91. package/src/shared/applications.ts +138 -0
  92. package/src/shared/auth/api-token.ts +416 -0
  93. package/src/shared/auth/auth0-helper.ts +284 -0
  94. package/src/shared/auth/headless-login.ts +265 -0
  95. package/src/shared/auth/jwt-helper.ts +186 -0
  96. package/src/shared/auth/sudo-vault.ts +69 -0
  97. package/src/shared/auth/test-accounts.ts +98 -0
  98. package/src/shared/auth/token-injection.ts +184 -0
  99. package/src/shared/auth/token-providers/auth0.ts +80 -0
  100. package/src/shared/auth/token-providers/index.ts +182 -0
  101. package/src/shared/auth/token-refresh.ts +273 -0
  102. package/src/shared/banner.ts +15 -0
  103. package/src/shared/brand.ts +79 -0
  104. package/src/shared/cli-tabs.ts +131 -0
  105. package/src/shared/constants.ts +128 -0
  106. package/src/shared/den-config.ts +688 -0
  107. package/src/shared/dev-metrics/emit.ts +354 -0
  108. package/src/shared/edge/config.ts +229 -0
  109. package/src/shared/edge/render.ts +221 -0
  110. package/src/shared/edge/runtime.ts +268 -0
  111. package/src/shared/edge/status.ts +74 -0
  112. package/src/shared/edge/types.ts +62 -0
  113. package/src/shared/engine.ts +67 -0
  114. package/src/shared/environment/aws-profiles.ts +49 -0
  115. package/src/shared/environment/env-selector.ts +161 -0
  116. package/src/shared/environment/playwright-env.ts +82 -0
  117. package/src/shared/environment-manager/catalog.ts +77 -0
  118. package/src/shared/environment-manager/clone-guard.ts +163 -0
  119. package/src/shared/environment-manager/dag.ts +286 -0
  120. package/src/shared/environment-manager/default-workspace.ts +182 -0
  121. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  122. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  123. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  124. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  125. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  126. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  127. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  128. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  129. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  130. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  131. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  132. package/src/shared/environment-manager/derivation/text.ts +40 -0
  133. package/src/shared/environment-manager/derivation/types.ts +69 -0
  134. package/src/shared/environment-manager/health.ts +635 -0
  135. package/src/shared/environment-manager/index.ts +6 -0
  136. package/src/shared/environment-manager/load-applications.ts +108 -0
  137. package/src/shared/environment-manager/log-tail.ts +213 -0
  138. package/src/shared/environment-manager/make-targets.ts +78 -0
  139. package/src/shared/environment-manager/opctl.ts +335 -0
  140. package/src/shared/environment-manager/readiness.ts +93 -0
  141. package/src/shared/environment-manager/register-local.ts +72 -0
  142. package/src/shared/environment-manager/secrets-check.ts +260 -0
  143. package/src/shared/environment-manager/service-container.ts +47 -0
  144. package/src/shared/environment-manager/service-env.ts +110 -0
  145. package/src/shared/environment-manager/service-runner.ts +1603 -0
  146. package/src/shared/environment-manager/sync-targets.ts +66 -0
  147. package/src/shared/environment-manager/up-plan.ts +326 -0
  148. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  149. package/src/shared/environment-manager/workspaces.ts +860 -0
  150. package/src/shared/manifest/deps.ts +27 -0
  151. package/src/shared/manifest/merge-override.ts +75 -0
  152. package/src/shared/manifest/migrate-manifest.ts +101 -0
  153. package/src/shared/manifest/placement.ts +321 -0
  154. package/src/shared/manifest/read-manifest.ts +278 -0
  155. package/src/shared/manifest/schema.ts +313 -0
  156. package/src/shared/manifest/service-links.ts +93 -0
  157. package/src/shared/manifest/validate-layers.ts +229 -0
  158. package/src/shared/manifest/validate-manifest.ts +200 -0
  159. package/src/shared/manifest/workspace-manifests.ts +92 -0
  160. package/src/shared/open-url.ts +122 -0
  161. package/src/shared/org.ts +44 -0
  162. package/src/shared/paths.ts +190 -0
  163. package/src/shared/perf/optional-fields.ts +105 -0
  164. package/src/shared/perf/report-normalizer.ts +404 -0
  165. package/src/shared/perf/report-paths.ts +19 -0
  166. package/src/shared/perf/report-store.ts +110 -0
  167. package/src/shared/perf/report-types.ts +70 -0
  168. package/src/shared/perf/test-paths.ts +25 -0
  169. package/src/shared/process/log-rotation.ts +147 -0
  170. package/src/shared/process/spawn-streaming.ts +229 -0
  171. package/src/shared/product.ts +92 -0
  172. package/src/shared/profile.ts +141 -0
  173. package/src/shared/redact.ts +107 -0
  174. package/src/shared/repo-kind.ts +32 -0
  175. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  176. package/src/shared/reporting/slack-reporter.ts +340 -0
  177. package/src/shared/results/result-reader.ts +266 -0
  178. package/src/shared/results/result-writer.ts +168 -0
  179. package/src/shared/secrets/doppler.ts +126 -0
  180. package/src/shared/secrets/env.ts +48 -0
  181. package/src/shared/secrets/index.ts +197 -0
  182. package/src/shared/secrets/keys.ts +33 -0
  183. package/src/shared/secrets/onepassword.ts +75 -0
  184. package/src/shared/secrets/types.ts +52 -0
  185. package/src/shared/service-url.ts +119 -0
  186. package/src/shared/smoke/branch-resolver.ts +141 -0
  187. package/src/shared/smoke/git-remote.ts +70 -0
  188. package/src/shared/smoke/repo-cloner.ts +151 -0
  189. package/src/shared/stacks/config.ts +137 -0
  190. package/src/shared/stacks/manifest-commands.ts +118 -0
  191. package/src/shared/stacks/render.ts +253 -0
  192. package/src/shared/stacks/runtime.ts +334 -0
  193. package/src/shared/stacks/status.ts +99 -0
  194. package/src/shared/stacks/types.ts +53 -0
  195. package/src/shared/terminal/tabs.ts +212 -0
  196. package/src/shared/test-discovery/module-scanner.ts +569 -0
  197. package/src/shared/types/index.ts +701 -0
  198. package/src/shared/ui-marker.ts +19 -0
  199. package/src/ui/cli/applications-command.ts +148 -0
  200. package/src/ui/cli/config-command.ts +68 -0
  201. package/src/ui/cli/create-main.ts +9 -0
  202. package/src/ui/cli/create.ts +360 -0
  203. package/src/ui/cli/edge-command.ts +81 -0
  204. package/src/ui/cli/flag-parser.ts +246 -0
  205. package/src/ui/cli/graph-command.ts +570 -0
  206. package/src/ui/cli/health-display.ts +63 -0
  207. package/src/ui/cli/index.ts +3742 -0
  208. package/src/ui/cli/init.ts +855 -0
  209. package/src/ui/cli/invocation-target.ts +175 -0
  210. package/src/ui/cli/logs-command.ts +104 -0
  211. package/src/ui/cli/mcp-main.ts +13 -0
  212. package/src/ui/cli/mcp-stdout-guard.ts +10 -0
  213. package/src/ui/cli/module-selection.ts +31 -0
  214. package/src/ui/cli/prompts.ts +767 -0
  215. package/src/ui/cli/results-viewer.ts +436 -0
  216. package/src/ui/cli/secrets-preflight.ts +91 -0
  217. package/src/ui/cli/stack-command.ts +117 -0
  218. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  219. package/src/ui/cli/test-runner.ts +1319 -0
  220. package/src/ui/cli/web-command.ts +1073 -0
  221. package/src/ui/mcp/api.ts +284 -0
  222. package/src/ui/mcp/guards.ts +141 -0
  223. package/src/ui/mcp/server.ts +147 -0
  224. package/src/ui/mcp/tools.ts +565 -0
  225. package/src/ui/server/audit.ts +71 -0
  226. package/src/ui/server/changelog-parser.ts +105 -0
  227. package/src/ui/server/daemon-reaper.ts +129 -0
  228. package/src/ui/server/editor-manager.ts +301 -0
  229. package/src/ui/server/github-service.ts +223 -0
  230. package/src/ui/server/graph-prefs.ts +89 -0
  231. package/src/ui/server/health-row.ts +15 -0
  232. package/src/ui/server/http-helpers.ts +98 -0
  233. package/src/ui/server/index.ts +3350 -0
  234. package/src/ui/server/log-history.ts +453 -0
  235. package/src/ui/server/perf-report.ts +42 -0
  236. package/src/ui/server/public/environments.html +496 -0
  237. package/src/ui/server/public/favicon.ico +0 -0
  238. package/src/ui/server/public/graph-model.js +416 -0
  239. package/src/ui/server/public/graph.css +976 -0
  240. package/src/ui/server/public/graph.js +986 -0
  241. package/src/ui/server/public/header.js +1373 -0
  242. package/src/ui/server/public/index.html +2595 -0
  243. package/src/ui/server/public/local-dev.html +5344 -0
  244. package/src/ui/server/public/logs.html +1496 -0
  245. package/src/ui/server/public/panel.js +787 -0
  246. package/src/ui/server/public/perf-report.js +359 -0
  247. package/src/ui/server/public/release.html +2111 -0
  248. package/src/ui/server/public/services.html +1103 -0
  249. package/src/ui/server/public/workspace-picker.js +69 -0
  250. package/src/ui/server/readonly-routes.ts +200 -0
  251. package/src/ui/server/release-deploy-session.ts +246 -0
  252. package/src/ui/server/release-service.ts +1125 -0
  253. package/src/ui/server/sudo-store.ts +111 -0
  254. package/src/ui/server/ui-assets.ts +73 -0
  255. package/src/ui/server/workspace-routes.ts +2185 -0
@@ -0,0 +1,19 @@
1
+ import * as path from "path";
2
+
3
+ export const PERF_REPORT_ROOT = ".perf-report";
4
+ export const PERF_CURRENT_REPORT_DIR = path.join(PERF_REPORT_ROOT, "current");
5
+ export const PERF_CURRENT_REPORT_FILE = path.join(
6
+ PERF_CURRENT_REPORT_DIR,
7
+ "report.json",
8
+ );
9
+
10
+ export function getPerfSummaryArtifactName(moduleName: string): string {
11
+ return `${moduleName.replace(/\//g, "-")}.summary.json`;
12
+ }
13
+
14
+ export function getPerfSummaryArtifactPath(moduleName: string): string {
15
+ return path.join(
16
+ PERF_CURRENT_REPORT_DIR,
17
+ getPerfSummaryArtifactName(moduleName),
18
+ );
19
+ }
@@ -0,0 +1,110 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import type { PerfRunReport } from "./report-types.js";
4
+ import { getDistRoot } from "../paths.js";
5
+ import { buildPerfRunReport, type K6Summary } from "./report-normalizer.js";
6
+ import {
7
+ PERF_CURRENT_REPORT_DIR,
8
+ PERF_CURRENT_REPORT_FILE,
9
+ getPerfSummaryArtifactPath,
10
+ } from "./report-paths.js";
11
+
12
+ interface WriteCurrentPerfRunReportInput {
13
+ rootDir?: string;
14
+ environment: string;
15
+ modules: string[];
16
+ startedAt?: string;
17
+ finishedAt?: string;
18
+ }
19
+
20
+ export function writeCurrentPerfRunReport(
21
+ input: WriteCurrentPerfRunReportInput,
22
+ ): PerfRunReport {
23
+ // Default anchored to the den root, NOT process.cwd(): this WRITES
24
+ // .perf-report/, and perf children spawned by the CLI inherit an arbitrary
25
+ // user cwd (bin/den never cds).
26
+ const rootDir = input.rootDir ?? getDistRoot();
27
+ const report = buildPerfRunReport({
28
+ environment: input.environment,
29
+ modules: input.modules.map((moduleName) => ({
30
+ module: moduleName,
31
+ summary: readSummaryArtifact(rootDir, moduleName),
32
+ artifactPath: getPerfSummaryArtifactPath(moduleName),
33
+ startedAt: input.startedAt,
34
+ finishedAt: input.finishedAt,
35
+ })),
36
+ startedAt: input.startedAt,
37
+ finishedAt: input.finishedAt,
38
+ });
39
+
40
+ fs.mkdirSync(path.join(rootDir, PERF_CURRENT_REPORT_DIR), {
41
+ recursive: true,
42
+ });
43
+ fs.writeFileSync(
44
+ path.join(rootDir, PERF_CURRENT_REPORT_FILE),
45
+ JSON.stringify(report, null, 2),
46
+ "utf-8",
47
+ );
48
+
49
+ return report;
50
+ }
51
+
52
+ export function loadCurrentPerfRunReport(
53
+ rootDir = getDistRoot(),
54
+ ): PerfRunReport | null {
55
+ const reportPath = path.join(rootDir, PERF_CURRENT_REPORT_FILE);
56
+ if (!fs.existsSync(reportPath)) {
57
+ return null;
58
+ }
59
+
60
+ // A truncated/corrupt report.json must not crash the request handler — treat
61
+ // an unparseable report as "no report yet".
62
+ try {
63
+ return JSON.parse(fs.readFileSync(reportPath, "utf-8")) as PerfRunReport;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Remove the previous run's normalized report + raw summary artifacts at the
71
+ * START of a run.
72
+ *
73
+ * `.perf-report/current/` is reused across runs, so without this the prior
74
+ * run's `report.json` (and per-module `*.summary.json`) linger. If the report
75
+ * is read before the current run regenerates it — or a threshold-breaching run
76
+ * never regenerates it — `loadCurrentPerfRunReport` silently serves STALE prior
77
+ * results (the back-to-back staleness bug). Clearing here makes such a read
78
+ * return `null` ("no report yet", loud) instead of stale data.
79
+ */
80
+ export function resetCurrentPerfRunReport(rootDir = getDistRoot()): void {
81
+ try {
82
+ fs.rmSync(path.join(rootDir, PERF_CURRENT_REPORT_FILE), { force: true });
83
+ } catch {
84
+ /* nothing to remove */
85
+ }
86
+ try {
87
+ const dir = path.join(rootDir, PERF_CURRENT_REPORT_DIR);
88
+ for (const entry of fs.readdirSync(dir)) {
89
+ if (entry.endsWith(".summary.json")) {
90
+ fs.rmSync(path.join(dir, entry), { force: true });
91
+ }
92
+ }
93
+ } catch {
94
+ /* dir may not exist yet */
95
+ }
96
+ }
97
+
98
+ function readSummaryArtifact(rootDir: string, moduleName: string): K6Summary {
99
+ const artifactPath = path.join(
100
+ rootDir,
101
+ getPerfSummaryArtifactPath(moduleName),
102
+ );
103
+ if (!fs.existsSync(artifactPath)) {
104
+ throw new Error(`Missing k6 summary artifact for module "${moduleName}"`);
105
+ }
106
+
107
+ // JSON boundary: k6 wrote this artifact, so the shape is trusted here and
108
+ // every field access in the normalizer is already optional/defensive.
109
+ return JSON.parse(fs.readFileSync(artifactPath, "utf-8")) as K6Summary;
110
+ }
@@ -0,0 +1,70 @@
1
+ export interface PerfArtifactRef {
2
+ kind: "k6-summary" | "normalized-report";
3
+ path: string;
4
+ }
5
+
6
+ export interface PerfReportValue {
7
+ label: string;
8
+ value: number | string | null;
9
+ unit?: string;
10
+ }
11
+
12
+ export interface PerfThresholdResult {
13
+ metric: string;
14
+ name: string;
15
+ expression: string;
16
+ passed: boolean;
17
+ actualValue: number | string | null;
18
+ unit?: string;
19
+ }
20
+
21
+ export interface PerfScenarioSummary {
22
+ name: string;
23
+ executor?: string;
24
+ startTime?: string;
25
+ gracefulStop?: string;
26
+ env?: Record<string, string>;
27
+ options?: Record<string, unknown>;
28
+ }
29
+
30
+ export interface PerfCheckResult {
31
+ name: string;
32
+ passes: number;
33
+ fails: number;
34
+ passRate: number | null;
35
+ }
36
+
37
+ export interface PerfMetricSummary {
38
+ key: string;
39
+ label: string;
40
+ type?: string;
41
+ contains?: string;
42
+ values: Record<string, number | null>;
43
+ thresholds: PerfThresholdResult[];
44
+ isCustom?: boolean;
45
+ }
46
+
47
+ export interface PerfModuleRunReport {
48
+ module: string;
49
+ status: "passed" | "failed";
50
+ startedAt?: string;
51
+ finishedAt?: string;
52
+ thresholds: PerfThresholdResult[];
53
+ coreMetrics: PerfReportValue[];
54
+ latencyMetrics: PerfReportValue[];
55
+ throughputMetrics: PerfReportValue[];
56
+ scenarios: PerfScenarioSummary[];
57
+ checks: PerfCheckResult[];
58
+ customMetrics: PerfMetricSummary[];
59
+ rawArtifacts: PerfArtifactRef[];
60
+ }
61
+
62
+ export interface PerfRunReport {
63
+ pillar: "performance";
64
+ environment: string;
65
+ status: "passed" | "failed";
66
+ startedAt?: string;
67
+ finishedAt?: string;
68
+ modules: PerfModuleRunReport[];
69
+ rawArtifacts: PerfArtifactRef[];
70
+ }
@@ -0,0 +1,25 @@
1
+ import * as path from "path";
2
+ import { getDistRoot } from "../paths.js";
3
+
4
+ /**
5
+ * Where a distribution keeps its own k6 suites, relative to the distribution
6
+ * root: `<dist root>/tests/pillars/perf/<module>/test.js`. The engine ships
7
+ * none; repo-owned suites are declared in each repo's manifest instead.
8
+ */
9
+ export const PERF_DIST_TESTS_DIR = path.join("tests", "pillars", "perf");
10
+
11
+ /** The k6 entry file every distribution perf module provides. */
12
+ export const PERF_TEST_ENTRY = "test.js";
13
+
14
+ /** Absolute path of the distribution's perf suites directory. */
15
+ export function getPerfTestsRoot(root: string = getDistRoot()): string {
16
+ return path.resolve(root, PERF_DIST_TESTS_DIR);
17
+ }
18
+
19
+ /** Absolute path of a distribution perf module's k6 entry file. */
20
+ export function getPerfTestFile(
21
+ moduleName: string,
22
+ root: string = getDistRoot(),
23
+ ): string {
24
+ return path.join(getPerfTestsRoot(root), moduleName, PERF_TEST_ENTRY);
25
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Size-capped rotation for service logs.
3
+ *
4
+ * Service logs are written by DETACHED child processes that inherited an
5
+ * O_APPEND file descriptor (see spawnDaemon): den opens the file, hands the
6
+ * fd to the child, then closes its own copy. Nothing can make that child
7
+ * reopen — the command is arbitrary shell with no SIGHUP contract — and it
8
+ * outlives den. A chatty service therefore grows the file forever; a
9
+ * localstack tail was measured at 8.6 GB.
10
+ *
11
+ * That single fact rules out the usual strategies, and is why no logging
12
+ * library helps here (they all rotate a stream THEY own):
13
+ *
14
+ * - RENAME (`mv x.log x.log.1`) is silently fatal. The child's fd follows the
15
+ * inode, so it keeps writing into the renamed file while `x.log` stays
16
+ * empty — and every reader tails by name, so live logs stop forever with no
17
+ * error anywhere.
18
+ * - TRUNCATE-HEAD (keep the last N bytes at offset 0) makes `tail -F` notice
19
+ * the file shrank and restart from the beginning, re-flooding every viewer
20
+ * with the whole retained tail. It also shifts every byte offset, so the
21
+ * history pager silently renders the wrong page.
22
+ *
23
+ * What works is copy-then-truncate-to-ZERO: copy the tail into `<name>.log.1`,
24
+ * then truncate the live file in place. The inode never moves, so the child's
25
+ * fd stays valid and O_APPEND puts its next write at the new EOF. Readers cope:
26
+ * `tail -F` restarts at offset 0 and finds one banner line rather than a flood,
27
+ * and a stale history cursor clamps to a tiny file and ends paging cleanly.
28
+ *
29
+ * INVARIANT: the child's fd must stay O_APPEND. With plain O_WRONLY the kernel
30
+ * keeps the old offset and truncation produces a multi-gigabyte sparse hole of
31
+ * NULs. spawn-streaming opens with "a"; a test pins it.
32
+ */
33
+
34
+ import * as fs from "fs";
35
+ import * as path from "path";
36
+ import { MAX_LOG_BYTES, ROTATED_KEEP_BYTES } from "../constants.js";
37
+
38
+ import { product } from "../product.js";
39
+ /** Where a rotated generation is kept — one only, overwritten each time. */
40
+ export function rotatedPathFor(logPath: string): string {
41
+ return `${logPath}.1`;
42
+ }
43
+
44
+ /**
45
+ * Rotate `logPath` when it exceeds `maxBytes`. Returns true when it rotated.
46
+ *
47
+ * Best-effort by design: a log that cannot be rotated must never break the
48
+ * service that writes to it, so every failure degrades to "left as-is".
49
+ */
50
+ export function rotateIfOversized(
51
+ logPath: string,
52
+ maxBytes: number = MAX_LOG_BYTES,
53
+ keepBytes: number = ROTATED_KEEP_BYTES,
54
+ ): boolean {
55
+ let size: number;
56
+ try {
57
+ const st = fs.statSync(logPath);
58
+ if (!st.isFile()) return false;
59
+ size = st.size;
60
+ } catch {
61
+ return false; // no log yet
62
+ }
63
+ if (size <= maxBytes) return false;
64
+
65
+ try {
66
+ copyTail(logPath, rotatedPathFor(logPath), Math.min(keepBytes, size));
67
+ // Truncate to ZERO, not to a retained tail — see the module docstring.
68
+ fs.truncateSync(logPath, 0);
69
+ fs.appendFileSync(
70
+ logPath,
71
+ `\n─── ${product().cli} rotated ${new Date().toISOString()} — earlier output in ${path.basename(rotatedPathFor(logPath))} ───\n`,
72
+ );
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Copy the last `bytes` of `from` into `to`, starting at the first line break
81
+ * so the rotated file doesn't open mid-line. Streams in chunks — the whole
82
+ * point is that these files are too big to hold in memory.
83
+ */
84
+ function copyTail(from: string, to: string, bytes: number): void {
85
+ const src = fs.openSync(from, "r");
86
+ try {
87
+ const size = fs.fstatSync(src).size;
88
+ let start = Math.max(0, size - bytes);
89
+ if (start > 0) {
90
+ // Snap forward to just past the next newline.
91
+ const probe = Buffer.alloc(Math.min(64 * 1024, size - start));
92
+ const read = fs.readSync(src, probe, 0, probe.length, start);
93
+ const nl = probe.subarray(0, read).indexOf(0x0a);
94
+ if (nl !== -1) start += nl + 1;
95
+ }
96
+ const dst = fs.openSync(to, "w");
97
+ try {
98
+ const buf = Buffer.alloc(1024 * 1024);
99
+ let pos = start;
100
+ for (;;) {
101
+ const read = fs.readSync(src, buf, 0, buf.length, pos);
102
+ if (read <= 0) break;
103
+ fs.writeSync(dst, buf, 0, read);
104
+ pos += read;
105
+ }
106
+ } finally {
107
+ fs.closeSync(dst);
108
+ }
109
+ } finally {
110
+ fs.closeSync(src);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Rotate every oversized `*.log` under a log root (`<root>/<repo>/<name>.log`).
116
+ * Returns the paths rotated. Never throws — a sweep failure must not take the
117
+ * server down.
118
+ */
119
+ export function sweepLogs(
120
+ logRoot: string,
121
+ maxBytes: number = MAX_LOG_BYTES,
122
+ ): string[] {
123
+ const rotated: string[] = [];
124
+ let dirs: fs.Dirent[];
125
+ try {
126
+ dirs = fs.readdirSync(logRoot, { withFileTypes: true });
127
+ } catch {
128
+ return rotated; // no logs yet
129
+ }
130
+ for (const dir of dirs) {
131
+ if (!dir.isDirectory()) continue;
132
+ const sub = path.join(logRoot, dir.name);
133
+ let files: string[];
134
+ try {
135
+ files = fs.readdirSync(sub);
136
+ } catch {
137
+ continue;
138
+ }
139
+ for (const file of files) {
140
+ // `.log.1` is the rotated generation — capping it again would discard it.
141
+ if (!file.endsWith(".log")) continue;
142
+ const full = path.join(sub, file);
143
+ if (rotateIfOversized(full, maxBytes)) rotated.push(full);
144
+ }
145
+ }
146
+ return rotated;
147
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Shared streaming process spawner
3
+ *
4
+ * Extracted from src/ui/cli/test-runner.ts so both the CLI test runner
5
+ * and the environment manager can spawn processes without importing from
6
+ * the UI layer.
7
+ */
8
+
9
+ import { spawn } from "child_process";
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { rotateIfOversized } from "./log-rotation.js";
13
+ import { getStateDir } from "../paths.js";
14
+
15
+ import { product } from "../product.js";
16
+ // ── Log path ──────────────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Canonical path for a service's daemon log file.
20
+ * ~/.den/logs/<repoName>/<subsystem>.log
21
+ */
22
+ export function getServiceLogPath(repoName: string, subsystem: string): string {
23
+ const dir = path.join(getServiceLogRoot(), repoName);
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ return path.join(dir, `${subsystem}.log`);
26
+ }
27
+
28
+ /** Root of the per-service log tree: `~/.den/logs/<repo>/<service>.log`. */
29
+ export function getServiceLogRoot(): string {
30
+ return path.join(getStateDir(), "logs");
31
+ }
32
+
33
+ // ── ANSI strip ────────────────────────────────────────────────────────────────
34
+
35
+ function stripAnsi(data: Buffer, onLine: (line: string) => void): void {
36
+ // eslint-disable-next-line no-control-regex
37
+ const text = data.toString().replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
38
+ for (const line of text.split("\n")) {
39
+ const visible = line.split("\r").filter(Boolean).pop() ?? "";
40
+ if (visible.trim()) onLine(visible);
41
+ }
42
+ }
43
+
44
+ // ── Daemon spawner ────────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Spawn a long-running service as a true daemon, fully decoupled from den's
48
+ * process space.
49
+ *
50
+ * Why this matters
51
+ * ────────────────
52
+ * `spawnStreaming` with `stdio: pipe` keeps a file-descriptor link between
53
+ * den and the service. When den exits (or the SSE response closes and
54
+ * Node GC's the stream), the pipe read-end closes → the child gets SIGPIPE on
55
+ * its next write → it dies. `proc.unref()` only removes the process from
56
+ * Node's event-loop ref-count; it does NOT sever the FD connection.
57
+ *
58
+ * This function fixes that by:
59
+ * 1. Writing a start-marker to `logPath` then opening it with `O_APPEND`.
60
+ * 2. Spawning the service with `stdio: ['ignore', logFd, logFd]` — no pipe,
61
+ * no connection to den's FD table.
62
+ * 3. Immediately closing the parent's copy of the log fd.
63
+ * 4. Starting a `tail -F` subprocess to stream output back to the caller.
64
+ *
65
+ * The returned process is the `tail` subprocess — kill it when you want to
66
+ * stop streaming without stopping the service. `onClose` fires when tail
67
+ * exits (i.e. when the caller kills it or the SSE detach window expires).
68
+ */
69
+ export function spawnDaemon(
70
+ command: string,
71
+ env: NodeJS.ProcessEnv,
72
+ logPath: string,
73
+ onLine: (line: string) => void,
74
+ onClose: (exitCode: number) => void,
75
+ cwd?: string,
76
+ onServiceExit?: (exitCode: number) => void,
77
+ ): ReturnType<typeof spawn> {
78
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
79
+
80
+ // Cap the log before the next run appends to it. Cheap (one statSync) and it
81
+ // covers restart-heavy services; the periodic sweep in the server catches
82
+ // long-lived ones that never respawn.
83
+ rotateIfOversized(logPath);
84
+
85
+ // Stamp the log so the tail starts reading from a clean boundary.
86
+ const ts = new Date().toISOString();
87
+ fs.appendFileSync(logPath, `\n\n─── ${product().cli} start ${ts} ───\n`);
88
+
89
+ // Open the log fd in the parent so we can hand it to the child.
90
+ // MUST stay "a" (O_APPEND): rotation truncates this file in place, and only
91
+ // O_APPEND recomputes the offset to EOF on every write. With plain O_WRONLY
92
+ // the child would keep its old offset and write a multi-GB sparse hole of
93
+ // NULs. See log-rotation.ts.
94
+ const logFd = fs.openSync(logPath, "a");
95
+
96
+ const svcProc = spawn(command, [], {
97
+ env,
98
+ cwd,
99
+ shell: true,
100
+ detached: true, // own process group → immune to Ctrl+C on den
101
+ stdio: ["ignore", logFd, logFd], // NO pipe to the CLI — survives its shutdown
102
+ });
103
+ // Notify caller when the service process itself exits (e.g. `make up` finishes
104
+ // after starting Docker containers). The service may still be "running" even
105
+ // after this fires — the process just handed off to the container runtime.
106
+ if (onServiceExit) {
107
+ svcProc.on("exit", (code) => onServiceExit(code ?? 0));
108
+ }
109
+ // Without an 'error' listener a spawn failure (e.g. cwd missing, ENOENT)
110
+ // emits an unhandled 'error' that crashes den. Surface it instead.
111
+ svcProc.on("error", (err) => {
112
+ onLine(`Error: ${err.message}`);
113
+ onServiceExit?.(1);
114
+ });
115
+ svcProc.unref(); // den's event loop can exit independently
116
+ fs.closeSync(logFd); // parent closes its copy; child keeps its own fd
117
+
118
+ // Tail the log for streaming back to the caller.
119
+ // -F follows by name (handles log rotation). -n 0 shows only new lines written
120
+ // after tail started (we already stamped the marker above).
121
+ const tailProc = spawn("tail", ["-F", "-n", "0", logPath], {
122
+ stdio: ["ignore", "pipe", "ignore"],
123
+ });
124
+
125
+ tailProc.stdout?.on("data", (data: Buffer) => stripAnsi(data, onLine));
126
+ tailProc.on("close", () => onClose(0));
127
+ tailProc.on("error", () => onClose(0));
128
+
129
+ return tailProc;
130
+ }
131
+
132
+ // ── Streaming spawner ─────────────────────────────────────────────────────────
133
+
134
+ /**
135
+ * Spawn a process and stream stdout/stderr line-by-line via callbacks.
136
+ * ANSI colour codes are stripped so browser/terminal output renders cleanly.
137
+ *
138
+ * When `detached` is true AND `logPath` is provided the process is spawned as
139
+ * a true daemon (see `spawnDaemon`). The returned proc is then the `tail`
140
+ * subprocess used for streaming — kill it to stop streaming without stopping
141
+ * the service.
142
+ *
143
+ * @param command Executable name or shell string (when args is empty, shell mode is used)
144
+ * @param args Argument array — pass [] to invoke command as a raw shell string
145
+ * @param env Process environment
146
+ * @param onLine Called for each non-empty output line
147
+ * @param onClose Called with exit code when the process terminates
148
+ * @param cwd Working directory (defaults to process.cwd())
149
+ * @param detached Put the child in its own process group (default false)
150
+ * @param logPath If provided together with detached=true, run as a daemon writing to this path
151
+ */
152
+ export function spawnStreaming(
153
+ command: string,
154
+ args: string[],
155
+ env: NodeJS.ProcessEnv,
156
+ onLine: (line: string) => void,
157
+ onClose: (exitCode: number) => void,
158
+ cwd?: string,
159
+ detached?: boolean,
160
+ logPath?: string,
161
+ onServiceExit?: (exitCode: number) => void,
162
+ ): ReturnType<typeof spawn> {
163
+ // Daemon mode: service runs independently from den — survives restarts.
164
+ if (detached && logPath) {
165
+ return spawnDaemon(
166
+ command,
167
+ env,
168
+ logPath,
169
+ onLine,
170
+ onClose,
171
+ cwd,
172
+ onServiceExit,
173
+ );
174
+ }
175
+
176
+ // Standard mode: pipe stdout/stderr for streaming, optionally detached.
177
+ const proc = spawn(command, args, {
178
+ env,
179
+ cwd,
180
+ shell: args.length === 0, // shell mode when command is a raw string (service commands)
181
+ detached: detached ?? false,
182
+ stdio: ["ignore", "pipe", "pipe"],
183
+ });
184
+
185
+ proc.stdout?.on("data", (data: Buffer) => stripAnsi(data, onLine));
186
+ proc.stderr?.on("data", (data: Buffer) => stripAnsi(data, onLine));
187
+
188
+ proc.on("close", (code) => onClose(code ?? 1));
189
+ proc.on("error", (err) => {
190
+ onLine(`Error: ${err.message}`);
191
+ onClose(1);
192
+ });
193
+
194
+ if (detached) proc.unref();
195
+
196
+ return proc;
197
+ }
198
+
199
+ // ── Interactive spawner ───────────────────────────────────────────────────────
200
+
201
+ /**
202
+ * Spawn a process with full TTY access — stdin, stdout, and stderr are all
203
+ * inherited from the parent process. Use this for interactive commands that
204
+ * require human input (HITL), such as auth logins, license prompts, etc.
205
+ *
206
+ * No output streaming is performed — the user sees and interacts with the
207
+ * process directly in their terminal.
208
+ */
209
+ export function spawnInteractive(
210
+ command: string,
211
+ env: NodeJS.ProcessEnv,
212
+ onClose: (exitCode: number) => void,
213
+ cwd?: string,
214
+ ): ReturnType<typeof spawn> {
215
+ const proc = spawn(command, [], {
216
+ env,
217
+ cwd,
218
+ shell: true,
219
+ stdio: "inherit",
220
+ });
221
+
222
+ proc.on("close", (code) => onClose(code ?? 1));
223
+ proc.on("error", (err) => {
224
+ void err;
225
+ onClose(1);
226
+ });
227
+
228
+ return proc;
229
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The running product's names, for code that needs them at runtime.
3
+ *
4
+ * Den never hard-codes a product name: the CLI, env-var prefix, config and
5
+ * manifest file names and directories all come from the distribution's
6
+ * profile (den.yml, see profile.ts). This module is the safe accessor: it
7
+ * never throws, falling back to DEN_PROFILE or Den's own profile when no
8
+ * distribution root can be found (e.g. help text printed outside a checkout).
9
+ */
10
+
11
+ import { getDistRoot } from "./paths.js";
12
+ import {
13
+ loadProfile,
14
+ profileFrom,
15
+ readProfileFile,
16
+ type ProductProfile,
17
+ } from "./profile.js";
18
+
19
+ export interface Product extends ProductProfile {
20
+ /** Display name: the name with its first letter upper-cased ("Den"). */
21
+ title: string;
22
+ /** The YAML manifest file name (".den.yml"). */
23
+ manifestYml: string;
24
+ /** The JSON manifest file name (".den.json"). */
25
+ manifestJson: string;
26
+ }
27
+
28
+ function withDerived(p: ProductProfile): Product {
29
+ const yml = p.manifestFiles[0];
30
+ return {
31
+ ...p,
32
+ title: p.name.charAt(0).toUpperCase() + p.name.slice(1),
33
+ manifestYml: yml,
34
+ manifestJson: p.manifestFiles[1] ?? yml.replace(/\.ya?ml$/, "") + ".json",
35
+ };
36
+ }
37
+
38
+ /** The running product. Never throws. */
39
+ export function product(): Product {
40
+ try {
41
+ return withDerived(loadProfile(getDistRoot()));
42
+ } catch {
43
+ const pinned = process.env.DEN_PROFILE?.trim();
44
+ try {
45
+ return withDerived(pinned ? readProfileFile(pinned) : profileFrom());
46
+ } catch {
47
+ return withDerived(profileFrom());
48
+ }
49
+ }
50
+ }
51
+
52
+ /** The env-var name this product uses for `key`: envKey("ROOT") → "DEN_ROOT". */
53
+ export function envKey(key: string): string {
54
+ return `${product().envPrefix}_${key}`;
55
+ }
56
+
57
+ /** The value of this product's `key` env var, or undefined. */
58
+ export function envValue(
59
+ key: string,
60
+ env: NodeJS.ProcessEnv = process.env,
61
+ ): string | undefined {
62
+ return env[envKey(key)];
63
+ }
64
+
65
+ /**
66
+ * Placeholders the web pages use for product names; serveFile fills them in.
67
+ * Pages are static files, so they cannot ask the profile themselves.
68
+ */
69
+ export function productTokens(p: Product = product()): Record<string, string> {
70
+ return {
71
+ __DEN_CLI__: p.cli,
72
+ __DEN_NAME__: p.name,
73
+ __DEN_TITLE__: p.title,
74
+ __DEN_ENV_PREFIX__: p.envPrefix,
75
+ __DEN_CONFIG_FILE__: p.configFile,
76
+ __DEN_MANIFEST_YML__: p.manifestYml,
77
+ __DEN_MANIFEST_JSON__: p.manifestJson,
78
+ __DEN_SERVICES_DIR__: p.servicesDir,
79
+ __DEN_WORKSPACES_DIR__: p.workspacesDir,
80
+ };
81
+ }
82
+
83
+ /** Replace every product placeholder in `text`. */
84
+ export function fillProductTokens(
85
+ text: string,
86
+ p: Product = product(),
87
+ ): string {
88
+ let out = text;
89
+ for (const [token, value] of Object.entries(productTokens(p)))
90
+ out = out.split(token).join(value);
91
+ return out;
92
+ }