@bugmole/cli 0.4.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 (308) hide show
  1. package/.bugmole.env.example +20 -0
  2. package/LICENSE +7 -0
  3. package/README.md +293 -0
  4. package/TESTING.md +117 -0
  5. package/bugmole.config.yaml +39 -0
  6. package/package.json +85 -0
  7. package/scripts/billing/paypal-setup.mjs +121 -0
  8. package/scripts/bugmole-continue.ts +318 -0
  9. package/scripts/bugmole-init.ts +188 -0
  10. package/scripts/bugmole.cjs +17 -0
  11. package/scripts/bugmole.test.ts +344 -0
  12. package/scripts/bugmole.ts +657 -0
  13. package/scripts/ensure-maestro.cjs +79 -0
  14. package/scripts/ios-tunnel-keeper.sh +45 -0
  15. package/scripts/ios-wda-keeper.sh +66 -0
  16. package/scripts/sync-plan-catalog.d.mts +3 -0
  17. package/scripts/sync-plan-catalog.mjs +16 -0
  18. package/scripts/ui-parity-diff.py +65 -0
  19. package/scripts/ui-parity-requirements.txt +1 -0
  20. package/scripts/verify-manage-to-plans.mts +194 -0
  21. package/spec/app-ui-audit.schema.json +176 -0
  22. package/spec/blockers.yaml +79 -0
  23. package/spec/bugs.index.json +42 -0
  24. package/spec/design-dna.schema.json +38 -0
  25. package/spec/domain_rules.yaml +24 -0
  26. package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
  27. package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
  28. package/spec/journey_graph.yaml +126 -0
  29. package/spec/journeys.graph.json +2618 -0
  30. package/spec/plans/dashboard-smoke.flow.yaml +20 -0
  31. package/spec/plans/example.flow.yaml +99 -0
  32. package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
  33. package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
  34. package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
  35. package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
  36. package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
  37. package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
  38. package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
  39. package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
  40. package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
  41. package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
  42. package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
  43. package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
  44. package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
  45. package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
  46. package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
  47. package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
  48. package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
  49. package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
  50. package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
  51. package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
  52. package/spec/plans/login.flow.yaml +22 -0
  53. package/spec/plans/owner-operations.flow.yaml +20 -0
  54. package/spec/project_config.yaml +55 -0
  55. package/spec/roles.yaml +30 -0
  56. package/spec/schema.md +394 -0
  57. package/spec/test-case-results.schema.json +62 -0
  58. package/spec/test-cases.schema.json +85 -0
  59. package/spec/ui-parity-audit.schema.json +194 -0
  60. package/spec/ui-reverse-engineering.schema.json +94 -0
  61. package/src/billing/plan-catalog.test.ts +46 -0
  62. package/src/billing/plan-catalog.ts +199 -0
  63. package/src/integrations/aws-sigv4.test.ts +42 -0
  64. package/src/integrations/aws-sigv4.ts +72 -0
  65. package/src/integrations/device-farm.ts +155 -0
  66. package/src/integrations/github-app.test.ts +57 -0
  67. package/src/integrations/github-app.ts +143 -0
  68. package/src/integrations/gitlab.ts +81 -0
  69. package/src/integrations/temp-email.test.ts +123 -0
  70. package/src/integrations/temp-email.ts +175 -0
  71. package/src/integrations/testflight-feedback.test.ts +51 -0
  72. package/src/integrations/testflight-feedback.ts +173 -0
  73. package/src/integrations/webdriver-client.ts +131 -0
  74. package/src/mcp/server.test.ts +1220 -0
  75. package/src/mcp/server.ts +3064 -0
  76. package/src/mcp/write-test-cases.test.ts +287 -0
  77. package/src/registry/api-key-client.ts +39 -0
  78. package/src/registry/control-plane-client.ts +212 -0
  79. package/src/registry/migrations/0001_registry.sql +47 -0
  80. package/src/registry/migrations/0002_device_authorizations.sql +23 -0
  81. package/src/registry/migrations/0003_project_environments.sql +25 -0
  82. package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
  83. package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
  84. package/src/registry/migrations/0006_workspaces.sql +36 -0
  85. package/src/registry/migrations/0007_project_apps.sql +26 -0
  86. package/src/registry/migrations/0008_agent_tasks.sql +30 -0
  87. package/src/registry/migrations/0009_journey_revisions.sql +17 -0
  88. package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
  89. package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
  90. package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
  91. package/src/registry/migrations/0013_device_sessions.sql +22 -0
  92. package/src/registry/migrations/0014_agent_task_step.sql +1 -0
  93. package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
  94. package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
  95. package/src/registry/migrations/0017_run_targets.sql +7 -0
  96. package/src/registry/migrations/0018_run_fix_from.sql +4 -0
  97. package/src/registry/migrations/0019_workspace_flags.sql +9 -0
  98. package/src/registry/migrations/0020_orgs.sql +40 -0
  99. package/src/registry/migrations/0021_billing_core.sql +58 -0
  100. package/src/registry/migrations/0022_cloud_runners.sql +19 -0
  101. package/src/registry/migrations/0023_signup.sql +4 -0
  102. package/src/registry/migrations/0024_billing.sql +67 -0
  103. package/src/registry/migrations/0025_notifications.sql +47 -0
  104. package/src/registry/migrations/0026_repo_bindings.sql +28 -0
  105. package/src/registry/migrations/0027_feedback.sql +29 -0
  106. package/src/registry/migrations/0028_devices.sql +48 -0
  107. package/src/registry/migrations/0029_sso.sql +31 -0
  108. package/src/registry/migrations/0030_workspace_domains.sql +18 -0
  109. package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
  110. package/src/registry/migrations/0032_personas.sql +15 -0
  111. package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
  112. package/src/registry/task-scheduling.test.ts +100 -0
  113. package/src/registry/task-scheduling.ts +80 -0
  114. package/src/registry-worker/ai/platform-model.ts +77 -0
  115. package/src/registry-worker/ai/routes.test.ts +88 -0
  116. package/src/registry-worker/ai/routes.ts +90 -0
  117. package/src/registry-worker/artifacts.test.ts +98 -0
  118. package/src/registry-worker/artifacts.ts +85 -0
  119. package/src/registry-worker/billing/billing-core.test.ts +175 -0
  120. package/src/registry-worker/billing/checkout-routes.ts +209 -0
  121. package/src/registry-worker/billing/enforcement.ts +69 -0
  122. package/src/registry-worker/billing/entitlements.ts +108 -0
  123. package/src/registry-worker/billing/ledger.ts +186 -0
  124. package/src/registry-worker/billing/paypal/api.ts +259 -0
  125. package/src/registry-worker/billing/paypal/client.ts +91 -0
  126. package/src/registry-worker/billing/paypal/provider.ts +143 -0
  127. package/src/registry-worker/billing/paypal.test.ts +466 -0
  128. package/src/registry-worker/billing/provider.ts +114 -0
  129. package/src/registry-worker/billing/routes.ts +66 -0
  130. package/src/registry-worker/billing/subscriptions.ts +780 -0
  131. package/src/registry-worker/billing/thresholds.ts +107 -0
  132. package/src/registry-worker/core.ts +308 -0
  133. package/src/registry-worker/devices/devices.test.ts +185 -0
  134. package/src/registry-worker/devices/policy.ts +71 -0
  135. package/src/registry-worker/devices/routes.ts +453 -0
  136. package/src/registry-worker/domains/domains.test.ts +210 -0
  137. package/src/registry-worker/domains/routes.ts +139 -0
  138. package/src/registry-worker/domains.ts +88 -0
  139. package/src/registry-worker/email/sender.ts +75 -0
  140. package/src/registry-worker/env.d.ts +14716 -0
  141. package/src/registry-worker/features.ts +20 -0
  142. package/src/registry-worker/feedback/feedback.test.ts +230 -0
  143. package/src/registry-worker/feedback/format.ts +148 -0
  144. package/src/registry-worker/feedback/routes.ts +386 -0
  145. package/src/registry-worker/flags.ts +39 -0
  146. package/src/registry-worker/github/checks.test.ts +177 -0
  147. package/src/registry-worker/github/checks.ts +374 -0
  148. package/src/registry-worker/gitlab/checks.test.ts +141 -0
  149. package/src/registry-worker/gitlab/checks.ts +349 -0
  150. package/src/registry-worker/hooks.ts +54 -0
  151. package/src/registry-worker/index.ts +2077 -0
  152. package/src/registry-worker/jobs/index.ts +29 -0
  153. package/src/registry-worker/jobs/retention.ts +68 -0
  154. package/src/registry-worker/mcp/mcp.test.ts +355 -0
  155. package/src/registry-worker/mcp/routes.ts +215 -0
  156. package/src/registry-worker/mcp/token.ts +126 -0
  157. package/src/registry-worker/mcp/tools.ts +563 -0
  158. package/src/registry-worker/notifications/alerts.ts +212 -0
  159. package/src/registry-worker/notifications/notifications.test.ts +298 -0
  160. package/src/registry-worker/notifications/outbox.ts +83 -0
  161. package/src/registry-worker/notifications/routes.ts +280 -0
  162. package/src/registry-worker/notifications/secrets.ts +49 -0
  163. package/src/registry-worker/notifications/slack.ts +96 -0
  164. package/src/registry-worker/notifications/teams.ts +46 -0
  165. package/src/registry-worker/org/audit.ts +116 -0
  166. package/src/registry-worker/org/routes.test.ts +163 -0
  167. package/src/registry-worker/org/routes.ts +302 -0
  168. package/src/registry-worker/personas/personas.test.ts +78 -0
  169. package/src/registry-worker/personas/routes.ts +100 -0
  170. package/src/registry-worker/repo-triggers.ts +20 -0
  171. package/src/registry-worker/routes/index.ts +74 -0
  172. package/src/registry-worker/run-events.ts +24 -0
  173. package/src/registry-worker/runner/dispatch.ts +219 -0
  174. package/src/registry-worker/runner/jobs.ts +43 -0
  175. package/src/registry-worker/runner/metering.ts +82 -0
  176. package/src/registry-worker/runner/policy.ts +59 -0
  177. package/src/registry-worker/runner/routes.ts +171 -0
  178. package/src/registry-worker/runner/runner.test.ts +358 -0
  179. package/src/registry-worker/runner/tokens.ts +93 -0
  180. package/src/registry-worker/runs.test.ts +60 -0
  181. package/src/registry-worker/signup/policy.ts +57 -0
  182. package/src/registry-worker/signup/routes.ts +106 -0
  183. package/src/registry-worker/signup/signup.test.ts +81 -0
  184. package/src/registry-worker/sso/aegis.ts +141 -0
  185. package/src/registry-worker/sso/membership.ts +157 -0
  186. package/src/registry-worker/sso/routes.ts +458 -0
  187. package/src/registry-worker/sso/sso.test.ts +344 -0
  188. package/src/registry-worker/testing/d1-shim.ts +180 -0
  189. package/src/registry-worker/testing/harness.ts +137 -0
  190. package/src/runner-worker/index.ts +108 -0
  191. package/src/runtime/ai-analysis.ts +97 -0
  192. package/src/runtime/ai-exploration.test.ts +32 -0
  193. package/src/runtime/ai-exploration.ts +69 -0
  194. package/src/runtime/ai-repair.ts +74 -0
  195. package/src/runtime/ai-work.test.ts +99 -0
  196. package/src/runtime/android-screen-record.test.ts +75 -0
  197. package/src/runtime/android-screen-record.ts +192 -0
  198. package/src/runtime/app-understanding.test.ts +123 -0
  199. package/src/runtime/app-understanding.ts +201 -0
  200. package/src/runtime/appium-driver.test.ts +179 -0
  201. package/src/runtime/appium-driver.ts +295 -0
  202. package/src/runtime/blocker-resolution.test.ts +113 -0
  203. package/src/runtime/blocker-resolution.ts +111 -0
  204. package/src/runtime/browser-matrix.integration.test.ts +212 -0
  205. package/src/runtime/browser-matrix.test.ts +143 -0
  206. package/src/runtime/browser-matrix.ts +200 -0
  207. package/src/runtime/canonical-flow.test.ts +52 -0
  208. package/src/runtime/config-validate.ts +185 -0
  209. package/src/runtime/continuous-execution.ts +291 -0
  210. package/src/runtime/cursor-applescript.ts +573 -0
  211. package/src/runtime/cursor-cli-driver.test.ts +78 -0
  212. package/src/runtime/cursor-cli-driver.ts +156 -0
  213. package/src/runtime/cursor-driver-example.ts +117 -0
  214. package/src/runtime/cursor-driver-index.ts +65 -0
  215. package/src/runtime/cursor-driver-init.ts +277 -0
  216. package/src/runtime/cursor-driver-run.test.ts +15 -0
  217. package/src/runtime/cursor-driver-run.ts +323 -0
  218. package/src/runtime/cursor-driver.ts +332 -0
  219. package/src/runtime/cursor-llm-example.ts +90 -0
  220. package/src/runtime/cursor-llm.ts +206 -0
  221. package/src/runtime/cursor-mcp-monitor.ts +386 -0
  222. package/src/runtime/device-clouds/browserstack.ts +73 -0
  223. package/src/runtime/device-clouds/device-farm.ts +52 -0
  224. package/src/runtime/device-clouds/index.ts +92 -0
  225. package/src/runtime/device-clouds/kobiton.ts +70 -0
  226. package/src/runtime/device-clouds/targets.ts +44 -0
  227. package/src/runtime/device-clouds/types.ts +62 -0
  228. package/src/runtime/diff-proposal.ts +84 -0
  229. package/src/runtime/discovery-task.test.ts +29 -0
  230. package/src/runtime/discovery-task.ts +284 -0
  231. package/src/runtime/driver-recovery.ts +69 -0
  232. package/src/runtime/driver.ts +79 -0
  233. package/src/runtime/environment.test.ts +104 -0
  234. package/src/runtime/environment.ts +137 -0
  235. package/src/runtime/executor.test.ts +509 -0
  236. package/src/runtime/executor.ts +921 -0
  237. package/src/runtime/explorer.test.ts +101 -0
  238. package/src/runtime/explorer.ts +1013 -0
  239. package/src/runtime/failure-analysis.test.ts +111 -0
  240. package/src/runtime/failure-analysis.ts +272 -0
  241. package/src/runtime/fixtures/fake-maestro.sh +36 -0
  242. package/src/runtime/flow-language.test.ts +268 -0
  243. package/src/runtime/flow-language.ts +414 -0
  244. package/src/runtime/init-wizard.ts +354 -0
  245. package/src/runtime/ios-screen-record.test.ts +68 -0
  246. package/src/runtime/ios-screen-record.ts +155 -0
  247. package/src/runtime/journey-editor.ts +452 -0
  248. package/src/runtime/journey-evidence.test.ts +161 -0
  249. package/src/runtime/journey-evidence.ts +180 -0
  250. package/src/runtime/journey-graph.test.ts +257 -0
  251. package/src/runtime/journey-graph.ts +170 -0
  252. package/src/runtime/legacy-names.ts +32 -0
  253. package/src/runtime/llm-example.ts +105 -0
  254. package/src/runtime/llm.ts +527 -0
  255. package/src/runtime/local-browser.test.ts +45 -0
  256. package/src/runtime/local-browser.ts +48 -0
  257. package/src/runtime/local-registry-stub.test.ts +325 -0
  258. package/src/runtime/local-registry-stub.ts +803 -0
  259. package/src/runtime/maestro-driver.test.ts +84 -0
  260. package/src/runtime/maestro-driver.ts +209 -0
  261. package/src/runtime/mole-voice.ts +21 -0
  262. package/src/runtime/nav-crawl.test.ts +100 -0
  263. package/src/runtime/nav-crawl.ts +153 -0
  264. package/src/runtime/pipeline.test.ts +405 -0
  265. package/src/runtime/pipeline.ts +833 -0
  266. package/src/runtime/planner.test.ts +37 -0
  267. package/src/runtime/planner.ts +274 -0
  268. package/src/runtime/platform-ai.ts +76 -0
  269. package/src/runtime/playwright-driver.test.ts +93 -0
  270. package/src/runtime/playwright-driver.ts +620 -0
  271. package/src/runtime/project-spec.ts +140 -0
  272. package/src/runtime/record-run-verdicts.ts +68 -0
  273. package/src/runtime/reporter.test.ts +56 -0
  274. package/src/runtime/reporter.ts +158 -0
  275. package/src/runtime/reset.test.ts +44 -0
  276. package/src/runtime/reset.ts +61 -0
  277. package/src/runtime/reviewer.test.ts +73 -0
  278. package/src/runtime/reviewer.ts +158 -0
  279. package/src/runtime/run-job.ts +136 -0
  280. package/src/runtime/run-once.test.ts +207 -0
  281. package/src/runtime/run-once.ts +168 -0
  282. package/src/runtime/run.ts +132 -0
  283. package/src/runtime/screen-recording.ts +34 -0
  284. package/src/runtime/serve-gateway.test.ts +74 -0
  285. package/src/runtime/serve-gateway.ts +164 -0
  286. package/src/runtime/serve-worker.test.ts +23 -0
  287. package/src/runtime/serve-worker.ts +278 -0
  288. package/src/runtime/site-discovery.test.ts +168 -0
  289. package/src/runtime/site-discovery.ts +308 -0
  290. package/src/runtime/target-runner.ts +144 -0
  291. package/src/runtime/test-case-verdicts.test.ts +94 -0
  292. package/src/runtime/test-case-verdicts.ts +120 -0
  293. package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
  294. package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
  295. package/src/runtime/ui-reverse-engineering/types.ts +197 -0
  296. package/src/runtime/web-suite.test.ts +97 -0
  297. package/src/runtime/web-suite.ts +176 -0
  298. package/src/storage/create-object-store.ts +144 -0
  299. package/src/storage/keys.ts +34 -0
  300. package/src/storage/local-artifact-server.test.ts +314 -0
  301. package/src/storage/local-artifact-server.ts +357 -0
  302. package/src/storage/object-store.test.ts +28 -0
  303. package/src/storage/object-store.ts +101 -0
  304. package/src/storage/registry-object-store.ts +88 -0
  305. package/src/storage/remote-object-store.ts +104 -0
  306. package/src/storage/storage-directory.test.ts +43 -0
  307. package/src/storage/storage-directory.ts +24 -0
  308. package/tsconfig.json +24 -0
