@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.
- package/.bugmole.env.example +20 -0
- package/LICENSE +7 -0
- package/README.md +293 -0
- package/TESTING.md +117 -0
- package/bugmole.config.yaml +39 -0
- package/package.json +85 -0
- package/scripts/billing/paypal-setup.mjs +121 -0
- package/scripts/bugmole-continue.ts +318 -0
- package/scripts/bugmole-init.ts +188 -0
- package/scripts/bugmole.cjs +17 -0
- package/scripts/bugmole.test.ts +344 -0
- package/scripts/bugmole.ts +657 -0
- package/scripts/ensure-maestro.cjs +79 -0
- package/scripts/ios-tunnel-keeper.sh +45 -0
- package/scripts/ios-wda-keeper.sh +66 -0
- package/scripts/sync-plan-catalog.d.mts +3 -0
- package/scripts/sync-plan-catalog.mjs +16 -0
- package/scripts/ui-parity-diff.py +65 -0
- package/scripts/ui-parity-requirements.txt +1 -0
- package/scripts/verify-manage-to-plans.mts +194 -0
- package/spec/app-ui-audit.schema.json +176 -0
- package/spec/blockers.yaml +79 -0
- package/spec/bugs.index.json +42 -0
- package/spec/design-dna.schema.json +38 -0
- package/spec/domain_rules.yaml +24 -0
- package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
- package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
- package/spec/journey_graph.yaml +126 -0
- package/spec/journeys.graph.json +2618 -0
- package/spec/plans/dashboard-smoke.flow.yaml +20 -0
- package/spec/plans/example.flow.yaml +99 -0
- package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
- package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
- package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
- package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
- package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
- package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
- package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
- package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
- package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
- package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
- package/spec/plans/login.flow.yaml +22 -0
- package/spec/plans/owner-operations.flow.yaml +20 -0
- package/spec/project_config.yaml +55 -0
- package/spec/roles.yaml +30 -0
- package/spec/schema.md +394 -0
- package/spec/test-case-results.schema.json +62 -0
- package/spec/test-cases.schema.json +85 -0
- package/spec/ui-parity-audit.schema.json +194 -0
- package/spec/ui-reverse-engineering.schema.json +94 -0
- package/src/billing/plan-catalog.test.ts +46 -0
- package/src/billing/plan-catalog.ts +199 -0
- package/src/integrations/aws-sigv4.test.ts +42 -0
- package/src/integrations/aws-sigv4.ts +72 -0
- package/src/integrations/device-farm.ts +155 -0
- package/src/integrations/github-app.test.ts +57 -0
- package/src/integrations/github-app.ts +143 -0
- package/src/integrations/gitlab.ts +81 -0
- package/src/integrations/temp-email.test.ts +123 -0
- package/src/integrations/temp-email.ts +175 -0
- package/src/integrations/testflight-feedback.test.ts +51 -0
- package/src/integrations/testflight-feedback.ts +173 -0
- package/src/integrations/webdriver-client.ts +131 -0
- package/src/mcp/server.test.ts +1220 -0
- package/src/mcp/server.ts +3064 -0
- package/src/mcp/write-test-cases.test.ts +287 -0
- package/src/registry/api-key-client.ts +39 -0
- package/src/registry/control-plane-client.ts +212 -0
- package/src/registry/migrations/0001_registry.sql +47 -0
- package/src/registry/migrations/0002_device_authorizations.sql +23 -0
- package/src/registry/migrations/0003_project_environments.sql +25 -0
- package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
- package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
- package/src/registry/migrations/0006_workspaces.sql +36 -0
- package/src/registry/migrations/0007_project_apps.sql +26 -0
- package/src/registry/migrations/0008_agent_tasks.sql +30 -0
- package/src/registry/migrations/0009_journey_revisions.sql +17 -0
- package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
- package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
- package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
- package/src/registry/migrations/0013_device_sessions.sql +22 -0
- package/src/registry/migrations/0014_agent_task_step.sql +1 -0
- package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
- package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
- package/src/registry/migrations/0017_run_targets.sql +7 -0
- package/src/registry/migrations/0018_run_fix_from.sql +4 -0
- package/src/registry/migrations/0019_workspace_flags.sql +9 -0
- package/src/registry/migrations/0020_orgs.sql +40 -0
- package/src/registry/migrations/0021_billing_core.sql +58 -0
- package/src/registry/migrations/0022_cloud_runners.sql +19 -0
- package/src/registry/migrations/0023_signup.sql +4 -0
- package/src/registry/migrations/0024_billing.sql +67 -0
- package/src/registry/migrations/0025_notifications.sql +47 -0
- package/src/registry/migrations/0026_repo_bindings.sql +28 -0
- package/src/registry/migrations/0027_feedback.sql +29 -0
- package/src/registry/migrations/0028_devices.sql +48 -0
- package/src/registry/migrations/0029_sso.sql +31 -0
- package/src/registry/migrations/0030_workspace_domains.sql +18 -0
- package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
- package/src/registry/migrations/0032_personas.sql +15 -0
- package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
- package/src/registry/task-scheduling.test.ts +100 -0
- package/src/registry/task-scheduling.ts +80 -0
- package/src/registry-worker/ai/platform-model.ts +77 -0
- package/src/registry-worker/ai/routes.test.ts +88 -0
- package/src/registry-worker/ai/routes.ts +90 -0
- package/src/registry-worker/artifacts.test.ts +98 -0
- package/src/registry-worker/artifacts.ts +85 -0
- package/src/registry-worker/billing/billing-core.test.ts +175 -0
- package/src/registry-worker/billing/checkout-routes.ts +209 -0
- package/src/registry-worker/billing/enforcement.ts +69 -0
- package/src/registry-worker/billing/entitlements.ts +108 -0
- package/src/registry-worker/billing/ledger.ts +186 -0
- package/src/registry-worker/billing/paypal/api.ts +259 -0
- package/src/registry-worker/billing/paypal/client.ts +91 -0
- package/src/registry-worker/billing/paypal/provider.ts +143 -0
- package/src/registry-worker/billing/paypal.test.ts +466 -0
- package/src/registry-worker/billing/provider.ts +114 -0
- package/src/registry-worker/billing/routes.ts +66 -0
- package/src/registry-worker/billing/subscriptions.ts +780 -0
- package/src/registry-worker/billing/thresholds.ts +107 -0
- package/src/registry-worker/core.ts +308 -0
- package/src/registry-worker/devices/devices.test.ts +185 -0
- package/src/registry-worker/devices/policy.ts +71 -0
- package/src/registry-worker/devices/routes.ts +453 -0
- package/src/registry-worker/domains/domains.test.ts +210 -0
- package/src/registry-worker/domains/routes.ts +139 -0
- package/src/registry-worker/domains.ts +88 -0
- package/src/registry-worker/email/sender.ts +75 -0
- package/src/registry-worker/env.d.ts +14716 -0
- package/src/registry-worker/features.ts +20 -0
- package/src/registry-worker/feedback/feedback.test.ts +230 -0
- package/src/registry-worker/feedback/format.ts +148 -0
- package/src/registry-worker/feedback/routes.ts +386 -0
- package/src/registry-worker/flags.ts +39 -0
- package/src/registry-worker/github/checks.test.ts +177 -0
- package/src/registry-worker/github/checks.ts +374 -0
- package/src/registry-worker/gitlab/checks.test.ts +141 -0
- package/src/registry-worker/gitlab/checks.ts +349 -0
- package/src/registry-worker/hooks.ts +54 -0
- package/src/registry-worker/index.ts +2077 -0
- package/src/registry-worker/jobs/index.ts +29 -0
- package/src/registry-worker/jobs/retention.ts +68 -0
- package/src/registry-worker/mcp/mcp.test.ts +355 -0
- package/src/registry-worker/mcp/routes.ts +215 -0
- package/src/registry-worker/mcp/token.ts +126 -0
- package/src/registry-worker/mcp/tools.ts +563 -0
- package/src/registry-worker/notifications/alerts.ts +212 -0
- package/src/registry-worker/notifications/notifications.test.ts +298 -0
- package/src/registry-worker/notifications/outbox.ts +83 -0
- package/src/registry-worker/notifications/routes.ts +280 -0
- package/src/registry-worker/notifications/secrets.ts +49 -0
- package/src/registry-worker/notifications/slack.ts +96 -0
- package/src/registry-worker/notifications/teams.ts +46 -0
- package/src/registry-worker/org/audit.ts +116 -0
- package/src/registry-worker/org/routes.test.ts +163 -0
- package/src/registry-worker/org/routes.ts +302 -0
- package/src/registry-worker/personas/personas.test.ts +78 -0
- package/src/registry-worker/personas/routes.ts +100 -0
- package/src/registry-worker/repo-triggers.ts +20 -0
- package/src/registry-worker/routes/index.ts +74 -0
- package/src/registry-worker/run-events.ts +24 -0
- package/src/registry-worker/runner/dispatch.ts +219 -0
- package/src/registry-worker/runner/jobs.ts +43 -0
- package/src/registry-worker/runner/metering.ts +82 -0
- package/src/registry-worker/runner/policy.ts +59 -0
- package/src/registry-worker/runner/routes.ts +171 -0
- package/src/registry-worker/runner/runner.test.ts +358 -0
- package/src/registry-worker/runner/tokens.ts +93 -0
- package/src/registry-worker/runs.test.ts +60 -0
- package/src/registry-worker/signup/policy.ts +57 -0
- package/src/registry-worker/signup/routes.ts +106 -0
- package/src/registry-worker/signup/signup.test.ts +81 -0
- package/src/registry-worker/sso/aegis.ts +141 -0
- package/src/registry-worker/sso/membership.ts +157 -0
- package/src/registry-worker/sso/routes.ts +458 -0
- package/src/registry-worker/sso/sso.test.ts +344 -0
- package/src/registry-worker/testing/d1-shim.ts +180 -0
- package/src/registry-worker/testing/harness.ts +137 -0
- package/src/runner-worker/index.ts +108 -0
- package/src/runtime/ai-analysis.ts +97 -0
- package/src/runtime/ai-exploration.test.ts +32 -0
- package/src/runtime/ai-exploration.ts +69 -0
- package/src/runtime/ai-repair.ts +74 -0
- package/src/runtime/ai-work.test.ts +99 -0
- package/src/runtime/android-screen-record.test.ts +75 -0
- package/src/runtime/android-screen-record.ts +192 -0
- package/src/runtime/app-understanding.test.ts +123 -0
- package/src/runtime/app-understanding.ts +201 -0
- package/src/runtime/appium-driver.test.ts +179 -0
- package/src/runtime/appium-driver.ts +295 -0
- package/src/runtime/blocker-resolution.test.ts +113 -0
- package/src/runtime/blocker-resolution.ts +111 -0
- package/src/runtime/browser-matrix.integration.test.ts +212 -0
- package/src/runtime/browser-matrix.test.ts +143 -0
- package/src/runtime/browser-matrix.ts +200 -0
- package/src/runtime/canonical-flow.test.ts +52 -0
- package/src/runtime/config-validate.ts +185 -0
- package/src/runtime/continuous-execution.ts +291 -0
- package/src/runtime/cursor-applescript.ts +573 -0
- package/src/runtime/cursor-cli-driver.test.ts +78 -0
- package/src/runtime/cursor-cli-driver.ts +156 -0
- package/src/runtime/cursor-driver-example.ts +117 -0
- package/src/runtime/cursor-driver-index.ts +65 -0
- package/src/runtime/cursor-driver-init.ts +277 -0
- package/src/runtime/cursor-driver-run.test.ts +15 -0
- package/src/runtime/cursor-driver-run.ts +323 -0
- package/src/runtime/cursor-driver.ts +332 -0
- package/src/runtime/cursor-llm-example.ts +90 -0
- package/src/runtime/cursor-llm.ts +206 -0
- package/src/runtime/cursor-mcp-monitor.ts +386 -0
- package/src/runtime/device-clouds/browserstack.ts +73 -0
- package/src/runtime/device-clouds/device-farm.ts +52 -0
- package/src/runtime/device-clouds/index.ts +92 -0
- package/src/runtime/device-clouds/kobiton.ts +70 -0
- package/src/runtime/device-clouds/targets.ts +44 -0
- package/src/runtime/device-clouds/types.ts +62 -0
- package/src/runtime/diff-proposal.ts +84 -0
- package/src/runtime/discovery-task.test.ts +29 -0
- package/src/runtime/discovery-task.ts +284 -0
- package/src/runtime/driver-recovery.ts +69 -0
- package/src/runtime/driver.ts +79 -0
- package/src/runtime/environment.test.ts +104 -0
- package/src/runtime/environment.ts +137 -0
- package/src/runtime/executor.test.ts +509 -0
- package/src/runtime/executor.ts +921 -0
- package/src/runtime/explorer.test.ts +101 -0
- package/src/runtime/explorer.ts +1013 -0
- package/src/runtime/failure-analysis.test.ts +111 -0
- package/src/runtime/failure-analysis.ts +272 -0
- package/src/runtime/fixtures/fake-maestro.sh +36 -0
- package/src/runtime/flow-language.test.ts +268 -0
- package/src/runtime/flow-language.ts +414 -0
- package/src/runtime/init-wizard.ts +354 -0
- package/src/runtime/ios-screen-record.test.ts +68 -0
- package/src/runtime/ios-screen-record.ts +155 -0
- package/src/runtime/journey-editor.ts +452 -0
- package/src/runtime/journey-evidence.test.ts +161 -0
- package/src/runtime/journey-evidence.ts +180 -0
- package/src/runtime/journey-graph.test.ts +257 -0
- package/src/runtime/journey-graph.ts +170 -0
- package/src/runtime/legacy-names.ts +32 -0
- package/src/runtime/llm-example.ts +105 -0
- package/src/runtime/llm.ts +527 -0
- package/src/runtime/local-browser.test.ts +45 -0
- package/src/runtime/local-browser.ts +48 -0
- package/src/runtime/local-registry-stub.test.ts +325 -0
- package/src/runtime/local-registry-stub.ts +803 -0
- package/src/runtime/maestro-driver.test.ts +84 -0
- package/src/runtime/maestro-driver.ts +209 -0
- package/src/runtime/mole-voice.ts +21 -0
- package/src/runtime/nav-crawl.test.ts +100 -0
- package/src/runtime/nav-crawl.ts +153 -0
- package/src/runtime/pipeline.test.ts +405 -0
- package/src/runtime/pipeline.ts +833 -0
- package/src/runtime/planner.test.ts +37 -0
- package/src/runtime/planner.ts +274 -0
- package/src/runtime/platform-ai.ts +76 -0
- package/src/runtime/playwright-driver.test.ts +93 -0
- package/src/runtime/playwright-driver.ts +620 -0
- package/src/runtime/project-spec.ts +140 -0
- package/src/runtime/record-run-verdicts.ts +68 -0
- package/src/runtime/reporter.test.ts +56 -0
- package/src/runtime/reporter.ts +158 -0
- package/src/runtime/reset.test.ts +44 -0
- package/src/runtime/reset.ts +61 -0
- package/src/runtime/reviewer.test.ts +73 -0
- package/src/runtime/reviewer.ts +158 -0
- package/src/runtime/run-job.ts +136 -0
- package/src/runtime/run-once.test.ts +207 -0
- package/src/runtime/run-once.ts +168 -0
- package/src/runtime/run.ts +132 -0
- package/src/runtime/screen-recording.ts +34 -0
- package/src/runtime/serve-gateway.test.ts +74 -0
- package/src/runtime/serve-gateway.ts +164 -0
- package/src/runtime/serve-worker.test.ts +23 -0
- package/src/runtime/serve-worker.ts +278 -0
- package/src/runtime/site-discovery.test.ts +168 -0
- package/src/runtime/site-discovery.ts +308 -0
- package/src/runtime/target-runner.ts +144 -0
- package/src/runtime/test-case-verdicts.test.ts +94 -0
- package/src/runtime/test-case-verdicts.ts +120 -0
- package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
- package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
- package/src/runtime/ui-reverse-engineering/types.ts +197 -0
- package/src/runtime/web-suite.test.ts +97 -0
- package/src/runtime/web-suite.ts +176 -0
- package/src/storage/create-object-store.ts +144 -0
- package/src/storage/keys.ts +34 -0
- package/src/storage/local-artifact-server.test.ts +314 -0
- package/src/storage/local-artifact-server.ts +357 -0
- package/src/storage/object-store.test.ts +28 -0
- package/src/storage/object-store.ts +101 -0
- package/src/storage/registry-object-store.ts +88 -0
- package/src/storage/remote-object-store.ts +104 -0
- package/src/storage/storage-directory.test.ts +43 -0
- package/src/storage/storage-directory.ts +24 -0
- 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
|
+
}
|