@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,68 @@
1
+ /**
2
+ * Pre-flight check: storage teardown scripts often call admin HTTP APIs (e.g. a service's
3
+ * `POST /api/v1/admin/purge`). If admin routes are disabled, fail before Playwright/k6.
4
+ * The probe path and 403 hint come from the manifest fixture (`adminProbePath`,
5
+ * `adminProbe403Hint`).
6
+ *
7
+ * Service-specific error hints belong to the caller via `hintOn403` — this module stays
8
+ * generic and has no knowledge of any particular service's error format.
9
+ */
10
+
11
+ export interface AssertStorageAdminProbeOptions {
12
+ baseUrl: string;
13
+ /** e.g. `/api/v1/admin/status` (GET, idempotent, passes admin guard only when enabled). */
14
+ adminProbePath: string;
15
+ /** Shown in errors (e.g. `files-api / api`). */
16
+ label: string;
17
+ /**
18
+ * Optional callback invoked on a 403 response to produce a service-specific remediation hint.
19
+ * Return a non-empty string to append it to the error message; return "" for generic output.
20
+ */
21
+ hintOn403?: (body: string) => string;
22
+ }
23
+
24
+ function joinBaseUrlAndProbePath(baseUrl: string, probePath: string): string {
25
+ const trimmedBase = baseUrl.replace(/\/$/, "");
26
+ const path = probePath.startsWith("/") ? probePath : `/${probePath}`;
27
+ return `${trimmedBase}${path}`;
28
+ }
29
+
30
+ /**
31
+ * GET `{baseUrl}{adminProbePath}` — expect 2xx when admin APIs are allowed for this deployment.
32
+ * Throws with a clear error (and optional caller-supplied hint) on non-2xx.
33
+ */
34
+ export async function assertStorageAdminProbe(
35
+ options: AssertStorageAdminProbeOptions,
36
+ ): Promise<void> {
37
+ const url = joinBaseUrlAndProbePath(options.baseUrl, options.adminProbePath);
38
+ console.log(`🔒 Admin API probe (${options.label}): ${url}`);
39
+ let response: Response;
40
+ try {
41
+ response = await fetch(url, {
42
+ method: "GET",
43
+ signal: AbortSignal.timeout(15_000),
44
+ });
45
+ } catch (error: unknown) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ throw new Error(
48
+ `Admin probe failed for ${options.label}: cannot reach ${url} (${message})`,
49
+ );
50
+ }
51
+ const body = await response.text();
52
+ if (response.ok) {
53
+ console.log(` ✓ Admin endpoints reachable (${response.status})`);
54
+ return;
55
+ }
56
+ if (response.status === 403) {
57
+ const hint = options.hintOn403?.(body) ?? "";
58
+ const detail =
59
+ `Admin probe failed for ${options.label}: ${url} returned 403.` +
60
+ (hint
61
+ ? `\n${hint}`
62
+ : `\n Response body (truncated): ${body.slice(0, 500)}`);
63
+ throw new Error(detail);
64
+ }
65
+ throw new Error(
66
+ `Admin probe failed for ${options.label}: ${url} returned ${response.status}: ${body.slice(0, 500)}`,
67
+ );
68
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Smoke-specific re-exports of the shared service URL resolution helpers.
3
+ *
4
+ * All smoke consumers import from this module. The underlying logic now lives
5
+ * in `src/shared/service-url.ts` so the perf pillar (and future pillars) can
6
+ * reuse it without importing from `pillars/smoke/`.
7
+ */
8
+ import type {
9
+ Environment,
10
+ RawManifest,
11
+ SmokeServiceConfig,
12
+ } from "../../shared/types/index.js";
13
+ import {
14
+ resolveManifestBaseUrl,
15
+ resolveServiceBaseUrl,
16
+ } from "../../shared/service-url.js";
17
+
18
+ export {
19
+ resolveManifestBaseUrl as resolveManifestSmokeBaseUrl,
20
+ resolveServiceBaseUrl as resolveSmokeBaseUrl,
21
+ } from "../../shared/service-url.js";
22
+
23
+ export type {
24
+ ResolveManifestBaseUrlOptions as ResolveManifestSmokeBaseUrlOptions,
25
+ ResolveServiceBaseUrlOptions as ResolveSmokeBaseUrlOptions,
26
+ } from "../../shared/service-url.js";
27
+
28
+ /** Same precedence as single-service smoke setup: override → manifest health URL → env config. */
29
+ export function resolveSmokeBaseUrlForService(
30
+ manifest: RawManifest,
31
+ repoName: string,
32
+ serviceName: string,
33
+ service: SmokeServiceConfig,
34
+ environment: Environment,
35
+ envConfig: { baseUrls: { api: string; web: string } },
36
+ /** Explicit override URL (pass `process.env.BASE_API_URL` at the call site). */
37
+ overrideUrl?: string | null,
38
+ ): string {
39
+ const configuredUrl =
40
+ service.baseUrlKey === "api"
41
+ ? envConfig.baseUrls.api
42
+ : envConfig.baseUrls.web;
43
+ const manifestUrl = resolveManifestBaseUrl({
44
+ manifest,
45
+ repoName,
46
+ serviceName,
47
+ environment,
48
+ baseUrlKey: service.baseUrlKey,
49
+ });
50
+ return resolveServiceBaseUrl({
51
+ baseUrlKey: service.baseUrlKey,
52
+ configuredUrl,
53
+ manifestUrl,
54
+ overrideUrl,
55
+ });
56
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ acquireFixture,
3
+ fixtureInstanceId,
4
+ releaseFixturesForConsumer,
5
+ resetFixtureManagerForTests,
6
+ runRepoShellCommand,
7
+ type ManagedFixtureSpec,
8
+ } from "./fixture-manager.js";
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Manifest Discovery
3
+ *
4
+ * Generic layer: scans .den/ for repos that have a .den.json
5
+ * manifest file and returns the raw parsed manifest.
6
+ *
7
+ * Smoke-specific helpers (resolveService) live below and read from the
8
+ * `smoke` key — unrelated to the `services` (service management) key.
9
+ *
10
+ * Local: .den/<repo> → symlink to sibling repo (make smoke-link)
11
+ * CI: .den/<repo> → actual checkout (smoke.yml workflow)
12
+ */
13
+
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+ import { readManifest } from "../../shared/manifest/read-manifest.js";
17
+ import { cloneDirFor } from "../../shared/smoke/repo-cloner.js";
18
+ import { isLocalEnabled } from "../../shared/den-config.js";
19
+ import {
20
+ getActiveWorkspace,
21
+ listWorkspaceServices,
22
+ } from "../../shared/environment-manager/workspaces.js";
23
+ import { readCatalog } from "../../shared/environment-manager/catalog.js";
24
+ import { getDistRoot } from "../../shared/paths.js";
25
+ import type {
26
+ DiscoveredRepo,
27
+ Environment,
28
+ PerformanceServiceConfig,
29
+ SmokeServiceConfig,
30
+ } from "../../shared/types/index.js";
31
+
32
+ import { product } from "../../shared/product.js";
33
+ // Den-root anchored so the legacy-registry branch agrees with the already
34
+ // anchored workspace branch from any cwd (reads only — never written).
35
+ const ROOT = getDistRoot();
36
+ const REGISTERED_FILE = path.resolve(
37
+ ROOT,
38
+ "config",
39
+ "local",
40
+ "registered-repos.json",
41
+ );
42
+ const LINKED_REPOS_DIR = path.resolve(ROOT, `.${product().name}`);
43
+
44
+ // ── Generic layer ─────────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Discover all registered repos.
48
+ *
49
+ * When Local Dev is enabled, the active workspace is the source of truth —
50
+ * tests run against the same repos you're developing against. When Local Dev
51
+ * is disabled, we fall back to the legacy registry:
52
+ * 1. config/local/registered-repos.json (set via `make register-repos`)
53
+ * 2. .den/ symlinks / checkouts (CI / legacy manual links)
54
+ */
55
+ export function discoverRepos(): DiscoveredRepo[] {
56
+ if (isLocalEnabled()) {
57
+ return discoverFromWorkspace();
58
+ }
59
+ return discoverFromRegistry();
60
+ }
61
+
62
+ /** Resolve repos from the active workspace's service set. */
63
+ function discoverFromWorkspace(): DiscoveredRepo[] {
64
+ const active = getActiveWorkspace();
65
+ if (!active) return discoverFromRegistry(); // no workspace → fall back
66
+
67
+ const catalog = readCatalog();
68
+ const services = listWorkspaceServices(active.config, catalog, active.name);
69
+
70
+ const results: DiscoveredRepo[] = [];
71
+ const seen = new Set<string>();
72
+
73
+ for (const svc of services) {
74
+ if (!svc.repoPath || seen.has(svc.repoName)) continue;
75
+ seen.add(svc.repoName);
76
+ addRepo(results, seen, svc.repoPath, svc.repoName);
77
+ }
78
+
79
+ return results;
80
+ }
81
+
82
+ /** Legacy discovery from registered-repos.json and .den/ symlinks. */
83
+ function discoverFromRegistry(): DiscoveredRepo[] {
84
+ const results: DiscoveredRepo[] = [];
85
+ const seen = new Set<string>();
86
+
87
+ // Source 1: config/local/registered-repos.json
88
+ if (fs.existsSync(REGISTERED_FILE)) {
89
+ try {
90
+ const paths = JSON.parse(
91
+ fs.readFileSync(REGISTERED_FILE, "utf-8"),
92
+ ) as string[];
93
+ for (const p of paths) addRepo(results, seen, p, path.basename(p));
94
+ } catch (err) {
95
+ console.warn(` ⚠ Failed to read ${REGISTERED_FILE}: ${err}`);
96
+ }
97
+ }
98
+
99
+ // Source 2: .den/ symlinks / CI checkouts
100
+ if (fs.existsSync(LINKED_REPOS_DIR)) {
101
+ for (const entry of fs.readdirSync(LINKED_REPOS_DIR)) {
102
+ const repoPath = path.join(LINKED_REPOS_DIR, entry);
103
+ try {
104
+ fs.lstatSync(repoPath);
105
+ } catch {
106
+ continue;
107
+ }
108
+ addRepo(results, seen, repoPath, entry);
109
+ }
110
+ }
111
+
112
+ return results;
113
+ }
114
+
115
+ function addRepo(
116
+ results: DiscoveredRepo[],
117
+ seen: Set<string>,
118
+ repoPath: string,
119
+ fallbackName: string,
120
+ ): void {
121
+ const manifest = readManifest(repoPath);
122
+ if (!manifest) return;
123
+ try {
124
+ const resolved = fs.realpathSync(repoPath);
125
+ if (seen.has(resolved)) return;
126
+ seen.add(resolved);
127
+ results.push({
128
+ repoName: manifest.name ?? fallbackName,
129
+ repoPath: resolved,
130
+ manifest,
131
+ });
132
+ } catch (err) {
133
+ // A held manifest can resolve for a managed clone that was never made.
134
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
135
+ console.warn(
136
+ ` ⚠ ${fallbackName}: no checkout at ${repoPath}, skipped. ` +
137
+ `Use your own (${product().cli} use-local ${fallbackName}) or let ${product().cli} clone it (${product().cli} up ${fallbackName}).`,
138
+ );
139
+ return;
140
+ }
141
+ console.warn(` ⚠ Failed to register repo at ${repoPath}: ${err}`);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Registered sibling checkout path (no smoke-clone semantics).
147
+ * Used when environment is `local`, `useLocalCheckout` is true, or callers only
148
+ * need the linked repo root (e.g. repo-owned perf tests staging from checkout).
149
+ */
150
+ export function resolveRegisteredRepoPath(discovered: DiscoveredRepo): string {
151
+ return discovered.repoPath;
152
+ }
153
+
154
+ /**
155
+ * Resolve the local filesystem path for a repo given a target environment.
156
+ *
157
+ * - local → the registered/symlinked path (existing behaviour, no clone)
158
+ * - other → .smoke-clones/{repoName}/{environment}/ by default
159
+ * - any env with useLocalCheckout=true → the registered/symlinked path
160
+ *
161
+ * This is the single integration point for environment-aware path resolution.
162
+ * All callers (setup.ts, setup-global.ts) should go through here instead of
163
+ * reaching into DiscoveredRepo.repoPath directly when running against non-local
164
+ * environments.
165
+ */
166
+ export function resolveRepoPath(
167
+ discovered: DiscoveredRepo,
168
+ environment: Environment,
169
+ useLocalCheckout = false,
170
+ ): string {
171
+ if (environment === "local" || useLocalCheckout) {
172
+ return resolveRegisteredRepoPath(discovered);
173
+ }
174
+
175
+ const cloneDir = cloneDirFor(discovered.repoName, environment);
176
+ if (!fs.existsSync(cloneDir)) {
177
+ throw new Error(
178
+ `Smoke clone for "${discovered.repoName}@${environment}" not found at ${cloneDir}.\n` +
179
+ ` Run first: npx tsx src/pillars/smoke/setup-env.ts --env ${environment} --repo ${discovered.repoName}\n` +
180
+ ` Or via make: make smoke-clone env=${environment} repo=${discovered.repoName}`,
181
+ );
182
+ }
183
+ return cloneDir;
184
+ }
185
+
186
+ /**
187
+ * Find a specific repo manifest by name.
188
+ */
189
+ export function findManifest(repoName: string): DiscoveredRepo | null {
190
+ const all = discoverRepos();
191
+ return (
192
+ all.find(
193
+ (r) => r.repoName === repoName || path.basename(r.repoPath) === repoName,
194
+ ) ?? null
195
+ );
196
+ }
197
+
198
+ // ── Smoke-specific layer ──────────────────────────────────────────────────────
199
+
200
+ export interface ResolvedSmokeService extends SmokeServiceConfig {
201
+ /** Absolute path to the smoke test directory */
202
+ testDir: string;
203
+ }
204
+
205
+ export interface ResolvedPerformanceService extends PerformanceServiceConfig {
206
+ /** Absolute path to the performance test directory */
207
+ testDir: string;
208
+ }
209
+
210
+ /**
211
+ * Resolve a smoke service config to absolute paths.
212
+ * Reads from `manifest.smoke.<serviceName>` — not `services`.
213
+ * Returns null if the service is not defined in the manifest.
214
+ */
215
+ export function resolveService(
216
+ discovered: DiscoveredRepo,
217
+ serviceName: string,
218
+ ): ResolvedSmokeService | null {
219
+ const svc = discovered.manifest.smoke?.[serviceName];
220
+ if (!svc) return null;
221
+ return {
222
+ ...svc,
223
+ testDir: path.resolve(discovered.repoPath, svc.testDir),
224
+ };
225
+ }
226
+
227
+ export function resolvePerformanceService(
228
+ discovered: DiscoveredRepo,
229
+ moduleName: string,
230
+ ): ResolvedPerformanceService | null {
231
+ const perf = discovered.manifest.performance?.[moduleName];
232
+ if (!perf) return null;
233
+ return {
234
+ ...perf,
235
+ testDir: path.resolve(discovered.repoPath, perf.testDir),
236
+ };
237
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Post–smoke-run check: if Playwright global teardown ran, it removes
3
+ * `.smoke-run/smoke-fixture-handoff.json` after releasing storage fixtures.
4
+ *
5
+ * A leftover file means global teardown did not complete — storage side effects
6
+ * may still exist (MinIO buckets, etc.). Fail loudly so CI/UI runs do not
7
+ * silently leak fixtures.
8
+ *
9
+ * Safe when no storage fixture was acquired (file never created): exits 0.
10
+ */
11
+
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import { getDistRoot } from "../../shared/paths.js";
15
+
16
+ import { product } from "../../shared/product.js";
17
+ // Den-root anchored: must resolve the SAME handoff path global-teardown.ts
18
+ // writes, or a cwd mismatch silently reports leaked fixtures as clean.
19
+ const PROJECT_ROOT = getDistRoot();
20
+ const HANDOFF = path.join(
21
+ PROJECT_ROOT,
22
+ ".smoke-run",
23
+ "smoke-fixture-handoff.json",
24
+ );
25
+
26
+ function main(): void {
27
+ if (fs.existsSync(HANDOFF)) {
28
+ console.error(
29
+ `[${product().cli}] smoke fixture handoff still present:\n ${HANDOFF}\n` +
30
+ ` Playwright global teardown did not finish releasing storage fixtures.\n` +
31
+ ` Fix: ensure tests exit normally so globalTeardown runs; check for crashes/kill -9.`,
32
+ );
33
+ process.exit(1);
34
+ }
35
+ console.log(
36
+ `[${product().cli}] smoke teardown verification: no stale storage fixture handoff`,
37
+ );
38
+ }
39
+
40
+ main();
@@ -0,0 +1,217 @@
1
+ import { spawn } from "child_process";
2
+ import type { CommandDef } from "../../shared/types/index.js";
3
+
4
+ export interface ManagedFixtureSpec {
5
+ fixtureKey: string;
6
+ resolvedPayload: Record<string, unknown>;
7
+ cwd: string;
8
+ env: NodeJS.ProcessEnv;
9
+ setup?: CommandDef;
10
+ teardown?: CommandDef;
11
+ }
12
+
13
+ function commandText(commandDef: CommandDef): string {
14
+ return typeof commandDef === "string" ? commandDef : commandDef.cmd;
15
+ }
16
+
17
+ const UNDEFINED_SENTINEL = "__fixture_manager_undefined__";
18
+
19
+ /** Recursively sort object keys so serialization is stable for arbitrary payloads. */
20
+ function stableSortValue(value: unknown): unknown {
21
+ if (value === undefined) {
22
+ return UNDEFINED_SENTINEL;
23
+ }
24
+ if (value === null || typeof value !== "object") {
25
+ return value;
26
+ }
27
+ if (Array.isArray(value)) {
28
+ return value.map(stableSortValue);
29
+ }
30
+ const obj = value as Record<string, unknown>;
31
+ const sorted: Record<string, unknown> = {};
32
+ for (const key of Object.keys(obj).sort()) {
33
+ sorted[key] = stableSortValue(obj[key]);
34
+ }
35
+ return sorted;
36
+ }
37
+
38
+ /**
39
+ * Deterministic identity for a managed fixture instance: stable serialization of
40
+ * fixtureKey + resolvedPayload.
41
+ */
42
+ export function fixtureInstanceId(
43
+ fixtureKey: string,
44
+ resolvedPayload: Record<string, unknown>,
45
+ ): string {
46
+ return JSON.stringify({
47
+ fixtureKey,
48
+ resolvedPayload: stableSortValue(resolvedPayload),
49
+ });
50
+ }
51
+
52
+ function normalizedCommand(
53
+ commandDef: CommandDef | undefined,
54
+ ): string | undefined {
55
+ return commandDef === undefined ? undefined : commandText(commandDef);
56
+ }
57
+
58
+ function specConflictKey(spec: ManagedFixtureSpec): string {
59
+ return JSON.stringify(
60
+ stableSortValue({
61
+ cwd: spec.cwd,
62
+ env: spec.env,
63
+ setup: normalizedCommand(spec.setup),
64
+ teardown: normalizedCommand(spec.teardown),
65
+ }),
66
+ );
67
+ }
68
+
69
+ export async function runRepoShellCommand(
70
+ command: string,
71
+ cwd: string,
72
+ env: NodeJS.ProcessEnv,
73
+ ): Promise<void> {
74
+ await new Promise<void>((resolve, reject) => {
75
+ const proc = spawn(command, [], {
76
+ cwd,
77
+ env,
78
+ shell: true,
79
+ stdio: "inherit",
80
+ });
81
+
82
+ proc.on("close", (code) => {
83
+ if (code === 0) {
84
+ resolve();
85
+ return;
86
+ }
87
+ reject(
88
+ new Error(`Command failed with exit code ${code ?? 1}: ${command}`),
89
+ );
90
+ });
91
+
92
+ proc.on("error", (error) => {
93
+ reject(
94
+ new Error(`Failed to start command "${command}": ${error.message}`),
95
+ );
96
+ });
97
+ });
98
+ }
99
+
100
+ interface RegistryEntry {
101
+ refCount: number;
102
+ spec: ManagedFixtureSpec;
103
+ specConflictKey: string;
104
+ }
105
+
106
+ const registry = new Map<string, RegistryEntry>();
107
+ const instanceTails = new Map<string, Promise<unknown>>();
108
+
109
+ function enqueueForInstance<T>(
110
+ instanceId: string,
111
+ task: () => Promise<T>,
112
+ ): Promise<T> {
113
+ const prev = instanceTails.get(instanceId) ?? Promise.resolve();
114
+ const result = prev.then(() => task());
115
+ instanceTails.set(
116
+ instanceId,
117
+ result.catch(() => {}),
118
+ );
119
+ return result as Promise<T>;
120
+ }
121
+
122
+ /**
123
+ * Acquire a shared fixture: runs `setup` once per instance identity, increments refCount.
124
+ * Returns a release function — call it in `finally` so teardown runs even when work fails.
125
+ * The release function is idempotent (safe to call at most once; repeats are no-ops).
126
+ */
127
+ export async function acquireFixture(
128
+ spec: ManagedFixtureSpec,
129
+ ): Promise<() => Promise<void>> {
130
+ const instanceId = fixtureInstanceId(spec.fixtureKey, spec.resolvedPayload);
131
+ const currentSpecConflictKey = specConflictKey(spec);
132
+
133
+ return enqueueForInstance(instanceId, async () => {
134
+ let entry = registry.get(instanceId);
135
+ if (!entry) {
136
+ entry = { refCount: 0, spec, specConflictKey: currentSpecConflictKey };
137
+ registry.set(instanceId, entry);
138
+ } else if (entry.specConflictKey !== currentSpecConflictKey) {
139
+ throw new Error(
140
+ `Conflicting fixture specs for shared fixture identity ${instanceId}`,
141
+ );
142
+ }
143
+
144
+ if (entry.refCount === 0 && spec.setup !== undefined) {
145
+ try {
146
+ await runRepoShellCommand(commandText(spec.setup), spec.cwd, spec.env);
147
+ } catch (error) {
148
+ registry.delete(instanceId);
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ entry.refCount += 1;
154
+
155
+ let released = false;
156
+ return async () => {
157
+ await enqueueForInstance(instanceId, async () => {
158
+ if (released) {
159
+ return;
160
+ }
161
+ released = true;
162
+
163
+ const current = registry.get(instanceId);
164
+ if (!current) {
165
+ throw new Error(
166
+ `Fixture release for unknown instance (already torn down?): ${instanceId}`,
167
+ );
168
+ }
169
+
170
+ current.refCount -= 1;
171
+ if (current.refCount < 0) {
172
+ throw new Error(`Fixture refCount underflow for ${instanceId}`);
173
+ }
174
+
175
+ if (current.refCount === 0) {
176
+ if (current.spec.teardown !== undefined) {
177
+ await runRepoShellCommand(
178
+ commandText(current.spec.teardown),
179
+ current.spec.cwd,
180
+ current.spec.env,
181
+ );
182
+ }
183
+ registry.delete(instanceId);
184
+ instanceTails.delete(instanceId);
185
+ }
186
+ });
187
+ };
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Release multiple acquire handles in LIFO order (typical nested `try/finally` stack).
193
+ * Runs every release even when one fails, then throws {@link AggregateError} if any failed.
194
+ */
195
+ export async function releaseFixturesForConsumer(
196
+ releases: ReadonlyArray<() => Promise<void>>,
197
+ ): Promise<void> {
198
+ const errors: Error[] = [];
199
+ for (let i = releases.length - 1; i >= 0; i -= 1) {
200
+ try {
201
+ await releases[i]();
202
+ } catch (e) {
203
+ errors.push(e instanceof Error ? e : new Error(String(e)));
204
+ }
205
+ }
206
+ if (errors.length > 0) {
207
+ throw new AggregateError(
208
+ errors,
209
+ `Fixture teardown failed (${errors.length} instance(s))`,
210
+ );
211
+ }
212
+ }
213
+
214
+ export function resetFixtureManagerForTests(): void {
215
+ registry.clear();
216
+ instanceTails.clear();
217
+ }