@@ -0,0 +1,657 @@
1
+ #!/usr/bin/env node
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { fileURLToPath } from "node:url";
8
+ import dotenv from "dotenv";
9
+ import YAML from "yaml";
10
+ import { resolveLocalStorageDirectory } from "../src/storage/storage-directory.js";
11
+ import { adoptLegacyEnv, preferExisting } from "../src/runtime/legacy-names.js";
12
+
13
+ type Mode =
14
+ | "api-key"
15
+ | "init"
16
+ | "mcp"
17
+ | "explore"
18
+ | "plan"
19
+ | "run"
20
+ | "propose-diff"
21
+ | "review"
22
+ | "report"
23
+ | "artifacts"
24
+ | "serve"
25
+ | "gateway"
26
+ | "run-once"
27
+ | "reset"
28
+ | "cursor-driver"
29
+ | "suite"
30
+ | "init-env";
31
+
32
+ function readArg(name: string, argv = process.argv): string | undefined {
33
+ const i = argv.indexOf(name);
34
+ if (i >= 0) return argv[i + 1];
35
+ return undefined;
36
+ }
37
+
38
+ export function parseArgs(argv = process.argv): Record<string, any> {
39
+ const args: Record<string, any> = {};
40
+ const positionalCommand = argv.slice(2).find((arg) => !arg.startsWith("--"));
41
+ if (positionalCommand === "init") args.init = true;
42
+ if (positionalCommand === "artifacts") args.artifacts = true;
43
+ if (positionalCommand === "serve") args.serve = true;
44
+ if (positionalCommand === "gateway") args.gateway = true;
45
+ if (positionalCommand === "run-once") args.runonce = true;
46
+ if (positionalCommand === "reset") args.reset = true;
47
+ for (let i = 0; i < argv.length; i++) {
48
+ const arg = argv[i];
49
+ if (arg.startsWith("--")) {
50
+ const key = arg.slice(2).replace(/-/g, "");
51
+ const next = argv[i + 1];
52
+ const value = next && !next.startsWith("--") ? next : true;
53
+ if (value !== true) i++;
54
+ // Try to parse as number or boolean
55
+ if (value === true || value === "true") args[key] = true;
56
+ else if (value === "false") args[key] = false;
57
+ else if (!isNaN(Number(value))) args[key] = Number(value);
58
+ else args[key] = value;
59
+ }
60
+ }
61
+ return args;
62
+ }
63
+
64
+ export function printHelp(init = false): void {
65
+ if (init) {
66
+ console.log(`Usage: bugmole init [options]
67
+
68
+ Initialize .bugmole/config.yaml and .bugmole.env without reading credentials.
69
+
70
+ Options:
71
+ --non-interactive Use defaults and never prompt
72
+ --force Overwrite an existing configuration
73
+ --storage-provider <name> local, gcs, s3, or r2
74
+ --workspace-id <id> Workspace identifier
75
+ --workspace-name <name> Workspace display name
76
+ --app-id <id> Default app identifier
77
+ --app-name <name> Default app display name
78
+ --config <path> Configuration output path
79
+ --help Show this help`);
80
+ return;
81
+ }
82
+ console.log(`Usage: bugmole [command] [options]
83
+
84
+ Commands:
85
+ init Initialize a project configuration
86
+ api-key create Generate a one-time machine API key
87
+ --mcp Start the MCP server
88
+ artifacts serve Serve local artifacts to the dashboard over loopback
89
+ --artifact-port <port> Loopback port (default: 3190)
90
+ --dashboard-origin <url> Allowed dashboard origin
91
+ serve Start the local artifact bridge and HTTP MCP server
92
+ --no-open Print the dashboard handoff without opening a browser
93
+ --dashboard-url <url> Dashboard URL for the handoff (overrides BUGMOLE_DASHBOARD_URL)
94
+ --local-registry Stand in for the real registry locally (overrides BUGMOLE_LOCAL_REGISTRY)
95
+ so queueing tasks/runs and this worker executing them works without
96
+ a real Aegis login or a BUGMOLE_API_KEY scoped to this project.
97
+ --local-registry-port Local registry stub port (default: 4600, or BUGMOLE_LOCAL_REGISTRY_PORT)
98
+ gateway Multiplex project serve workers over loopback
99
+ --gateway-port <port> Loopback port (default: 3191)
100
+ --gateway-secret <value> Shared secret for worker registration
101
+ reset Delete current-project execution data after confirmation
102
+ run-once Execute one Bugmole Cloud run and exit (used inside cloud runners;
103
+ reads BUGMOLE_REGISTRY_URL, BUGMOLE_RUN_ID, BUGMOLE_RUN_TOKEN)
104
+ --mode <mode> explore, plan, run, report, review, propose-diff, or suite
105
+ --mode suite Run a directory of web flows against a running app
106
+ --flows <dir> Flow directory (default: .maestro)
107
+ --base-url <url> Application base URL (or BUGMOLE_BASE_URL)
108
+ --artifacts-dir <dir> Evidence directory (default: qa/bugmole-runs)
109
+ --project <id> Project id used in the evidence path
110
+ --grep <text> Only run flows whose file name contains <text>
111
+ --share-session Keep one signed-in session across the suite
112
+ --headed Run with a visible browser
113
+ --browsers <list> Run on several browsers in parallel: chromium, firefox, webkit, msedge
114
+ --devices <list> Also emulate devices, e.g. "iPhone 15,Pixel 7"
115
+ --mode run Execute a published plan
116
+ --plan-id <id> Plan to run
117
+ --base-url <url> Run against this environment URL instead of the plan's
118
+ --browsers, --devices Same matrix flags as --mode suite
119
+ --help Show this help
120
+
121
+ Aliases: bugmole and bm`);
122
+ }
123
+
124
+ export function resolveMode(args: Record<string, any>): Mode {
125
+ if (args.init === true) return "init";
126
+ if (args.mcp === true) return "mcp";
127
+ if (args.serve === true) return "serve";
128
+ if (args.gateway === true) return "gateway";
129
+ if (args.runonce === true) return "run-once";
130
+ if (args.reset === true) return "reset";
131
+ if (args.artifacts === true) return "artifacts";
132
+ return (args.mode ?? "mcp") as Mode;
133
+ }
134
+
135
+ // `run` never throws on a failed or blocked target (see src/runtime/executor.ts) — it
136
+ // reports that in `result.status` so the caller can decide what a failure means. A CI
137
+ // step needs it to fail the build, so `--mode run` is the one mode whose exit code
138
+ // reflects the result rather than just whether the process threw.
139
+ export function runModeExitCode(mode: string, result: unknown): number {
140
+ if (mode !== "run") return 0;
141
+ const status = result && typeof result === "object" ? (result as { status?: unknown }).status : undefined;
142
+ return status === "success" ? 0 : 1;
143
+ }
144
+
145
+ export function shouldOpenServeBrowser(
146
+ args: Record<string, any>,
147
+ env: Record<string, string | undefined> = process.env,
148
+ platform: NodeJS.Platform = process.platform,
149
+ ): boolean {
150
+ if (args.noopen === true) return false;
151
+ // A headless machine (CI, a cloud container) has no browser to open.
152
+ if (env.CI?.trim() === "true" || env.BUGMOLE_HEADLESS?.trim() === "true") return false;
153
+ if (platform === "linux" && !env.DISPLAY?.trim() && !env.WAYLAND_DISPLAY?.trim()) return false;
154
+ return true;
155
+ }
156
+
157
+ export function resolveDashboardUrl(args: Record<string, any> = {}): string {
158
+ if (args.dashboardurl === true) {
159
+ throw new Error("--dashboard-url requires a URL");
160
+ }
161
+ const configuredUrl =
162
+ typeof args.dashboardurl === "string" && args.dashboardurl.trim()
163
+ ? args.dashboardurl.trim()
164
+ : process.env.BUGMOLE_DASHBOARD_URL?.trim() || "https://app.bugmole.com";
165
+ try {
166
+ return new URL(configuredUrl).toString();
167
+ } catch {
168
+ throw new Error(`Invalid dashboard URL: ${configuredUrl}`);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * The local artifact bridge's CORS allowlist must match whatever origin the
174
+ * printed handoff link actually sends the browser to. `--dashboard-origin`
175
+ * remains an explicit override for when the two legitimately differ (e.g. a
176
+ * proxy in front of the dashboard); otherwise this defaults to the resolved
177
+ * dashboard URL's own origin instead of silently staying production and
178
+ * CORS-blocking every non-default dashboard.
179
+ */
180
+ export function resolveDashboardOrigin(args: Record<string, any>, dashboardUrl: string): string {
181
+ if (typeof args.dashboardorigin === "string" && args.dashboardorigin.trim()) {
182
+ return args.dashboardorigin.trim();
183
+ }
184
+ return new URL(dashboardUrl).origin;
185
+ }
186
+
187
+ /**
188
+ * A project with no real registered access (no BUGMOLE_API_KEY scoped to
189
+ * it) has every registry write rejected — "Explore this app", "Queue
190
+ * task", and "Run tests" all 403, and `bugmole serve`'s own worker
191
+ * never starts ("Worker idle: registry URL, API key, and project id are
192
+ * required"). `--local-registry`/BUGMOLE_LOCAL_REGISTRY opts into a
193
+ * loopback-only stand-in registry (`src/runtime/local-registry-stub.ts`)
194
+ * instead, so the full local write→claim→execute loop works without real
195
+ * Aegis credentials. Explicit opt-in, default off, so `bugmole serve` against
196
+ * a project that DOES have real registry access keeps using it unchanged.
197
+ */
198
+ export function resolveLocalRegistryEnabled(args: Record<string, any>): boolean {
199
+ if (args.localregistry === true) return true;
200
+ if (args.localregistry === false) return false;
201
+ return process.env.BUGMOLE_LOCAL_REGISTRY?.trim() === "true";
202
+ }
203
+
204
+ // Fixed (not random) so a dashboard's .env.local pointing BUGMOLE_REGISTRY_URL
205
+ // here keeps working across restarts without regenerating it each session —
206
+ // unlike the artifact bridge's port, this one isn't handed to the dashboard
207
+ // via a freshly-printed handoff link every time. Chosen off the Fetch
208
+ // spec's forbidden-port list (4190, an earlier default, silently failed
209
+ // every fetch with "bad port" — 4190 is the reserved ManageSieve port).
210
+ const DEFAULT_LOCAL_REGISTRY_PORT = 4600;
211
+
212
+ export function resolveLocalRegistryPort(args: Record<string, any>): number {
213
+ if (typeof args.localregistryport === "number") return args.localregistryport;
214
+ const fromEnv = process.env.BUGMOLE_LOCAL_REGISTRY_PORT?.trim();
215
+ return fromEnv ? Number(fromEnv) : DEFAULT_LOCAL_REGISTRY_PORT;
216
+ }
217
+
218
+ /**
219
+ * Aegis's device-passkey flow binds a `deviceId` to one tenant permanently
220
+ * (`deviceBinding` is unique per `(tenantId, deviceId)`) — a second claim
221
+ * attempt for the same id fails with `device_already_claimed` forever after,
222
+ * it does not expire or reset. `deviceId` used to be `cfg.project?.id`,
223
+ * which for this repo's own `.bugmole/config.yaml` is the fixed literal
224
+ * `"bugmole"`: the same string for every checkout, every teammate, and
225
+ * every re-run of `bugmole serve` from this project. The first successful
226
+ * onboarding permanently claims that id, so literally anyone's second
227
+ * attempt — a different machine, a different person, or the same person
228
+ * after restarting `bugmole serve` — collides with it. A device id needs to
229
+ * identify one physical machine, not one project name, so this generates a
230
+ * random one on first use and persists it outside the repo (survives
231
+ * `git clean`, shared by every project run from this machine, matching how
232
+ * a device passkey is actually scoped to one machine's secure enclave).
233
+ */
234
+ export function resolveDeviceId(options: { stateDir?: string } = {}): string {
235
+ const override = process.env.BUGMOLE_DEVICE_ID?.trim();
236
+ if (override) return override;
237
+ const stateDir = options.stateDir ?? preferExisting(path.join(os.homedir(), ".bugmole"), path.join(os.homedir(), ".valkyrie"));
238
+ const statePath = path.join(stateDir, "device-id");
239
+ try {
240
+ const existing = fs.readFileSync(statePath, "utf-8").trim();
241
+ if (existing) return existing;
242
+ } catch {
243
+ // No cached device id yet (or the state dir doesn't exist) — generate one below.
244
+ }
245
+ const generated = crypto.randomUUID();
246
+ try {
247
+ fs.mkdirSync(stateDir, { recursive: true });
248
+ fs.writeFileSync(statePath, `${generated}\n`, { mode: 0o600 });
249
+ } catch {
250
+ // Best-effort persistence: if the cache can't be written, this run still
251
+ // gets a valid device id, it just won't be remembered for next time.
252
+ }
253
+ return generated;
254
+ }
255
+
256
+ export function buildDashboardHandoffUrl(
257
+ dashboardUrl: string,
258
+ projectId: string,
259
+ fragment: URLSearchParams,
260
+ ): string {
261
+ const handoff = new URL(dashboardUrl);
262
+ handoff.searchParams.set("project", projectId);
263
+ handoff.hash = fragment.toString();
264
+ return handoff.toString();
265
+ }
266
+
267
+ function requireEnv(key: string): string {
268
+ const v = process.env[key];
269
+ if (!v) throw new Error(`Missing required env var: ${key}`);
270
+ return v;
271
+ }
272
+
273
+ // tiny ${ENV} interpolator for yaml configs
274
+ // Supports ${VAR:-default} syntax for default values
275
+ function interpolateEnv(obj: any): any {
276
+ if (typeof obj === "string") {
277
+ return obj.replace(/\$\{([A-Z0-9_]+)(?::-([^}]+))?\}/g, (_, k, defaultValue) => {
278
+ const envValue = process.env[k];
279
+ if (envValue !== undefined && envValue !== "") {
280
+ return envValue;
281
+ }
282
+ return defaultValue !== undefined ? defaultValue : "";
283
+ });
284
+ }
285
+ if (Array.isArray(obj)) return obj.map(interpolateEnv);
286
+ if (obj && typeof obj === "object") {
287
+ const out: any = {};
288
+ for (const [k, v] of Object.entries(obj)) out[k] = interpolateEnv(v);
289
+ return out;
290
+ }
291
+ return obj;
292
+ }
293
+
294
+ function loadConfig(configPath: string) {
295
+ const raw = fs.readFileSync(configPath, "utf-8");
296
+ const parsed = YAML.parse(raw);
297
+ return interpolateEnv(parsed);
298
+ }
299
+
300
+ function generateEnvExample() {
301
+ const envExample = `# --- LLM / Agent Provider ---
302
+ LLM_PROVIDER=openai
303
+ LLM_API_KEY=REPLACE_ME
304
+ LLM_MODEL=gpt-4o-mini
305
+
306
+ # Optional: if your provider uses these
307
+ LLM_BASE_URL=
308
+ LLM_ORG=
309
+ LLM_PROJECT=
310
+
311
+ # --- Runtime ---
312
+ BUGMOLE_ENV=sandbox
313
+ BUGMOLE_WORKSPACE_ID=default
314
+ BUGMOLE_RUN_ID_PREFIX=local
315
+
316
+ # --- Artifacts ---
317
+ BUGMOLE_ARTIFACTS_DIR=./runs
318
+ BUGMOLE_SPEC_DIR=./spec
319
+
320
+ # --- MCP (optional) ---
321
+ MCP_ENABLED=true
322
+ MCP_HOST=127.0.0.1
323
+ MCP_PORT=3187
324
+
325
+ # --- Playwright ---
326
+ PLAYWRIGHT_HEADLESS=true
327
+ PLAYWRIGHT_BASE_URL=http://localhost:3000
328
+
329
+ # --- Cursor Driver (optional) ---
330
+ # Project directory to open in Cursor before testing
331
+ # If specified, will open a new Cursor window in this directory
332
+ CURSOR_PROJECT_DIRECTORY=
333
+ `;
334
+
335
+ const envPath = ".env.example";
336
+ if (fs.existsSync(envPath)) {
337
+ console.log(`[Bugmole MCP] ${envPath} already exists. Skipping generation.`);
338
+ return;
339
+ }
340
+
341
+ fs.writeFileSync(envPath, envExample, "utf-8");
342
+ console.log(`[Bugmole MCP] Generated ${envPath}`);
343
+ console.log("[Bugmole MCP] Copy it to .env and fill in your LLM_API_KEY");
344
+ }
345
+
346
+ async function main() {
347
+ const args = parseArgs();
348
+ const positionalCommand = process.argv.slice(2).find((arg) => !arg.startsWith("--"));
349
+ if (positionalCommand === "init") args.init = true;
350
+ if (positionalCommand === "api-key") args.apikey = true;
351
+ if (positionalCommand === "artifacts") args.artifacts = true;
352
+ if (positionalCommand === "serve") args.serve = true;
353
+ if (positionalCommand === "reset") args.reset = true;
354
+ // quiet: dotenv 17 prints an "injected env" banner on every load, which
355
+ // would land in the middle of this CLI's own output.
356
+ dotenv.config({ path: preferExisting(".bugmole.env", ".valk.env"), quiet: true });
357
+ adoptLegacyEnv();
358
+ if (args.help === true) {
359
+ printHelp(positionalCommand === "init");
360
+ return;
361
+ }
362
+
363
+ if (args.apikey === true) {
364
+ if (process.argv.includes("create") === false) {
365
+ throw new Error("Usage: bugmole api-key create --name <machine-name>");
366
+ }
367
+ const registryUrl =
368
+ process.env.BUGMOLE_REGISTRY_URL?.trim() || "https://api.bugmole.com";
369
+ const apiKey = requireEnv("BUGMOLE_API_KEY");
370
+ const { createRemoteApiKey } = await import("../src/registry/api-key-client.js");
371
+ const key = await createRemoteApiKey({
372
+ registryUrl,
373
+ aegisId: "api-key-client",
374
+ apiKey,
375
+ name: String(args.name ?? "bugmole-client"),
376
+ projectIds: args.projectids ? String(args.projectids).split(",").filter(Boolean) : undefined,
377
+ expiresAt: args.expiresat ? String(args.expiresat) : undefined,
378
+ });
379
+ console.log(`API key created: ${key.id}`);
380
+ console.log("Store this token securely. It will not be shown again:");
381
+ console.log(key.token);
382
+ return;
383
+ }
384
+
385
+ // Init must run from the caller's workspace, not the package directory.
386
+ if (resolveMode(args) === "init") {
387
+ const { runInitWizard } = await import("../src/runtime/init-wizard.js");
388
+ await runInitWizard({
389
+ nonInteractive: args.noninteractive === true,
390
+ force: args.force === true,
391
+ configPath: args.config,
392
+ workspaceId: args.workspaceid,
393
+ workspaceName: args.workspacename,
394
+ projectId: args.projectid,
395
+ projectName: args.projectname,
396
+ appId: args.appid,
397
+ appName: args.appname,
398
+ environment: args.environment,
399
+ specDir: args.specdir,
400
+ evidenceManifest: args.evidencemanifest,
401
+ evidenceScreenshots: args.evidencescreenshots,
402
+ artifactsDir: args.artifactsdir,
403
+ storageProvider: args.storageprovider,
404
+ bucket: args.bucket,
405
+ region: args.region,
406
+ endpoint: args.endpoint,
407
+ cloudProjectId: args.cloudprojectid,
408
+ accountId: args.accountid,
409
+ });
410
+ return;
411
+ }
412
+
413
+ // Keep relative config, spec, and artifact paths rooted at the caller's
414
+ // workspace. The init wizard writes project configuration there.
415
+ const workspaceRoot = path.resolve(process.env.BUGMOLE_WORKSPACE || process.cwd());
416
+ process.chdir(workspaceRoot);
417
+
418
+ const mode = resolveMode(args);
419
+ if (mode === "gateway") {
420
+ const { startServeGateway } = await import("../src/runtime/serve-gateway.js");
421
+ const gateway = await startServeGateway({
422
+ port: args.gatewayport ? Number(args.gatewayport) : undefined,
423
+ secret: typeof args.gatewaysecret === "string" ? args.gatewaysecret : undefined,
424
+ });
425
+ console.log(`[Bugmole] Serve gateway listening at ${gateway.origin}`);
426
+ console.log(`[Bugmole] Gateway secret: ${gateway.secret}`);
427
+ const stop = () => void gateway.close();
428
+ process.once("SIGINT", stop);
429
+ process.once("SIGTERM", stop);
430
+ await new Promise<void>(() => undefined);
431
+ return;
432
+ }
433
+ if (mode === "run-once") {
434
+ const { runOnce } = await import("../src/runtime/run-once.js");
435
+ // Exit outright: browsers a stopped run left behind must not keep the container alive.
436
+ process.exit(await runOnce());
437
+ }
438
+ const configPath =
439
+ args.config ??
440
+ process.env.BUGMOLE_CONFIG ??
441
+ [".bugmole/config.yaml", ".valkyrie/config.yaml", "bugmole.config.yaml", "valkyrie.config.yaml"]
442
+ .find((candidate) => fs.existsSync(path.join(workspaceRoot, candidate))) ??
443
+ "bugmole.config.yaml";
444
+
445
+ // Special mode: generate .env.example
446
+ if (mode === "init-env") {
447
+ generateEnvExample();
448
+ return;
449
+ }
450
+
451
+ // A web suite is deterministic: it needs a running app and flow files, not
452
+ // an LLM or a project configuration, so it runs in any workspace as-is.
453
+ if (mode === "suite") {
454
+ const { runWebSuite } = await import("../src/runtime/web-suite.js");
455
+ const { targetsFromFlags } = await import("../src/runtime/browser-matrix.js");
456
+ const baseUrl = typeof args.baseurl === "string" ? args.baseurl : process.env.BUGMOLE_BASE_URL;
457
+ if (!baseUrl) throw new Error("--base-url (or BUGMOLE_BASE_URL) is required for --mode suite");
458
+ const report = await runWebSuite({
459
+ flowsDir: path.resolve(workspaceRoot, typeof args.flows === "string" ? args.flows : ".maestro"),
460
+ baseUrl,
461
+ artifactsDir: path.resolve(workspaceRoot, typeof args.artifactsdir === "string" ? args.artifactsdir : preferExisting(path.join(workspaceRoot, "qa/bugmole-runs"), path.join(workspaceRoot, "qa/valkyrie-runs"))),
462
+ projectId: typeof args.project === "string" ? args.project : path.basename(workspaceRoot),
463
+ headless: args.headed !== true,
464
+ timeoutMs: typeof args.timeout === "number" ? args.timeout : undefined,
465
+ grep: typeof args.grep === "string" ? args.grep : undefined,
466
+ shareSession: args.sharesession === true,
467
+ targets: targetsFromFlags(args.browsers, args.devices),
468
+ });
469
+ const { total, passed, failed, blocked } = report.totals;
470
+ console.log(`[Bugmole] Suite ${passed}/${total} passed (${failed} failed, ${blocked} blocked). Report: ${report.reportPath}`);
471
+ if (failed > 0 || blocked > 0) process.exitCode = 1;
472
+ return;
473
+ }
474
+
475
+ const cfg = loadConfig(configPath);
476
+
477
+ if (mode === "reset") {
478
+ const { confirmProjectReset, resetProjectData } = await import("../src/runtime/reset.js");
479
+ const projectId = cfg.project?.id ?? "default";
480
+ if (args.yes !== true && !(await confirmProjectReset(projectId))) {
481
+ console.log("[Bugmole] Reset cancelled.");
482
+ return;
483
+ }
484
+ await resetProjectData(cfg);
485
+ console.log(`[Bugmole] Reset execution data for project ${projectId}.`);
486
+ return;
487
+ }
488
+
489
+ // Serve only needs the local runtime/storage and registry settings. Full
490
+ // execution validation belongs to plan/run modes, which require LLM and
491
+ // browser configuration that an idle worker does not use.
492
+ if (mode !== "serve" && mode !== "artifacts" && mode !== "explore") {
493
+ const { validateConfigOrThrow } = await import("../src/runtime/config-validate.js");
494
+ validateConfigOrThrow(cfg);
495
+ }
496
+
497
+ // Local storage needs a directory; remote providers create objects lazily.
498
+ if (!cfg.storage || cfg.storage.provider === "local") {
499
+ fs.mkdirSync(cfg.runtime.artifacts_dir, { recursive: true });
500
+ fs.mkdirSync(resolveLocalStorageDirectory(cfg), { recursive: true });
501
+ }
502
+ fs.mkdirSync(cfg.runtime.spec_dir, { recursive: true });
503
+
504
+ // Dispatch modes
505
+ switch (mode) {
506
+ case "serve": {
507
+ const remoteStorage = Boolean(cfg.storage?.provider && cfg.storage.provider !== "local");
508
+ let closeBridge: () => void = () => {};
509
+ if (remoteStorage) {
510
+ // A cloud worker writes evidence straight to remote storage. The loopback bridge only serves
511
+ // a local artifacts directory to a dashboard on the same machine, so it has nothing to do here.
512
+ console.log(`[Bugmole] Remote storage (${cfg.storage.provider}): local artifact bridge and dashboard handoff are off`);
513
+ } else {
514
+ const dashboardUrl = resolveDashboardUrl(args);
515
+ const { startLocalArtifactServer } = await import("../src/storage/local-artifact-server.js");
516
+ const bridge = await startLocalArtifactServer({
517
+ directory: resolveLocalStorageDirectory(cfg),
518
+ projectId: cfg.project?.id ?? "default",
519
+ aegisEmail: process.env.BUGMOLE_AEGIS_EMAIL?.trim(),
520
+ port: args.artifactport ? Number(args.artifactport) : undefined,
521
+ // --dashboard-url/BUGMOLE_DASHBOARD_URL previously only changed the
522
+ // printed handoff link; the bridge's CORS allowlist stayed the
523
+ // production origin regardless, so any non-default dashboard (e.g.
524
+ // local dev) was silently blocked by CORS the moment it tried to
525
+ // read artifacts. Default the allowlist to the same origin the
526
+ // handoff link actually points at.
527
+ dashboardOrigin: resolveDashboardOrigin(args, dashboardUrl),
528
+ service: "serve",
529
+ });
530
+ const fragment = new URLSearchParams({
531
+ "local-artifact-project": cfg.project?.id ?? "default",
532
+ "local-artifact-origin": bridge.origin,
533
+ "local-artifact-token": bridge.token,
534
+ });
535
+ const dashboardHandoff = new URL(dashboardUrl);
536
+ dashboardHandoff.searchParams.set("project", cfg.project?.id ?? "default");
537
+ dashboardHandoff.searchParams.set("deviceId", resolveDeviceId());
538
+ if (process.env.BUGMOLE_AEGIS_EMAIL?.trim()) {
539
+ dashboardHandoff.searchParams.set("email", process.env.BUGMOLE_AEGIS_EMAIL.trim());
540
+ }
541
+ console.log(`[Bugmole] Local artifact bridge listening at ${bridge.origin}`);
542
+ console.log(`[Bugmole] MCP will start on the configured HTTP port`);
543
+ console.log(`[Bugmole] Capability expires at ${bridge.expiresAt}`);
544
+ if (!process.env.BUGMOLE_AEGIS_EMAIL?.trim()) {
545
+ console.warn("[Bugmole] No BUGMOLE_AEGIS_EMAIL found; enter your Aegis email on the login page to generate rid.");
546
+ }
547
+ console.log(
548
+ `[Bugmole] Dashboard handoff: ${buildDashboardHandoffUrl(dashboardHandoff.toString(), cfg.project?.id ?? "default", fragment)}`,
549
+ );
550
+ if (shouldOpenServeBrowser(args)) {
551
+ const { openBrowser } = await import("../src/runtime/init-wizard.js");
552
+ openBrowser(buildDashboardHandoffUrl(dashboardHandoff.toString(), cfg.project?.id ?? "default", fragment));
553
+ }
554
+ closeBridge = () => void bridge.close("shutdown");
555
+ }
556
+ let localRegistryStop = () => {};
557
+ if (resolveLocalRegistryEnabled(args)) {
558
+ const { startLocalRegistryStub } = await import("../src/runtime/local-registry-stub.js");
559
+ const localRegistry = await startLocalRegistryStub({
560
+ directory: resolveLocalStorageDirectory(cfg),
561
+ port: resolveLocalRegistryPort(args),
562
+ });
563
+ localRegistryStop = () => void localRegistry.close();
564
+ // Redirect this process's own registry calls (this worker's
565
+ // ControlPlaneClient, and any MCP-tool call this session makes that
566
+ // publishes a plan or journey revision) at the local stub instead of
567
+ // the real registry. Never overwrite an operator-supplied value —
568
+ // this only fills in what would otherwise leave the worker idle.
569
+ if (!process.env.BUGMOLE_REGISTRY_URL?.trim()) process.env.BUGMOLE_REGISTRY_URL = localRegistry.origin;
570
+ if (!process.env.BUGMOLE_API_KEY?.trim()) process.env.BUGMOLE_API_KEY = "local-registry-stub";
571
+ console.log(`[Bugmole] Local registry stub listening at ${localRegistry.origin}`);
572
+ console.log(
573
+ `[Bugmole] Set BUGMOLE_REGISTRY_URL=${localRegistry.origin} in the dashboard's own env ` +
574
+ `(apps/qa/.env.local) so its API routes write here instead of the real registry.`,
575
+ );
576
+ }
577
+ const stop = () => {
578
+ localRegistryStop();
579
+ closeBridge();
580
+ };
581
+ process.once("SIGINT", stop);
582
+ process.once("SIGTERM", stop);
583
+ cfg.mcp = { ...(cfg.mcp ?? {}), enabled: true };
584
+ const { startServeWorker } = await import("../src/runtime/serve-worker.js");
585
+ // The worker owns a long-lived registry connection. Start it alongside
586
+ // MCP so this process exposes the project-scoped tools as well.
587
+ void startServeWorker(cfg).catch((error) => {
588
+ console.error(`[Bugmole] Serve worker stopped: ${error instanceof Error ? error.message : error}`);
589
+ });
590
+ await startMcpServerForServe(cfg);
591
+ // Keep the serve command's main promise alive independently of the
592
+ // HTTP servers. This prevents a future MCP implementation from
593
+ // returning after startup and silently ending the project worker.
594
+ await new Promise<void>((resolve) => {
595
+ const keepAlive = () => resolve();
596
+ process.once("SIGINT", keepAlive);
597
+ process.once("SIGTERM", keepAlive);
598
+ });
599
+ break;
600
+ }
601
+ case "artifacts": {
602
+ if (cfg.storage?.provider && cfg.storage.provider !== "local") {
603
+ throw new Error("The local artifact bridge requires storage.provider=local");
604
+ }
605
+ const dashboardUrl = resolveDashboardUrl(args);
606
+ const { startLocalArtifactServer } = await import("../src/storage/local-artifact-server.js");
607
+ const bridge = await startLocalArtifactServer({
608
+ directory: resolveLocalStorageDirectory(cfg),
609
+ projectId: cfg.project?.id ?? "default",
610
+ aegisEmail: process.env.BUGMOLE_AEGIS_EMAIL?.trim(),
611
+ port: args.artifactport ? Number(args.artifactport) : undefined,
612
+ dashboardOrigin: resolveDashboardOrigin(args, dashboardUrl),
613
+ service: "artifacts",
614
+ });
615
+ const fragment = new URLSearchParams({
616
+ "local-artifact-project": cfg.project?.id ?? "default",
617
+ "local-artifact-origin": bridge.origin,
618
+ "local-artifact-token": bridge.token,
619
+ });
620
+ console.log(`[Bugmole] Local artifact bridge listening at ${bridge.origin}`);
621
+ console.log(`[Bugmole] Capability expires at ${bridge.expiresAt}`);
622
+ const dashboardHandoff = buildDashboardHandoffUrl(dashboardUrl, cfg.project?.id ?? "default", fragment);
623
+ console.log(`[Bugmole] Dashboard handoff: ${dashboardHandoff}`);
624
+ const { openBrowser } = await import("../src/runtime/init-wizard.js");
625
+ openBrowser(dashboardHandoff);
626
+ await new Promise<void>((resolve, reject) => {
627
+ const stop = () => void bridge.close().then(resolve, reject);
628
+ process.once("SIGINT", stop);
629
+ process.once("SIGTERM", stop);
630
+ });
631
+ break;
632
+ }
633
+ case "mcp": {
634
+ const { startMcpServer } = await import("../src/mcp/server.js");
635
+ await startMcpServer(cfg);
636
+ break;
637
+ }
638
+ default: {
639
+ const { runBugmole } = await import("../src/runtime/run.js");
640
+ const result = await runBugmole(cfg, { mode, args });
641
+ process.exitCode = runModeExitCode(mode, result);
642
+ break;
643
+ }
644
+ }
645
+ }
646
+
647
+ async function startMcpServerForServe(cfg: any): Promise<void> {
648
+ const { startMcpServer } = await import("../src/mcp/server.js");
649
+ await startMcpServer(cfg, { forceHttp: true });
650
+ }
651
+
652
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
653
+ main().catch((err) => {
654
+ console.error(`[Bugmole MCP] fatal: ${err?.message ?? err}`);
655
+ process.exit(1);
656
+ });
657
+ }