@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,767 @@
1
+ /**
2
+ * CLI Prompts
3
+ * Interactive prompts for user input
4
+ */
5
+
6
+ import inquirer from "inquirer";
7
+ import chalk from "chalk";
8
+ import * as fs from "fs/promises";
9
+ import { existsSync, type Dirent } from "fs";
10
+ import * as path from "path";
11
+ import type { Environment, Pillar } from "../../shared/types/index.js";
12
+ import { getReposRoot } from "../../shared/den-config.js";
13
+ import { envSelector } from "../../shared/environment/env-selector.js";
14
+ import { moduleScanner } from "../../shared/test-discovery/module-scanner.js";
15
+ import * as yaml from "yaml";
16
+ import { readManifestAsync } from "../../shared/manifest/read-manifest.js";
17
+
18
+ import { product } from "../../shared/product.js";
19
+ import { getDistRoot } from "../../shared/paths.js";
20
+ // Linked repos (.<name>/<repo>) live at the distribution root, wherever the
21
+ // CLI was launched from.
22
+ const linkedRepo = (...parts: string[]): string =>
23
+ path.join(getDistRoot(), `.${product().name}`, ...parts);
24
+
25
+ export class CLIPrompts {
26
+ /**
27
+ * Prompt for environment selection
28
+ */
29
+ async promptForEnvironment(): Promise<Environment> {
30
+ const availableEnvs = await envSelector.getAvailableEnvironments();
31
+
32
+ const { environment } = await inquirer.prompt<{ environment: Environment }>(
33
+ [
34
+ {
35
+ type: "list",
36
+ name: "environment",
37
+ message: "Select environment:",
38
+ choices: availableEnvs.map((env) => ({
39
+ name: this.formatEnvironmentName(env),
40
+ value: env,
41
+ })),
42
+ },
43
+ ],
44
+ );
45
+
46
+ return environment;
47
+ }
48
+
49
+ /**
50
+ * Prompt for test pillar selection
51
+ */
52
+ async promptForPillar(): Promise<Pillar> {
53
+ const { pillar } = await inquirer.prompt<{ pillar: Pillar }>([
54
+ {
55
+ type: "list",
56
+ name: "pillar",
57
+ message: "Select test pillar:",
58
+ choices: [
59
+ {
60
+ name: "๐ŸŒ Synthetic (Browser Tests)",
61
+ value: "synthetic",
62
+ },
63
+ {
64
+ name: "๐Ÿ”— Integration (API Tests)",
65
+ value: "integration",
66
+ },
67
+ {
68
+ name: "๐Ÿ’จ Smoke (Service-Owned Tests)",
69
+ value: "smoke",
70
+ },
71
+ {
72
+ name: "โšก Performance (Load Tests)",
73
+ value: "performance",
74
+ },
75
+ {
76
+ name: "๐Ÿงญ Journeys (Agent E2E)",
77
+ value: "journeys",
78
+ },
79
+ {
80
+ name: "๐Ÿ›ก Security (OWASP ZAP)",
81
+ value: "security",
82
+ },
83
+ {
84
+ name: "๐Ÿงช Suites (repo-owned tests)",
85
+ value: "suites",
86
+ },
87
+ ],
88
+ },
89
+ ]);
90
+
91
+ return pillar;
92
+ }
93
+
94
+ /**
95
+ * Prompt for test module selection (works for all pillars)
96
+ * Auto-discovers modules from filesystem
97
+ */
98
+ async promptForModule(pillar: Pillar): Promise<string | null | "back"> {
99
+ // Scan for available modules
100
+ const modules = await moduleScanner.scanModules(pillar);
101
+
102
+ // Pillar-specific labels
103
+ const labels: Record<Pillar, { all: string; message: string }> = {
104
+ synthetic: {
105
+ all: "๐ŸŽฏ All Modules (complete test suite)",
106
+ message: "Select test module:",
107
+ },
108
+ integration: {
109
+ all: "๐ŸŽฏ All APIs (complete test suite)",
110
+ message: "Select API module:",
111
+ },
112
+ smoke: {
113
+ all: "๐ŸŽฏ All Services (complete smoke suite)",
114
+ message: "Select service:",
115
+ },
116
+ performance: {
117
+ all: "๐ŸŽฏ All Performance Tests (complete suite)",
118
+ message: "Select performance test:",
119
+ },
120
+ journeys: {
121
+ all: "๐ŸŽฏ All Journeys (complete suite)",
122
+ message: "Select journey:",
123
+ },
124
+ security: {
125
+ all: "๐ŸŽฏ All Security Targets (baseline scans)",
126
+ message: "Select security target:",
127
+ },
128
+ suites: {
129
+ all: "๐ŸŽฏ Every repo suite",
130
+ message: "Select suite:",
131
+ },
132
+ };
133
+
134
+ // Build choices dynamically
135
+ const choices: Array<
136
+ | { name: string; value: string | null }
137
+ | InstanceType<typeof inquirer.Separator>
138
+ > = [
139
+ {
140
+ name: labels[pillar].all,
141
+ value: "all",
142
+ },
143
+ {
144
+ name: "โฌ…๏ธ Back to pillar selection",
145
+ value: "back",
146
+ },
147
+ new inquirer.Separator("โ”".repeat(40)),
148
+ ];
149
+
150
+ // Add discovered modules (show count and tags)
151
+ for (const module of modules) {
152
+ // For performance/journeys/security: no count (a journeys module IS
153
+ // one journey; a security module IS one scan target). For smoke: count files not test cases.
154
+ const unit = pillar === "smoke" ? "file" : "test";
155
+ const countStr =
156
+ pillar === "performance" ||
157
+ pillar === "journeys" ||
158
+ pillar === "security"
159
+ ? ""
160
+ : module.testCount === 0
161
+ ? chalk.gray(`(0 ${unit}s)`)
162
+ : chalk.cyan(
163
+ `(${module.testCount} ${unit}${module.testCount > 1 ? "s" : ""})`,
164
+ );
165
+
166
+ // Show tags if present
167
+ const tagsStr =
168
+ module.tags.length > 0
169
+ ? chalk.gray(` [${module.tags.join(", ")}]`)
170
+ : "";
171
+
172
+ choices.push({
173
+ name: `${module.icon} ${module.displayName} ${countStr}${tagsStr} - ${module.description}`,
174
+ value: module.name,
175
+ });
176
+ }
177
+
178
+ const { module } = await inquirer.prompt<{ module: string }>([
179
+ {
180
+ type: "list",
181
+ name: "module",
182
+ message: labels[pillar].message,
183
+ choices,
184
+ },
185
+ ]);
186
+
187
+ if (module === "back") return "back";
188
+ return module === "all" ? null : module;
189
+ }
190
+
191
+ /**
192
+ * Prompt for a smoke collection (suite) within a service
193
+ * moduleName format: "shop/api"
194
+ */
195
+ async promptForSmokeCollection(
196
+ moduleName: string,
197
+ ): Promise<string | null | "back"> {
198
+ const [repo, service] = moduleName.split("/");
199
+
200
+ let testDir = "";
201
+ try {
202
+ const manifest = await readManifestAsync(linkedRepo(repo));
203
+ testDir = (manifest as Record<string, unknown> | null)
204
+ ? ((manifest as { smoke?: Record<string, { testDir?: string }> })
205
+ .smoke?.[service]?.testDir ?? "")
206
+ : "";
207
+ } catch {
208
+ // manifest unreadable โ€” fall through with empty testDir
209
+ }
210
+
211
+ const collectionsDir = linkedRepo(repo, testDir);
212
+
213
+ // Discover collection subdirectories
214
+ const collections: Array<{
215
+ name: string;
216
+ icon: string;
217
+ displayName: string;
218
+ testCount: number;
219
+ }> = [];
220
+ try {
221
+ const entries = await fs.readdir(collectionsDir, { withFileTypes: true });
222
+ for (const entry of entries) {
223
+ if (!entry.isDirectory()) continue;
224
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
225
+
226
+ // Count .smoke.ts files in this collection
227
+ let testCount = 0;
228
+ try {
229
+ const files = await fs.readdir(path.join(collectionsDir, entry.name));
230
+ testCount = files.filter((f) => f.endsWith(".smoke.ts")).length;
231
+ } catch {
232
+ /* ignore */
233
+ }
234
+
235
+ // Load manifest.yml for icon/name
236
+ let icon = "๐Ÿ“ฆ";
237
+ let displayName = entry.name;
238
+ try {
239
+ const mYaml = await fs.readFile(
240
+ path.join(collectionsDir, entry.name, "manifest.yml"),
241
+ "utf-8",
242
+ );
243
+ const m = yaml.parse(mYaml);
244
+ if (m?.icon) icon = m.icon;
245
+ if (m?.name) displayName = m.name;
246
+ } catch {
247
+ /* no manifest */
248
+ }
249
+
250
+ collections.push({ name: entry.name, icon, displayName, testCount });
251
+ }
252
+ } catch {
253
+ /* dir unreadable */
254
+ }
255
+
256
+ collections.sort((a, b) => a.name.localeCompare(b.name));
257
+
258
+ const choices: Array<
259
+ | { name: string; value: string | null }
260
+ | InstanceType<typeof inquirer.Separator>
261
+ > = [
262
+ { name: `๐ŸŽฏ All collections in ${service}`, value: null },
263
+ { name: "โฌ…๏ธ Back to service selection", value: "back" },
264
+ new inquirer.Separator("โ”".repeat(40)),
265
+ ];
266
+
267
+ for (const col of collections) {
268
+ const countStr =
269
+ col.testCount === 0
270
+ ? chalk.gray("(0 files)")
271
+ : chalk.cyan(
272
+ `(${col.testCount} file${col.testCount !== 1 ? "s" : ""})`,
273
+ );
274
+ choices.push({
275
+ name: `${col.icon} ${col.displayName} ${countStr}`,
276
+ value: col.name,
277
+ });
278
+ }
279
+
280
+ const { collection } = await inquirer.prompt<{ collection: string | null }>(
281
+ [
282
+ {
283
+ type: "list",
284
+ name: "collection",
285
+ message: `Select collection in ${repo}/${service}:`,
286
+ choices,
287
+ },
288
+ ],
289
+ );
290
+
291
+ return collection === "back" ? "back" : collection;
292
+ }
293
+
294
+ /**
295
+ * Prompt for credentials setup
296
+ */
297
+ async promptForCredentials(environment: Environment): Promise<{
298
+ username: string;
299
+ password: string;
300
+ auth0ClientId?: string;
301
+ }> {
302
+ console.log(
303
+ `\n๐Ÿ” Setup credentials for: ${this.formatEnvironmentName(environment)}`,
304
+ );
305
+ console.log("โ”".repeat(50));
306
+
307
+ const answers = await inquirer.prompt([
308
+ {
309
+ type: "input",
310
+ name: "username",
311
+ message: "Username/Email:",
312
+ validate: (input: string) => {
313
+ if (!input || input.trim().length === 0) {
314
+ return "Username is required";
315
+ }
316
+ return true;
317
+ },
318
+ },
319
+ {
320
+ type: "password",
321
+ name: "password",
322
+ message: "Password:",
323
+ mask: "*",
324
+ validate: (input: string) => {
325
+ if (!input || input.trim().length === 0) {
326
+ return "Password is required";
327
+ }
328
+ return true;
329
+ },
330
+ },
331
+ {
332
+ type: "input",
333
+ name: "auth0ClientId",
334
+ message: "Auth0 Client ID (optional):",
335
+ },
336
+ ]);
337
+
338
+ return answers;
339
+ }
340
+
341
+ /**
342
+ * Prompt for elevated credentials
343
+ */
344
+ async promptForElevatedCredentials(environment: Environment): Promise<{
345
+ username: string;
346
+ password: string;
347
+ } | null> {
348
+ const { needElevated } = await inquirer.prompt<{ needElevated: boolean }>([
349
+ {
350
+ type: "confirm",
351
+ name: "needElevated",
352
+ message: "Setup elevated/admin credentials?",
353
+ default: false,
354
+ },
355
+ ]);
356
+
357
+ if (!needElevated) {
358
+ return null;
359
+ }
360
+
361
+ console.log(
362
+ `\n๐Ÿ”‘ Setup elevated credentials for: ${this.formatEnvironmentName(environment)}`,
363
+ );
364
+ console.log("โ”".repeat(50));
365
+
366
+ const answers = await inquirer.prompt([
367
+ {
368
+ type: "input",
369
+ name: "username",
370
+ message: "Admin Username/Email:",
371
+ validate: (input: string) => {
372
+ if (!input || input.trim().length === 0) {
373
+ return "Username is required";
374
+ }
375
+ return true;
376
+ },
377
+ },
378
+ {
379
+ type: "password",
380
+ name: "password",
381
+ message: "Admin Password:",
382
+ mask: "*",
383
+ validate: (input: string) => {
384
+ if (!input || input.trim().length === 0) {
385
+ return "Password is required";
386
+ }
387
+ return true;
388
+ },
389
+ },
390
+ ]);
391
+
392
+ return answers;
393
+ }
394
+
395
+ /**
396
+ * Confirm action
397
+ */
398
+ async confirm(
399
+ message: string,
400
+ defaultValue: boolean = true,
401
+ ): Promise<boolean> {
402
+ const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
403
+ {
404
+ type: "confirm",
405
+ name: "confirmed",
406
+ message,
407
+ default: defaultValue,
408
+ },
409
+ ]);
410
+
411
+ return confirmed;
412
+ }
413
+
414
+ /**
415
+ * Prompt for main menu action
416
+ */
417
+ async promptForMainAction(): Promise<
418
+ "run" | "results" | "environment" | "exit"
419
+ > {
420
+ const { action } = await inquirer.prompt<{
421
+ action: "run" | "results" | "environment" | "exit";
422
+ }>([
423
+ {
424
+ type: "list",
425
+ name: "action",
426
+ message: "What would you like to do?",
427
+ choices: [
428
+ {
429
+ name: "โ–ถ๏ธ Run Tests",
430
+ value: "run",
431
+ },
432
+ {
433
+ name: "๐Ÿ“Š View Results",
434
+ value: "results",
435
+ },
436
+ {
437
+ name: "โŒ Exit",
438
+ value: "exit",
439
+ },
440
+ ],
441
+ },
442
+ ]);
443
+
444
+ return action;
445
+ }
446
+
447
+ /**
448
+ * Prompt for number of days for results
449
+ */
450
+ async promptForResultsDays(): Promise<number> {
451
+ const { days } = await inquirer.prompt<{ days: string }>([
452
+ {
453
+ type: "list",
454
+ name: "days",
455
+ message: "Show results from:",
456
+ choices: [
457
+ { name: "Last 24 hours", value: "1" },
458
+ { name: "Last 7 days", value: "7" },
459
+ { name: "Last 30 days", value: "30" },
460
+ { name: "All time", value: "365" },
461
+ ],
462
+ },
463
+ ]);
464
+
465
+ return parseInt(days, 10);
466
+ }
467
+
468
+ async promptForWorkspaceAction(): Promise<
469
+ | "list"
470
+ | "use"
471
+ | "create"
472
+ | "add-service"
473
+ | "remove-service"
474
+ | "delete"
475
+ | "back"
476
+ > {
477
+ const { action } = await inquirer.prompt<{
478
+ action:
479
+ | "list"
480
+ | "use"
481
+ | "create"
482
+ | "add-service"
483
+ | "remove-service"
484
+ | "delete"
485
+ | "back";
486
+ }>([
487
+ {
488
+ type: "list",
489
+ name: "action",
490
+ message: "Workspaces:",
491
+ choices: [
492
+ { name: "๐Ÿ“‹ List Workspaces", value: "list" },
493
+ { name: "๐Ÿ”€ Switch Workspace", value: "use" },
494
+ { name: "โž• Create Workspace", value: "create" },
495
+ { name: "โž• Add Service to Workspace", value: "add-service" },
496
+ { name: "โž– Remove Service from Workspace", value: "remove-service" },
497
+ { name: "๐Ÿ—‘ Delete Workspace", value: "delete" },
498
+ { name: "โ† Back", value: "back" },
499
+ ],
500
+ },
501
+ ]);
502
+ return action;
503
+ }
504
+
505
+ async promptForRegistrationSource(): Promise<"local" | "github"> {
506
+ const { source } = await inquirer.prompt<{ source: "local" | "github" }>([
507
+ {
508
+ type: "list",
509
+ name: "source",
510
+ message: "Where is this repo?",
511
+ choices: [
512
+ {
513
+ name: `Local checkout โ€” I already have it on disk (${product().cli} reads it, never modifies)`,
514
+ value: "local",
515
+ },
516
+ {
517
+ name: `GitHub repo โ€” ${product().cli} clones and manages it for me`,
518
+ value: "github",
519
+ },
520
+ ],
521
+ },
522
+ ]);
523
+ return source;
524
+ }
525
+
526
+ async promptForGhOrgRepo(
527
+ repos: { name: string; nameWithOwner: string; defaultBranch: string }[],
528
+ ): Promise<{ name: string; nameWithOwner: string; defaultBranch: string }> {
529
+ const { selected } = await inquirer.prompt<{ selected: string }>([
530
+ {
531
+ type: "list",
532
+ name: "selected",
533
+ message: "Select GitHub repo:",
534
+ choices: repos.map((r) => ({
535
+ name: r.nameWithOwner,
536
+ value: r.nameWithOwner,
537
+ })),
538
+ pageSize: 20,
539
+ },
540
+ ]);
541
+ return repos.find((r) => r.nameWithOwner === selected)!;
542
+ }
543
+
544
+ async promptForRepoPath(alreadyRegistered: string[] = []): Promise<string> {
545
+ const reposRoot = getReposRoot();
546
+ const MANUAL_ENTRY = "__manual__";
547
+
548
+ // Recursively discover repos under reposRoot (dirs with .den.yml), up to depth 4.
549
+ type RepoCandidate = {
550
+ relPath: string;
551
+ absPath: string;
552
+ registered: boolean;
553
+ };
554
+ const candidates: RepoCandidate[] = [];
555
+
556
+ const walkDir = async (
557
+ dir: string,
558
+ relPrefix: string,
559
+ depth: number,
560
+ ): Promise<void> => {
561
+ if (depth > 4) return;
562
+ let entries: Dirent[];
563
+ try {
564
+ entries = await fs.readdir(dir, { withFileTypes: true });
565
+ } catch {
566
+ return;
567
+ }
568
+ await Promise.all(
569
+ entries.map(async (e) => {
570
+ if (!e.isDirectory() && !e.isSymbolicLink()) return;
571
+ if (e.name.startsWith(".")) return;
572
+ const absPath = path.join(dir, e.name);
573
+ if (absPath === getDistRoot()) return; // skip this distribution itself
574
+ const relPath = relPrefix ? `${relPrefix}/${e.name}` : e.name;
575
+ const hasManifest = await fs
576
+ .access(path.join(absPath, `${product().manifestYml}`))
577
+ .then(() => true)
578
+ .catch(() => false);
579
+ if (hasManifest) {
580
+ candidates.push({
581
+ relPath,
582
+ absPath,
583
+ registered: alreadyRegistered.includes(relPath),
584
+ });
585
+ } else {
586
+ await walkDir(absPath, relPath, depth + 1);
587
+ }
588
+ }),
589
+ );
590
+ };
591
+ await walkDir(reposRoot, "", 0);
592
+
593
+ const choices: { name: string; value: string }[] = candidates
594
+ .sort((a, b) => a.relPath.localeCompare(b.relPath))
595
+ .map((c) => {
596
+ const reg = c.registered ? chalk.gray(" (already registered)") : "";
597
+ return { name: `${chalk.green(c.relPath)}${reg}`, value: c.absPath };
598
+ });
599
+
600
+ choices.push({
601
+ name: chalk.gray("Enter path manuallyโ€ฆ"),
602
+ value: MANUAL_ENTRY,
603
+ });
604
+
605
+ const { selected } = await inquirer.prompt<{ selected: string }>([
606
+ {
607
+ type: "list",
608
+ name: "selected",
609
+ message: "Select repo:",
610
+ choices,
611
+ pageSize: 15,
612
+ },
613
+ ]);
614
+
615
+ if (selected !== MANUAL_ENTRY) return selected;
616
+
617
+ const { repoPath } = await inquirer.prompt<{ repoPath: string }>([
618
+ {
619
+ type: "input",
620
+ name: "repoPath",
621
+ message: "Absolute path to repo checkout:",
622
+ validate: (v: string) => {
623
+ const p = v.trim();
624
+ if (!p) return "Path is required";
625
+ if (!existsSync(p)) return `Path does not exist: ${p}`;
626
+ return true;
627
+ },
628
+ },
629
+ ]);
630
+ return repoPath.trim();
631
+ }
632
+
633
+ async promptForBranch(defaultValue = "main"): Promise<string> {
634
+ const { branch } = await inquirer.prompt<{ branch: string }>([
635
+ {
636
+ type: "input",
637
+ name: "branch",
638
+ message: `Branch to track (${product().cli} will clone/pull this branch):`,
639
+ default: defaultValue,
640
+ validate: (v: string) => (v.trim() ? true : "Branch name is required"),
641
+ },
642
+ ]);
643
+ return branch.trim();
644
+ }
645
+
646
+ async promptForLinkedPath(defaultPath: string): Promise<string> {
647
+ const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
648
+ {
649
+ type: "confirm",
650
+ name: "confirmed",
651
+ message: `Use this path for the linked checkout?\n ${defaultPath}`,
652
+ default: true,
653
+ },
654
+ ]);
655
+ if (confirmed) return defaultPath;
656
+
657
+ const { p } = await inquirer.prompt<{ p: string }>([
658
+ {
659
+ type: "input",
660
+ name: "p",
661
+ message: "Absolute path to your local checkout:",
662
+ validate: (v: string) => {
663
+ const trimmed = v.trim();
664
+ if (!trimmed) return "Path is required";
665
+ if (!existsSync(trimmed)) return `Path does not exist: ${trimmed}`;
666
+ return true;
667
+ },
668
+ },
669
+ ]);
670
+ return p.trim();
671
+ }
672
+
673
+ async promptForGhSlug(defaultValue?: string): Promise<string> {
674
+ const { slug } = await inquirer.prompt<{ slug: string }>([
675
+ {
676
+ type: "input",
677
+ name: "slug",
678
+ message: "GitHub repo slug (owner/repo):",
679
+ default: defaultValue,
680
+ validate: (v: string) =>
681
+ /^[\w.-]+\/[\w.-]+$/.test(v.trim())
682
+ ? true
683
+ : "Expected format: owner/repo",
684
+ },
685
+ ]);
686
+ return slug.trim();
687
+ }
688
+
689
+ async promptSelectWorkspace(
690
+ workspaces: string[],
691
+ current: string,
692
+ ): Promise<string> {
693
+ const { name } = await inquirer.prompt<{ name: string }>([
694
+ {
695
+ type: "list",
696
+ name: "name",
697
+ message: "Select workspace:",
698
+ choices: workspaces.map((w) => ({
699
+ name: w === current ? `${w} (active)` : w,
700
+ value: w,
701
+ })),
702
+ },
703
+ ]);
704
+ return name;
705
+ }
706
+
707
+ async promptForWorkspaceName(existing: string[]): Promise<string> {
708
+ const { name } = await inquirer.prompt<{ name: string }>([
709
+ {
710
+ type: "input",
711
+ name: "name",
712
+ message: "Workspace name:",
713
+ validate: (v: string) => {
714
+ const n = v.trim();
715
+ if (!n) return "Name is required";
716
+ if (existing.includes(n)) return `Workspace "${n}" already exists`;
717
+ // eslint-disable-next-line no-control-regex
718
+ if (/["'\\/<>\x00-\x1f]/.test(n))
719
+ return "Name must not contain quotes, slashes, or control characters";
720
+ return true;
721
+ },
722
+ },
723
+ ]);
724
+ return name.trim();
725
+ }
726
+
727
+ async promptSelectServices(
728
+ addresses: string[],
729
+ message: string,
730
+ defaultAll = true,
731
+ ): Promise<string[]> {
732
+ const { selected } = await inquirer.prompt<{ selected: string[] }>([
733
+ {
734
+ type: "checkbox",
735
+ name: "selected",
736
+ message,
737
+ choices: addresses.map((a) => ({
738
+ name: a,
739
+ value: a,
740
+ checked: defaultAll,
741
+ })),
742
+ pageSize: 20,
743
+ },
744
+ ]);
745
+ return selected;
746
+ }
747
+
748
+ /**
749
+ * Format environment name for display
750
+ */
751
+ private formatEnvironmentName(env: Environment): string {
752
+ const names: Record<Environment, string> = {
753
+ local: "๐Ÿ  Local",
754
+ dev: "๐Ÿ”ง Development",
755
+ sandbox: "๐Ÿ–๏ธ Sandbox",
756
+ staging: "๐ŸŽญ Staging",
757
+ beta: "๐Ÿงช Beta",
758
+ prod: "๐Ÿš€ Prod",
759
+ production: "๐Ÿš€ Production",
760
+ };
761
+
762
+ return names[env] || env;
763
+ }
764
+ }
765
+
766
+ // Export singleton instance
767
+ export const cliPrompts = new CLIPrompts();