@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,415 @@
1
+ /**
2
+ * Per-engineer admin 1Password setup — for environments with no shared admin.
3
+ *
4
+ * Environments where the org config declares a shared admin account
5
+ * (`test_accounts.roles.admin`, optionally limited by its `environments`)
6
+ * need no setup.
7
+ *
8
+ * Elsewhere you need a personal account with admin access. This script lets
9
+ * you point the test pillars at your own 1Password item for those envs.
10
+ *
11
+ * Saves config/local/admin-auth[-{env}].json (gitignored — no password stored)
12
+ *
13
+ * Usage:
14
+ * make auth-configure env=staging # your personal staging admin account
15
+ * make auth-configure env=production # your personal production admin account
16
+ */
17
+
18
+ import { execFile, spawnSync } from "child_process";
19
+ import { promisify } from "util";
20
+ import * as readline from "readline";
21
+ import * as path from "path";
22
+ import * as fs from "fs";
23
+ import { fileURLToPath } from "url";
24
+ import { OP_ACCOUNT_DEFAULT } from "../src/shared/constants.js";
25
+ import {
26
+ readTestAccountsConfig,
27
+ testAccountsVault,
28
+ } from "../src/shared/auth/test-accounts.js";
29
+
30
+ // Ensure op invocations hit the den-owned account even when this script is
31
+ // run directly via `npx tsx` (not through make, which already exports OP_ACCOUNT).
32
+ process.env.OP_ACCOUNT ||= OP_ACCOUNT_DEFAULT;
33
+
34
+ const execFileAsync = promisify(execFile);
35
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
+
37
+ // ─── 1Password types ──────────────────────────────────────────────────────────
38
+
39
+ interface OpVault {
40
+ id: string;
41
+ name: string;
42
+ }
43
+
44
+ interface OpItem {
45
+ id: string;
46
+ title: string;
47
+ category: string;
48
+ }
49
+
50
+ interface OpField {
51
+ id: string;
52
+ label: string;
53
+ value?: string;
54
+ type: string;
55
+ purpose?: string;
56
+ }
57
+
58
+ interface OpItemDetail {
59
+ id: string;
60
+ title: string;
61
+ fields?: OpField[];
62
+ }
63
+
64
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
65
+
66
+ async function opJson<T>(args: string[]): Promise<T> {
67
+ const { stdout } = await execFileAsync("op", [...args, "--format=json"]);
68
+ return JSON.parse(stdout) as T;
69
+ }
70
+
71
+ function prompt(rl: readline.Interface, question: string): Promise<string> {
72
+ return new Promise((resolve) => rl.question(question, resolve));
73
+ }
74
+
75
+ async function pickFromList<T>(
76
+ rl: readline.Interface,
77
+ items: T[],
78
+ getLabel: (item: T) => string,
79
+ question: string,
80
+ ): Promise<T> {
81
+ items.forEach((item, i) => {
82
+ console.log(` ${String(i + 1).padStart(2)}. ${getLabel(item)}`);
83
+ });
84
+ console.log();
85
+ let selected: T | undefined;
86
+ do {
87
+ const answer = await prompt(rl, question);
88
+ const n = parseInt(answer.trim(), 10);
89
+ if (n >= 1 && n <= items.length) {
90
+ selected = items[n - 1];
91
+ } else {
92
+ console.log(` Enter a number between 1 and ${items.length}`);
93
+ }
94
+ } while (selected === undefined);
95
+ return selected;
96
+ }
97
+
98
+ // ─── Defaults ────────────────────────────────────────────────────────────────
99
+
100
+ /** The org's shared admin item (test_accounts.roles.admin), offered first. */
101
+ function sharedAdminDefault():
102
+ | { vault: string; item: string; field: string }
103
+ | undefined {
104
+ const cfg = readTestAccountsConfig();
105
+ const admin = cfg.roles?.admin;
106
+ if (!admin?.item) return undefined;
107
+ return {
108
+ vault: admin.vault ?? testAccountsVault(cfg),
109
+ item: admin.item,
110
+ field: admin.field ?? "password",
111
+ };
112
+ }
113
+
114
+ // ─── Main ────────────────────────────────────────────────────────────────────
115
+
116
+ async function main() {
117
+ const environment = process.env.E2E_ENVIRONMENT;
118
+ const configFilename = environment
119
+ ? `admin-auth-${environment}.json`
120
+ : "admin-auth.json";
121
+ const configScope = environment
122
+ ? `environment: ${environment}`
123
+ : "all environments (generic fallback)";
124
+
125
+ console.log();
126
+ console.log("🔐 Admin 1Password Configuration");
127
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
128
+ console.log(`Scope: ${configScope}`);
129
+ console.log(
130
+ `Saves config/local/${configFilename} (gitignored — no password stored).`,
131
+ );
132
+
133
+ if (environment === "production") {
134
+ console.log();
135
+ console.log(
136
+ " ⚠ Production: for auditability you may want to use your own personal",
137
+ );
138
+ console.log(
139
+ " admin credentials rather than a shared account, so that actions in",
140
+ );
141
+ console.log(" the environment are traceable to you specifically.");
142
+ console.log();
143
+ console.log(
144
+ " Select a vault and item that has your personal admin account email",
145
+ );
146
+ console.log(" and password for production saved in 1Password.");
147
+ }
148
+
149
+ console.log();
150
+
151
+ // ── Check op is installed ────────────────────────────────────────────────
152
+ try {
153
+ await execFileAsync("op", ["--version"]);
154
+ } catch {
155
+ console.error("❌ 1Password CLI (op) not found.");
156
+ console.error(" Install it: brew install 1password-cli");
157
+ process.exit(1);
158
+ }
159
+
160
+ // ── Check signed in (auto-sign-in if not) ───────────────────────────────
161
+ try {
162
+ await execFileAsync("op", ["whoami"]);
163
+ } catch {
164
+ console.log("🔐 Not signed in to 1Password — signing in now...");
165
+ const result = spawnSync("op", ["signin"], { stdio: "inherit" });
166
+ if (result.status !== 0) {
167
+ console.error("❌ 1Password sign-in failed.");
168
+ process.exit(1);
169
+ }
170
+ // Verify it worked
171
+ try {
172
+ await execFileAsync("op", ["whoami"]);
173
+ } catch {
174
+ console.error("❌ Still not signed in after op signin.");
175
+ process.exit(1);
176
+ }
177
+ }
178
+
179
+ const rl = readline.createInterface({
180
+ input: process.stdin,
181
+ output: process.stdout,
182
+ });
183
+
184
+ try {
185
+ // ── Step 1: Select vault ────────────────────────────────────────────────
186
+ console.log("Fetching vaults...");
187
+ const vaults = await opJson<OpVault[]>(["vault", "list"]);
188
+ if (vaults.length === 0) {
189
+ console.error("❌ No vaults found.");
190
+ process.exit(1);
191
+ }
192
+
193
+ // Offer the org's shared admin item (if configured) before the full picker
194
+ const SHARED_ADMIN = sharedAdminDefault();
195
+ if (SHARED_ADMIN) {
196
+ const defaultVault = vaults.find((v) => v.name === SHARED_ADMIN.vault);
197
+ if (defaultVault) {
198
+ console.log(` Default: ${SHARED_ADMIN.vault} / ${SHARED_ADMIN.item}`);
199
+ const answer = await prompt(rl, " Use this default? (Y/n): ");
200
+ if (answer.trim().toLowerCase() !== "n") {
201
+ const allItems = await opJson<OpItem[]>([
202
+ "item",
203
+ "list",
204
+ "--vault",
205
+ defaultVault.id,
206
+ ]);
207
+ const defaultItem = allItems.find(
208
+ (i) => i.title === SHARED_ADMIN.item,
209
+ );
210
+ if (defaultItem) {
211
+ const detail = await opJson<OpItemDetail>([
212
+ "item",
213
+ "get",
214
+ defaultItem.id,
215
+ "--vault",
216
+ defaultVault.id,
217
+ ]);
218
+ const fields = detail.fields ?? [];
219
+ const passwordField = fields.find(
220
+ (f) => f.label === SHARED_ADMIN.field,
221
+ );
222
+ const emailField = fields.find(
223
+ (f) =>
224
+ f.purpose === "USERNAME" ||
225
+ f.type === "EMAIL" ||
226
+ f.label?.toLowerCase() === "username" ||
227
+ f.label?.toLowerCase() === "email",
228
+ );
229
+ if (passwordField && emailField?.value) {
230
+ const opRef = `op://${defaultVault.name}/${defaultItem.title}/${passwordField.label}`;
231
+ const email = emailField.value;
232
+ const configDir = path.resolve(__dirname, "../config/local");
233
+ fs.mkdirSync(configDir, { recursive: true });
234
+ fs.writeFileSync(
235
+ path.join(configDir, configFilename),
236
+ JSON.stringify(
237
+ {
238
+ email,
239
+ opRef,
240
+ itemId: defaultItem.id,
241
+ vaultId: defaultVault.id,
242
+ fieldLabel: passwordField.label,
243
+ },
244
+ null,
245
+ 2,
246
+ ) + "\n",
247
+ );
248
+ console.log();
249
+ console.log(
250
+ `✅ Admin config saved → config/local/${configFilename}`,
251
+ );
252
+ console.log(` scope: ${configScope}`);
253
+ console.log(` email: ${email}`);
254
+ console.log(` opRef: ${opRef}`);
255
+ console.log();
256
+ console.log(`Run tests: make test-syn-basic env=${environment}`);
257
+ return;
258
+ }
259
+ }
260
+ console.log(
261
+ " Default item not found — falling back to manual selection.",
262
+ );
263
+ console.log();
264
+ }
265
+ }
266
+ }
267
+
268
+ console.log();
269
+ console.log("Select vault:");
270
+ const vault = await pickFromList(
271
+ rl,
272
+ vaults,
273
+ (v) => v.name,
274
+ "Vault number: ",
275
+ );
276
+
277
+ // ── Step 2: Select item ─────────────────────────────────────────────────
278
+ console.log();
279
+ console.log(`Fetching items from "${vault.name}"...`);
280
+ const allItems = await opJson<OpItem[]>([
281
+ "item",
282
+ "list",
283
+ "--vault",
284
+ vault.id,
285
+ ]);
286
+
287
+ // Prefer Login items, but show all if none; sort alphabetically
288
+ const loginItems = allItems.filter((i) => i.category === "LOGIN");
289
+ const items = (loginItems.length > 0 ? loginItems : allItems).sort((a, b) =>
290
+ a.title.localeCompare(b.title),
291
+ );
292
+
293
+ if (items.length === 0) {
294
+ console.error(`❌ No items found in vault "${vault.name}".`);
295
+ process.exit(1);
296
+ }
297
+ console.log();
298
+ console.log("Select item:");
299
+ const item = await pickFromList(rl, items, (i) => i.title, "Item number: ");
300
+
301
+ // ── Step 3: Inspect fields ──────────────────────────────────────────────
302
+ console.log();
303
+ console.log(`Fetching fields for "${item.title}"...`);
304
+ const detail = await opJson<OpItemDetail>([
305
+ "item",
306
+ "get",
307
+ item.id,
308
+ "--vault",
309
+ vault.id,
310
+ ]);
311
+
312
+ const fields = detail.fields ?? [];
313
+
314
+ // Detect email from USERNAME / EMAIL fields
315
+ const emailField = fields.find(
316
+ (f) =>
317
+ f.purpose === "USERNAME" ||
318
+ f.type === "EMAIL" ||
319
+ f.label?.toLowerCase() === "username" ||
320
+ f.label?.toLowerCase() === "email",
321
+ );
322
+
323
+ // Find secret fields for the op:// reference
324
+ const secretFields = fields.filter(
325
+ (f) => f.type === "CONCEALED" || f.purpose === "PASSWORD",
326
+ );
327
+
328
+ let passwordField: OpField | undefined;
329
+ if (secretFields.length === 1) {
330
+ passwordField = secretFields[0];
331
+ } else if (secretFields.length > 1) {
332
+ console.log();
333
+ console.log("Select password field:");
334
+ passwordField = await pickFromList(
335
+ rl,
336
+ secretFields,
337
+ (f) => f.label,
338
+ "Field number: ",
339
+ );
340
+ } else {
341
+ // Fallback: show all fields
342
+ console.log();
343
+ console.log("No password field detected. Select the credential field:");
344
+ const allNamed = fields.filter((f) => f.label);
345
+ passwordField = await pickFromList(
346
+ rl,
347
+ allNamed,
348
+ (f) => `${f.label} (${f.type})`,
349
+ "Field number: ",
350
+ );
351
+ }
352
+
353
+ if (!passwordField) {
354
+ console.error("❌ No field selected.");
355
+ process.exit(1);
356
+ }
357
+
358
+ const opRef = `op://${vault.name}/${item.title}/${passwordField.label}`;
359
+
360
+ // ── Step 4: Confirm / enter email ───────────────────────────────────────
361
+ let email: string;
362
+ console.log();
363
+ if (emailField?.value) {
364
+ const answer = await prompt(rl, `Admin email [${emailField.value}]: `);
365
+ email = answer.trim() || emailField.value;
366
+ } else {
367
+ email = (await prompt(rl, "Admin email (@example.com): ")).trim();
368
+ }
369
+
370
+ if (!email) {
371
+ console.error("❌ Email is required.");
372
+ process.exit(1);
373
+ }
374
+
375
+ // ── Step 5: Save ────────────────────────────────────────────────────────
376
+ // Store item ID + field label alongside opRef.
377
+ // headless-login.ts uses the IDs (via `op item get`) which is robust against
378
+ // special characters in vault/item names that can break `op read op://...`.
379
+ const configDir = path.resolve(__dirname, "../config/local");
380
+ const configFile = path.join(configDir, configFilename);
381
+ fs.mkdirSync(configDir, { recursive: true });
382
+ fs.writeFileSync(
383
+ configFile,
384
+ JSON.stringify(
385
+ {
386
+ email,
387
+ opRef,
388
+ itemId: item.id,
389
+ vaultId: vault.id,
390
+ fieldLabel: passwordField.label,
391
+ },
392
+ null,
393
+ 2,
394
+ ) + "\n",
395
+ );
396
+
397
+ console.log();
398
+ console.log(`✅ Admin config saved → config/local/${configFilename}`);
399
+ console.log(` scope: ${configScope}`);
400
+ console.log(` email: ${email}`);
401
+ console.log(` opRef: ${opRef}`);
402
+ console.log();
403
+ const testCmd = environment
404
+ ? `make test-syn-basic env=${environment}`
405
+ : "make test-syn-basic";
406
+ console.log(`Run tests: ${testCmd}`);
407
+ } finally {
408
+ rl.close();
409
+ }
410
+ }
411
+
412
+ main().catch((err: Error) => {
413
+ console.error("❌", err.message);
414
+ process.exit(1);
415
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Idempotent heal for `config/den.yml`.
3
+ *
4
+ * Ensures required keys are populated so users upgrading from older den
5
+ * installs — who may have `github_org` commented out or absent — don't end
6
+ * up with silent empty repo lists. Safe to run repeatedly; exits 0 with a
7
+ * single "already healthy" line when nothing needs doing.
8
+ *
9
+ * Runs during `make setup`; also available as `make den-heal-config`.
10
+ *
11
+ * Keys healed:
12
+ * - github_org — auto-detected from this den checkout's `origin` URL.
13
+ */
14
+
15
+ import * as fs from "fs";
16
+ import * as path from "path";
17
+ import { execFileSync } from "child_process";
18
+
19
+ import { OP_ACCOUNT_DEFAULT } from "../src/shared/constants.js";
20
+
21
+ import { product } from "../src/shared/product.js";
22
+ // Ensure op (if used later) hits the right account when this script runs
23
+ // standalone via npx tsx, matching the pattern in setup-1password.ts et al.
24
+ process.env.OP_ACCOUNT ||= OP_ACCOUNT_DEFAULT;
25
+
26
+ const ROOT = process.cwd();
27
+ const CONFIG_PATH = path.join(ROOT, product().configFile);
28
+
29
+ function detectOrgFromGitRemote(): string | null {
30
+ try {
31
+ const url = execFileSync("git", ["config", "--get", "remote.origin.url"], {
32
+ cwd: ROOT,
33
+ encoding: "utf-8",
34
+ stdio: ["ignore", "pipe", "ignore"],
35
+ }).trim();
36
+ // SSH: git@github.com:ORG/den.git
37
+ // HTTPS: https://github.com/ORG/den.git
38
+ // https://github.com/ORG/den
39
+ const m = url.match(/[:/]([^/:]+)\/[^/]+?(?:\.git)?$/);
40
+ return m ? m[1] : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function main(): void {
47
+ if (!fs.existsSync(CONFIG_PATH)) {
48
+ console.error(
49
+ ` ✗ ${CONFIG_PATH} not found — are you in the ${product().cli} repo root?`,
50
+ );
51
+ process.exit(1);
52
+ }
53
+
54
+ const content = fs.readFileSync(CONFIG_PATH, "utf-8");
55
+
56
+ // Already set (uncommented line with a non-empty value) → no-op.
57
+ const activeMatch = content.match(/^github_org:\s*(\S+)/m);
58
+ if (activeMatch) {
59
+ console.log(` ✓ github_org already set to ${activeMatch[1]}`);
60
+ return;
61
+ }
62
+
63
+ const detected = detectOrgFromGitRemote();
64
+ if (!detected) {
65
+ console.error(
66
+ ` ✗ could not auto-detect github_org from this ${product().cli} checkout's ` +
67
+ `\`remote.origin.url\`. Add \`github_org: <slug>\` to ${product().configFile} ` +
68
+ "manually.",
69
+ );
70
+ process.exit(1);
71
+ }
72
+
73
+ let newContent: string;
74
+ if (/^#\s*github_org:/m.test(content)) {
75
+ // Uncomment-and-set the existing commented placeholder, preserving
76
+ // surrounding comments so the file stays readable.
77
+ newContent = content.replace(
78
+ /^#\s*github_org:.*$/m,
79
+ `github_org: ${detected}`,
80
+ );
81
+ } else {
82
+ // No placeholder present — append.
83
+ newContent = content.replace(/\n*$/, "\n") + `github_org: ${detected}\n`;
84
+ }
85
+
86
+ fs.writeFileSync(CONFIG_PATH, newContent);
87
+ console.log(
88
+ ` ✓ healed ${product().configFile}: github_org: ${detected} (auto-detected from origin)`,
89
+ );
90
+ }
91
+
92
+ main();
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Custom Playwright Reporter for JSONL
3
+ * Writes test results to JSONL files for historical tracking
4
+ */
5
+
6
+ import { spawn } from "child_process";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
9
+ import os from "os";
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const writerScript = path.join(__dirname, "write-result.js");
13
+
14
+ class JSONLReporter {
15
+ onBegin(config, suite) {
16
+ console.log("[JSONL] Reporter initialized");
17
+ }
18
+
19
+ async onTestEnd(test, result) {
20
+ try {
21
+ // Detect pillar from file path
22
+ const filePath = test.location.file;
23
+ let pillar = "synthetic";
24
+ if (filePath.includes("/integration/")) pillar = "integration";
25
+ else if (filePath.includes("/performance/")) pillar = "performance";
26
+
27
+ // Get environment
28
+ const environment = process.env.TEST_ENV || "local";
29
+
30
+ // Build test name
31
+ const testTitle = test.titlePath().slice(1).join(" › ");
32
+ const fileName = path.basename(filePath, path.extname(filePath));
33
+ const testName = `${fileName} › ${testTitle}`;
34
+
35
+ // Determine status
36
+ let status = "passed";
37
+ if (result.status === "failed") status = "failed";
38
+ else if (result.status === "skipped") status = "skipped";
39
+ else if (result.status === "timedOut") status = "failed";
40
+
41
+ // Build result object
42
+ const resultData = {
43
+ timestamp: new Date().toISOString(),
44
+ pillar,
45
+ environment,
46
+ test: testName,
47
+ status,
48
+ duration: result.duration,
49
+ user: process.env.USER || os.userInfo().username || "unknown",
50
+ error: result.error?.message || null,
51
+ retries: result.retry || 0,
52
+ };
53
+
54
+ // Write to JSONL asynchronously
55
+ const child = spawn("node", [writerScript, JSON.stringify(resultData)], {
56
+ stdio: "inherit",
57
+ });
58
+
59
+ // Don't wait for completion to avoid slowing down tests
60
+ } catch (error) {
61
+ console.warn(`[JSONL] Failed to write result: ${error.message}`);
62
+ }
63
+ }
64
+
65
+ onEnd(result) {
66
+ console.log(`[JSONL] Results written to results/ directory`);
67
+ }
68
+ }
69
+
70
+ export default JSONLReporter;
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Makefile helper — reads a .den.yml or .den.json manifest and
4
+ * outputs a value to stdout.
5
+ *
6
+ * Usage:
7
+ * node scripts/manifest-read.cjs <repo-path> <operation>
8
+ *
9
+ * Operations:
10
+ * smoke-keys — space-separated smoke service names
11
+ * smoke-testdir <svc> — testDir for a specific smoke service
12
+ * synth-testdir — synthetics.testDir
13
+ *
14
+ * Returns empty string (exit 0) if the file/key doesn't exist.
15
+ */
16
+
17
+ "use strict";
18
+
19
+ const fs = require("fs");
20
+ const path = require("path");
21
+ const yaml = require("yaml");
22
+
23
+ const [, , repoPath, operation, ...rest] = process.argv;
24
+
25
+ if (!repoPath || !operation) {
26
+ console.error("Usage: manifest-read.cjs <repo-path> <operation> [arg]");
27
+ process.exit(1);
28
+ }
29
+
30
+ /**
31
+ * The manifest file names, from the distribution's den.yml (`product.manifest`,
32
+ * else `.<product.name>.yml`), found by walking up from the cwd.
33
+ */
34
+ function manifestNames() {
35
+ let dir = process.cwd();
36
+ for (;;) {
37
+ const f = path.join(dir, "den.yml");
38
+ if (fs.existsSync(f)) {
39
+ const p = (yaml.parse(fs.readFileSync(f, "utf-8")) || {}).product || {};
40
+ const name = (typeof p.name === "string" && p.name.trim()) || "den";
41
+ const yml =
42
+ (typeof p.manifest === "string" && p.manifest.trim()) || `.${name}.yml`;
43
+ return [yml, yml.replace(/\.ya?ml$/, "") + ".json"];
44
+ }
45
+ const up = path.dirname(dir);
46
+ if (up === dir) return [".den.yml", ".den.json"];
47
+ dir = up;
48
+ }
49
+ }
50
+
51
+ function readManifest(dir) {
52
+ for (const name of manifestNames()) {
53
+ const p = path.join(dir, name);
54
+ if (!fs.existsSync(p)) continue;
55
+ try {
56
+ const raw = fs.readFileSync(p, "utf-8");
57
+ return name.endsWith(".yml") ? yaml.parse(raw) : JSON.parse(raw);
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+
65
+ const manifest = readManifest(repoPath);
66
+ if (!manifest) {
67
+ process.stdout.write("");
68
+ process.exit(0);
69
+ }
70
+
71
+ switch (operation) {
72
+ case "smoke-keys": {
73
+ const keys = Object.keys(manifest.smoke || {});
74
+ process.stdout.write(keys.join(" "));
75
+ break;
76
+ }
77
+ case "smoke-testdir": {
78
+ const svc = rest[0];
79
+ const dir = manifest.smoke?.[svc]?.testDir ?? "";
80
+ process.stdout.write(dir);
81
+ break;
82
+ }
83
+ case "synth-testdir": {
84
+ const dir = manifest.synthetics?.testDir ?? "(no synthetics key)";
85
+ process.stdout.write(dir);
86
+ break;
87
+ }
88
+ default:
89
+ console.error(`Unknown operation: ${operation}`);
90
+ process.exit(1);
91
+ }