@bugmole/cli 0.5.0 → 0.6.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/package.json +5 -2
- package/scripts/bugmole-admin.mjs +194 -0
- package/scripts/bugmole-admin.test.mjs +62 -0
- package/scripts/bugmole.test.ts +94 -2
- package/scripts/bugmole.ts +233 -46
- package/scripts/sync-byok.d.mts +2 -0
- package/scripts/sync-byok.mjs +18 -0
- package/scripts/sync-plan-catalog.d.mts +3 -0
- package/scripts/sync-plan-catalog.mjs +16 -6
- package/spec/domain_rules.yaml +139 -0
- package/spec/roles.yaml +20 -0
- package/spec/test-case-results.schema.json +20 -4
- package/spec/test-cases.schema.json +131 -16
- package/src/billing/plan-catalog.test.ts +82 -2
- package/src/billing/plan-catalog.ts +212 -9
- package/src/integrations/jira.ts +264 -0
- package/src/mcp/roles-and-review.test.ts +111 -0
- package/src/mcp/server.ts +358 -22
- package/src/mcp/write-test-cases.test.ts +68 -0
- package/src/registry/control-plane-client.ts +121 -6
- package/src/registry/migrations/0036_task_approval.sql +11 -0
- package/src/registry/migrations/0037_spec_proposals.sql +27 -0
- package/src/registry/migrations/0038_local_worker_seen.sql +5 -0
- package/src/registry/migrations/0039_project_secrets.sql +37 -0
- package/src/registry/migrations/0040_roles_and_task_review.sql +31 -0
- package/src/registry/migrations/0041_jira_integration.sql +75 -0
- package/src/registry/migrations/0042_subscription_gaps.sql +10 -0
- package/src/registry/migrations/0043_workspace_feature_overrides.sql +17 -0
- package/src/registry/migrations/0044_project_identity.sql +31 -0
- package/src/registry/project-identity.test.ts +99 -0
- package/src/registry/project-identity.ts +206 -0
- package/src/registry/roles.test.ts +66 -0
- package/src/registry/roles.ts +201 -0
- package/src/registry/task-scheduling.test.ts +94 -0
- package/src/registry/task-scheduling.ts +199 -2
- package/src/registry/test-case-revisions.test.ts +57 -0
- package/src/registry/test-case-revisions.ts +132 -0
- package/src/registry-worker/ai/routes.ts +3 -3
- package/src/registry-worker/artifacts.ts +23 -4
- package/src/registry-worker/billing/billing-core.test.ts +1 -1
- package/src/registry-worker/billing/checkout-routes.ts +53 -4
- package/src/registry-worker/billing/enforcement.ts +67 -12
- package/src/registry-worker/billing/paypal/api.ts +14 -0
- package/src/registry-worker/billing/paypal/client.ts +9 -0
- package/src/registry-worker/billing/paypal/provider.ts +10 -1
- package/src/registry-worker/billing/paypal.test.ts +108 -1
- package/src/registry-worker/billing/plan-gaps.test.ts +216 -0
- package/src/registry-worker/billing/provider.ts +8 -0
- package/src/registry-worker/billing/routes.ts +2 -1
- package/src/registry-worker/billing/subscriptions.ts +114 -4
- package/src/registry-worker/core.ts +22 -0
- package/src/registry-worker/devices/policy.ts +4 -5
- package/src/registry-worker/devices/routes.ts +8 -8
- package/src/registry-worker/feature-access.ts +165 -0
- package/src/registry-worker/feature-flags/admin.ts +300 -0
- package/src/registry-worker/feature-flags/feature-flags.test.ts +270 -0
- package/src/registry-worker/feature-flags/routes.ts +102 -0
- package/src/registry-worker/features.ts +9 -0
- package/src/registry-worker/feedback/routes.ts +2 -2
- package/src/registry-worker/flags.ts +80 -18
- package/src/registry-worker/github/checks.ts +4 -4
- package/src/registry-worker/hooks.ts +9 -0
- package/src/registry-worker/identity/oidc.test.ts +103 -0
- package/src/registry-worker/identity/oidc.ts +176 -0
- package/src/registry-worker/index.ts +477 -174
- package/src/registry-worker/jira/connection.ts +111 -0
- package/src/registry-worker/jira/jira.test.ts +404 -0
- package/src/registry-worker/jira/routes.ts +432 -0
- package/src/registry-worker/jira/workflow.ts +577 -0
- package/src/registry-worker/jobs/retention.ts +21 -5
- package/src/registry-worker/mcp/tools.ts +2 -1
- package/src/registry-worker/notifications/alerts.ts +4 -4
- package/src/registry-worker/notifications/notifications.test.ts +11 -0
- package/src/registry-worker/notifications/routes.ts +9 -10
- package/src/registry-worker/notifications/teams.ts +6 -2
- package/src/registry-worker/org/routes.ts +4 -4
- package/src/registry-worker/projects/identity.ts +194 -0
- package/src/registry-worker/projects/inactivity.ts +162 -0
- package/src/registry-worker/projects/projects.test.ts +283 -0
- package/src/registry-worker/proposals/proposals.test.ts +80 -0
- package/src/registry-worker/proposals/routes.ts +183 -0
- package/src/registry-worker/roles/roles.test.ts +84 -0
- package/src/registry-worker/roles/routes.ts +141 -0
- package/src/registry-worker/runner/dispatch.ts +1 -0
- package/src/registry-worker/runner/routes.ts +22 -5
- package/src/registry-worker/runner/runner.test.ts +21 -0
- package/src/registry-worker/runner/tokens.ts +8 -0
- package/src/registry-worker/secrets/crypto.ts +135 -0
- package/src/registry-worker/secrets/routes.ts +296 -0
- package/src/registry-worker/secrets/secrets.test.ts +237 -0
- package/src/registry-worker/signup/policy.ts +2 -2
- package/src/registry-worker/signup/routes.ts +12 -3
- package/src/registry-worker/sso/membership.ts +4 -2
- package/src/registry-worker/sso/routes.ts +24 -8
- package/src/registry-worker/sso/sso.test.ts +3 -2
- package/src/registry-worker/task-approval.test.ts +67 -0
- package/src/registry-worker/task-resume.test.ts +196 -0
- package/src/registry-worker/task-review.test.ts +121 -0
- package/src/runtime/appium-driver.ts +1 -1
- package/src/runtime/apply-proposals.test.ts +74 -0
- package/src/runtime/apply-proposals.ts +41 -0
- package/src/runtime/cloud-secrets.test.ts +201 -0
- package/src/runtime/cloud-secrets.ts +210 -0
- package/src/runtime/config-validate.ts +12 -9
- package/src/runtime/cursor-driver-run.ts +14 -3
- package/src/runtime/discovery-task.test.ts +26 -1
- package/src/runtime/discovery-task.ts +67 -6
- package/src/runtime/executor.ts +18 -10
- package/src/runtime/explorer.test.ts +32 -0
- package/src/runtime/explorer.ts +48 -0
- package/src/runtime/flow-language.ts +22 -8
- package/src/runtime/init-wizard.ts +112 -6
- package/src/runtime/journey-editor.ts +39 -2
- package/src/runtime/journey-graph.test.ts +18 -0
- package/src/runtime/local-registry-stub.test.ts +64 -0
- package/src/runtime/local-registry-stub.ts +255 -7
- package/src/runtime/local-vault.ts +65 -0
- package/src/runtime/pipeline.test.ts +23 -0
- package/src/runtime/pipeline.ts +61 -20
- package/src/runtime/planner.test.ts +24 -1
- package/src/runtime/planner.ts +88 -23
- package/src/runtime/playwright-driver.test.ts +21 -2
- package/src/runtime/playwright-driver.ts +49 -7
- package/src/runtime/project-identity.test.ts +161 -0
- package/src/runtime/project-identity.ts +232 -0
- package/src/runtime/project-roles.test.ts +107 -0
- package/src/runtime/project-roles.ts +158 -0
- package/src/runtime/propose-cli.ts +66 -0
- package/src/runtime/record-run-verdicts.test.ts +58 -0
- package/src/runtime/record-run-verdicts.ts +38 -4
- package/src/runtime/reporter.ts +1 -1
- package/src/runtime/reset.ts +2 -0
- package/src/runtime/roles-cli.test.ts +47 -0
- package/src/runtime/roles-cli.ts +60 -0
- package/src/runtime/run-once.ts +68 -5
- package/src/runtime/scenario-matrix.test.ts +41 -0
- package/src/runtime/scenario-matrix.ts +98 -0
- package/src/runtime/secret-driver.ts +77 -0
- package/src/runtime/secret-redaction.ts +69 -0
- package/src/runtime/secret-sources.test.ts +442 -0
- package/src/runtime/secret-sources.ts +176 -0
- package/src/runtime/secrets-cli.ts +146 -0
- package/src/runtime/serve-worker.ts +64 -6
- package/src/runtime/site-discovery.test.ts +47 -4
- package/src/runtime/site-discovery.ts +79 -9
- package/src/runtime/vault-federation.test.ts +37 -0
- package/src/runtime/vault-federation.ts +128 -0
- package/src/runtime/web-suite.ts +5 -2
- package/src/storage/create-object-store.ts +5 -1
- package/src/storage/object-store.ts +12 -1
- package/src/storage/test-case-results.test.ts +64 -0
- package/src/storage/test-case-results.ts +49 -0
- package/src/vendor/byok.ts +371 -0
package/scripts/bugmole.ts
CHANGED
|
@@ -9,6 +9,7 @@ import dotenv from "dotenv";
|
|
|
9
9
|
import YAML from "yaml";
|
|
10
10
|
import { resolveLocalStorageDirectory } from "../src/storage/storage-directory.js";
|
|
11
11
|
import { adoptLegacyEnv, preferExisting } from "../src/runtime/legacy-names.js";
|
|
12
|
+
import { approvalModeOf, workerApprovalMode, type ApprovalMode } from "../src/registry/task-scheduling.js";
|
|
12
13
|
|
|
13
14
|
type Mode =
|
|
14
15
|
| "api-key"
|
|
@@ -25,10 +26,13 @@ type Mode =
|
|
|
25
26
|
| "serve"
|
|
26
27
|
| "gateway"
|
|
27
28
|
| "run-once"
|
|
29
|
+
| "propose"
|
|
30
|
+
| "roles"
|
|
28
31
|
| "reset"
|
|
29
32
|
| "cursor-driver"
|
|
30
33
|
| "suite"
|
|
31
|
-
| "init-env"
|
|
34
|
+
| "init-env"
|
|
35
|
+
| "secrets";
|
|
32
36
|
|
|
33
37
|
function readArg(name: string, argv = process.argv): string | undefined {
|
|
34
38
|
const i = argv.indexOf(name);
|
|
@@ -45,23 +49,42 @@ export function parseArgs(argv = process.argv): Record<string, any> {
|
|
|
45
49
|
if (positionalCommand === "gateway") args.gateway = true;
|
|
46
50
|
if (positionalCommand === "run-once") args.runonce = true;
|
|
47
51
|
if (positionalCommand === "reset") args.reset = true;
|
|
52
|
+
if (positionalCommand === "roles") args.roles = true;
|
|
53
|
+
if (positionalCommand === "secrets") args.secrets = true;
|
|
54
|
+
if (positionalCommand === "propose") {
|
|
55
|
+
args.propose = true;
|
|
56
|
+
const file = argv.slice(argv.indexOf("propose") + 1).find((arg) => !arg.startsWith("--"));
|
|
57
|
+
if (file) args.proposefile = file;
|
|
58
|
+
}
|
|
48
59
|
for (let i = 0; i < argv.length; i++) {
|
|
49
60
|
const arg = argv[i];
|
|
50
61
|
if (arg.startsWith("--")) {
|
|
51
|
-
const
|
|
62
|
+
const name = arg.slice(2);
|
|
63
|
+
const key = name.replace(/-/g, "");
|
|
52
64
|
const next = argv[i + 1];
|
|
53
65
|
const value = next && !next.startsWith("--") ? next : true;
|
|
54
66
|
if (value !== true) i++;
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
else
|
|
67
|
+
// Only counts and durations become numbers: `--grep 404` or
|
|
68
|
+
// `--project 2024` are names, and must stay strings.
|
|
69
|
+
let parsed: unknown;
|
|
70
|
+
if (value === true || value === "true") parsed = true;
|
|
71
|
+
else if (value === "false") parsed = false;
|
|
72
|
+
else if (NUMERIC_FLAGS.has(key.toLowerCase()) && value.trim() !== "" && !isNaN(Number(value))) parsed = Number(value);
|
|
73
|
+
else parsed = value;
|
|
74
|
+
args[key] = parsed;
|
|
75
|
+
// Modes read either spelling: --file-path is args.filepath and args.filePath.
|
|
76
|
+
const camel = name.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
|
77
|
+
if (camel !== key) args[camel] = parsed;
|
|
60
78
|
}
|
|
61
79
|
}
|
|
62
80
|
return args;
|
|
63
81
|
}
|
|
64
82
|
|
|
83
|
+
const NUMERIC_FLAGS = new Set([
|
|
84
|
+
"timeout", "maxpages", "maxdepth", "localregistryport", "artifactport", "gatewayport", "pollinterval",
|
|
85
|
+
"nudgeinterval", "nudgemaxnudges", "nudgeinactivitythreshold", "nudgeprogressreporttimeout",
|
|
86
|
+
]);
|
|
87
|
+
|
|
65
88
|
export function printHelp(init = false): void {
|
|
66
89
|
if (init) {
|
|
67
90
|
console.log(`Usage: bugmole init [options]
|
|
@@ -71,40 +94,87 @@ Initialize .bugmole/config.yaml and .bugmole.env without reading credentials.
|
|
|
71
94
|
Options:
|
|
72
95
|
--non-interactive Use defaults and never prompt
|
|
73
96
|
--force Overwrite an existing configuration
|
|
74
|
-
--storage-provider <name> local, gcs, s3, or r2
|
|
97
|
+
--storage-provider <name> local, bugmole, gcs, s3, or r2
|
|
98
|
+
--bucket <name> Bucket for gcs, s3 or r2
|
|
99
|
+
--region <region> S3 region
|
|
100
|
+
--account-id <id> R2 account ID
|
|
101
|
+
--cloud-project-id <id> Google Cloud project for gcs
|
|
102
|
+
--endpoint <url> Custom storage endpoint
|
|
103
|
+
--project-id <id> Project identifier (default: the folder name)
|
|
104
|
+
--project-name <name> Project display name
|
|
105
|
+
--environment <name> Environment name (default: local)
|
|
106
|
+
--spec-dir <folder> Where flows, plans and roles live (default: ./spec)
|
|
75
107
|
--workspace-id <id> Workspace identifier
|
|
76
108
|
--workspace-name <name> Workspace display name
|
|
77
109
|
--app-id <id> Default app identifier
|
|
78
110
|
--app-name <name> Default app display name
|
|
111
|
+
--app-path <folder> The app's folder in a repository with several apps
|
|
112
|
+
--app-url <url> The app's URL (a public one identifies the project by domain)
|
|
79
113
|
--config <path> Configuration output path
|
|
80
|
-
--help Show this help
|
|
114
|
+
--help Show this help
|
|
115
|
+
|
|
116
|
+
Bugmole tests web apps and mobile apps (Android and iOS). One app is one
|
|
117
|
+
project: a repository's git remote plus the app's folder identify it, so
|
|
118
|
+
teammates who clone the same repository join the same project.`);
|
|
81
119
|
return;
|
|
82
120
|
}
|
|
83
121
|
console.log(`Usage: bugmole [command] [options]
|
|
84
122
|
|
|
85
123
|
Commands:
|
|
86
124
|
init Initialize a project configuration
|
|
87
|
-
api-key create Generate a one-time machine API key
|
|
125
|
+
api-key create Generate a one-time machine API key (needs a key with keys:write;
|
|
126
|
+
the dashboard's API keys page works without one)
|
|
127
|
+
--name <name> What the key is for
|
|
128
|
+
--project-ids <ids> Comma-separated projects it may use
|
|
88
129
|
--mcp Start the MCP server
|
|
89
130
|
artifacts serve Serve local artifacts to the dashboard over loopback
|
|
90
131
|
--artifact-port <port> Loopback port (default: 3190)
|
|
91
132
|
--dashboard-origin <url> Allowed dashboard origin
|
|
133
|
+
propose <file.json> Propose test-case changes (add, change, retire) for review;
|
|
134
|
+
once accepted each becomes a new revision and the old one obsolete
|
|
135
|
+
secrets List the {{secret.NAME}}s your flows use and where each comes from
|
|
136
|
+
secrets set <NAME> Keep a value in this machine's secure store (typed hidden, or piped on stdin)
|
|
137
|
+
secrets delete <NAME> Remove it. Or map names to your vault (gcpsm://, awssm://, keychain://,
|
|
138
|
+
env://) under secrets: in the config
|
|
139
|
+
roles List the project's roles: confirmed ones are used for test cases and runs
|
|
140
|
+
roles suggest <name> Suggest a role for a person to confirm on the dashboard's Roles page
|
|
141
|
+
--evidence <text> What points at it (a sign-in wall, a gated route or menu)
|
|
142
|
+
--description <text> Who this is and what they can do
|
|
143
|
+
--from-evidence Suggest every role the journey graph, blockers and roles.yaml imply
|
|
92
144
|
serve Start the local artifact bridge and HTTP MCP server
|
|
93
145
|
--no-open Print the dashboard handoff without opening a browser
|
|
146
|
+
--approval <mode> manual | auto | extreme: only take tasks this mode would allow.
|
|
147
|
+
Stricter than the project's mode, never looser (sets BUGMOLE_APPROVAL).
|
|
94
148
|
--dashboard-url <url> Dashboard URL for the handoff (overrides BUGMOLE_DASHBOARD_URL)
|
|
95
149
|
--local-registry Stand in for the real registry locally (overrides BUGMOLE_LOCAL_REGISTRY)
|
|
96
150
|
so queueing tasks/runs and this worker executing them works without
|
|
97
|
-
|
|
151
|
+
signing in or a BUGMOLE_API_KEY scoped to this project.
|
|
98
152
|
--local-registry-port Local registry stub port (default: 4600, or BUGMOLE_LOCAL_REGISTRY_PORT)
|
|
99
153
|
gateway Multiplex project serve workers over loopback
|
|
100
154
|
--gateway-port <port> Loopback port (default: 3191)
|
|
101
155
|
--gateway-secret <value> Shared secret for worker registration
|
|
102
156
|
reset Delete current-project execution data after confirmation
|
|
157
|
+
--yes Skip the typed confirmation (scripts and CI)
|
|
103
158
|
run-once Execute one Bugmole Cloud run and exit (used inside cloud runners;
|
|
104
159
|
reads BUGMOLE_REGISTRY_URL, BUGMOLE_RUN_ID, BUGMOLE_RUN_TOKEN)
|
|
105
|
-
--mode <mode> explore, plan, run, report, review, propose-diff, or
|
|
160
|
+
--mode <mode> explore, plan, run, report, review, propose-diff, suite, or discover
|
|
161
|
+
--app-path <folder> The app's folder in a repository with several apps
|
|
162
|
+
--app-url <url> The app's URL
|
|
163
|
+
--timeout <ms> How long a flow may take
|
|
164
|
+
--mode explore Learn the app's screens from its source, or by crawling it
|
|
165
|
+
--base-url <url> The running app
|
|
166
|
+
--mode plan Write a runnable plan for a journey
|
|
167
|
+
--journey-id <id> The journey to plan
|
|
168
|
+
--actor <role> A confirmed role to plan it for
|
|
169
|
+
--mode discover Click through a running app and write a flow for each page and form
|
|
170
|
+
--base-url <url> The running app
|
|
171
|
+
--out <dir> Where to write the flows (required)
|
|
172
|
+
--sign-in <flow> Sign in with this flow first, to explore the signed-in app
|
|
173
|
+
--max-pages <n> Stop after this many pages
|
|
174
|
+
--max-depth <n> Follow links at most this many clicks from the start
|
|
175
|
+
--open-each-page Open every page directly as well as by clicking
|
|
106
176
|
--mode suite Run a directory of web flows against a running app
|
|
107
|
-
--flows <dir> Flow directory (default: .maestro)
|
|
177
|
+
--flows <dir> Flow directory (default: spec/flows, then flows, then .maestro)
|
|
108
178
|
--base-url <url> Application base URL (or BUGMOLE_BASE_URL)
|
|
109
179
|
--artifacts-dir <dir> Evidence directory (default: qa/bugmole-runs)
|
|
110
180
|
--project <id> Project id used in the evidence path
|
|
@@ -129,6 +199,9 @@ export function resolveMode(args: Record<string, any>): Mode {
|
|
|
129
199
|
if (args.gateway === true) return "gateway";
|
|
130
200
|
if (args.runonce === true) return "run-once";
|
|
131
201
|
if (args.reset === true) return "reset";
|
|
202
|
+
if (args.propose === true) return "propose";
|
|
203
|
+
if (args.roles === true) return "roles";
|
|
204
|
+
if (args.secrets === true) return "secrets";
|
|
132
205
|
if (args.artifacts === true) return "artifacts";
|
|
133
206
|
return (args.mode ?? "mcp") as Mode;
|
|
134
207
|
}
|
|
@@ -138,9 +211,10 @@ export function resolveMode(args: Record<string, any>): Mode {
|
|
|
138
211
|
// step needs it to fail the build, so `--mode run` is the one mode whose exit code
|
|
139
212
|
// reflects the result rather than just whether the process threw.
|
|
140
213
|
export function runModeExitCode(mode: string, result: unknown): number {
|
|
141
|
-
if (mode !== "run") return 0;
|
|
142
214
|
const status = result && typeof result === "object" ? (result as { status?: unknown }).status : undefined;
|
|
143
|
-
|
|
215
|
+
// A run passes only when it all passed; the other modes fail on an error.
|
|
216
|
+
if (mode === "run") return status === "success" ? 0 : 1;
|
|
217
|
+
return status === "error" ? 1 : 0;
|
|
144
218
|
}
|
|
145
219
|
|
|
146
220
|
export function shouldOpenServeBrowser(
|
|
@@ -210,6 +284,19 @@ export function resolveLocalRegistryEnabled(args: Record<string, any>): boolean
|
|
|
210
284
|
// every fetch with "bad port" — 4190 is the reserved ManageSieve port).
|
|
211
285
|
const DEFAULT_LOCAL_REGISTRY_PORT = 4600;
|
|
212
286
|
|
|
287
|
+
/**
|
|
288
|
+
* `--approval manual|auto|extreme` for `bugmole serve`. It becomes
|
|
289
|
+
* BUGMOLE_APPROVAL so every part of this process (worker, MCP, the coding
|
|
290
|
+
* agent driver) holds back the same tasks. Returns null for a bad value.
|
|
291
|
+
*/
|
|
292
|
+
export function applyApprovalFlag(args: Record<string, any>, env: Record<string, string | undefined> = process.env): ApprovalMode | undefined | null {
|
|
293
|
+
if (args.approval === undefined) return workerApprovalMode(env);
|
|
294
|
+
const mode = approvalModeOf(typeof args.approval === "string" ? args.approval.trim() : "");
|
|
295
|
+
if (!mode) return null;
|
|
296
|
+
env.BUGMOLE_APPROVAL = mode;
|
|
297
|
+
return mode;
|
|
298
|
+
}
|
|
299
|
+
|
|
213
300
|
export function resolveLocalRegistryPort(args: Record<string, any>): number {
|
|
214
301
|
if (typeof args.localregistryport === "number") return args.localregistryport;
|
|
215
302
|
const fromEnv = process.env.BUGMOLE_LOCAL_REGISTRY_PORT?.trim();
|
|
@@ -352,6 +439,13 @@ async function main() {
|
|
|
352
439
|
if (positionalCommand === "artifacts") args.artifacts = true;
|
|
353
440
|
if (positionalCommand === "serve") args.serve = true;
|
|
354
441
|
if (positionalCommand === "reset") args.reset = true;
|
|
442
|
+
if (positionalCommand === "roles") args.roles = true;
|
|
443
|
+
if (positionalCommand === "secrets") args.secrets = true;
|
|
444
|
+
if (positionalCommand === "propose") {
|
|
445
|
+
args.propose = true;
|
|
446
|
+
const file = process.argv.slice(process.argv.indexOf("propose") + 1).find((arg) => !arg.startsWith("--"));
|
|
447
|
+
if (file) args.proposefile = file;
|
|
448
|
+
}
|
|
355
449
|
// quiet: dotenv 17 prints an "injected env" banner on every load, which
|
|
356
450
|
// would land in the middle of this CLI's own output.
|
|
357
451
|
dotenv.config({ path: preferExisting(".bugmole.env", ".valk.env"), quiet: true });
|
|
@@ -385,29 +479,38 @@ async function main() {
|
|
|
385
479
|
|
|
386
480
|
// Init must run from the caller's workspace, not the package directory.
|
|
387
481
|
if (resolveMode(args) === "init") {
|
|
388
|
-
const { runInitWizard } = await import("../src/runtime/init-wizard.js");
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
482
|
+
const { runInitWizard, PlanLimitError, describePlanLimit } = await import("../src/runtime/init-wizard.js");
|
|
483
|
+
try {
|
|
484
|
+
await runInitWizard({
|
|
485
|
+
nonInteractive: args.noninteractive === true,
|
|
486
|
+
force: args.force === true,
|
|
487
|
+
configPath: args.config,
|
|
488
|
+
workspaceId: args.workspaceid,
|
|
489
|
+
workspaceName: args.workspacename,
|
|
490
|
+
projectId: args.projectid,
|
|
491
|
+
projectName: args.projectname,
|
|
492
|
+
appId: args.appid,
|
|
493
|
+
appName: args.appname,
|
|
494
|
+
environment: args.environment,
|
|
495
|
+
specDir: args.specdir,
|
|
496
|
+
evidenceManifest: args.evidencemanifest,
|
|
497
|
+
evidenceScreenshots: args.evidencescreenshots,
|
|
498
|
+
artifactsDir: args.artifactsdir,
|
|
499
|
+
storageProvider: args.storageprovider,
|
|
500
|
+
bucket: args.bucket,
|
|
501
|
+
region: args.region,
|
|
502
|
+
endpoint: args.endpoint,
|
|
503
|
+
cloudProjectId: args.cloudprojectid,
|
|
504
|
+
accountId: args.accountid,
|
|
505
|
+
appPath: typeof args.apppath === "string" ? args.apppath : undefined,
|
|
506
|
+
appUrl: typeof args.appurl === "string" ? args.appurl : undefined,
|
|
507
|
+
});
|
|
508
|
+
} catch (error) {
|
|
509
|
+
// A plan limit is an answer, not a crash: say what the plan allows and where to upgrade.
|
|
510
|
+
if (!(error instanceof PlanLimitError)) throw error;
|
|
511
|
+
console.error(describePlanLimit(error));
|
|
512
|
+
process.exitCode = 1;
|
|
513
|
+
}
|
|
411
514
|
return;
|
|
412
515
|
}
|
|
413
516
|
|
|
@@ -449,23 +552,56 @@ async function main() {
|
|
|
449
552
|
return;
|
|
450
553
|
}
|
|
451
554
|
|
|
555
|
+
// Suite and discover run without a project configuration, but use its
|
|
556
|
+
// secrets: map when there is one, so flows can type values from a vault.
|
|
557
|
+
const flowSecrets = async (projectId: string) => {
|
|
558
|
+
const { secretMap } = await import("../src/runtime/secret-sources.js");
|
|
559
|
+
const { withFlowSecrets } = await import("../src/runtime/secret-driver.js");
|
|
560
|
+
let cfg: { secrets?: unknown; project?: { id?: string } } | null = null;
|
|
561
|
+
try {
|
|
562
|
+
if (fs.existsSync(path.resolve(workspaceRoot, configPath))) cfg = loadConfig(path.resolve(workspaceRoot, configPath));
|
|
563
|
+
} catch {
|
|
564
|
+
cfg = null;
|
|
565
|
+
}
|
|
566
|
+
const { map, problems } = secretMap(cfg);
|
|
567
|
+
for (const problem of problems) console.warn(`[Bugmole] ${problem}`);
|
|
568
|
+
const id = cfg?.project?.id ?? projectId;
|
|
569
|
+
return (driver: import("../src/runtime/driver.js").AppDriver) => withFlowSecrets(driver, { map, projectId: id });
|
|
570
|
+
};
|
|
571
|
+
|
|
452
572
|
// A web suite is deterministic: it needs a running app and flow files, not
|
|
453
573
|
// an LLM or a project configuration, so it runs in any workspace as-is.
|
|
454
574
|
if (mode === "suite") {
|
|
455
575
|
const { runWebSuite } = await import("../src/runtime/web-suite.js");
|
|
576
|
+
let configured: { project?: { id?: string }; runtime?: { spec_dir?: string } } | null = null;
|
|
577
|
+
try {
|
|
578
|
+
if (fs.existsSync(path.resolve(workspaceRoot, configPath))) configured = loadConfig(path.resolve(workspaceRoot, configPath));
|
|
579
|
+
} catch {
|
|
580
|
+
configured = null;
|
|
581
|
+
}
|
|
582
|
+
// Evidence goes under the project's ID, so reset finds it.
|
|
583
|
+
const suiteProject = typeof args.project === "string" ? args.project : configured?.project?.id ?? path.basename(workspaceRoot);
|
|
584
|
+
// The first flows folder that exists: spec/flows (where the docs put
|
|
585
|
+
// them), flows/, then .maestro.
|
|
586
|
+
const specFlows = path.join(configured?.runtime?.spec_dir ?? "spec", "flows");
|
|
587
|
+
const defaultFlows = [specFlows, "flows", ".maestro"].find((dir) => fs.existsSync(path.resolve(workspaceRoot, dir)));
|
|
588
|
+
if (typeof args.flows !== "string" && !defaultFlows) {
|
|
589
|
+
throw new Error(`No flows folder found (looked for ${specFlows}, flows and .maestro). Pass --flows <dir>.`);
|
|
590
|
+
}
|
|
456
591
|
const { targetsFromFlags } = await import("../src/runtime/browser-matrix.js");
|
|
457
592
|
const baseUrl = typeof args.baseurl === "string" ? args.baseurl : process.env.BUGMOLE_BASE_URL;
|
|
458
593
|
if (!baseUrl) throw new Error("--base-url (or BUGMOLE_BASE_URL) is required for --mode suite");
|
|
459
594
|
const report = await runWebSuite({
|
|
460
|
-
flowsDir: path.resolve(workspaceRoot, typeof args.flows === "string" ? args.flows :
|
|
595
|
+
flowsDir: path.resolve(workspaceRoot, typeof args.flows === "string" ? args.flows : defaultFlows!),
|
|
461
596
|
baseUrl,
|
|
462
597
|
artifactsDir: path.resolve(workspaceRoot, typeof args.artifactsdir === "string" ? args.artifactsdir : preferExisting(path.join(workspaceRoot, "qa/bugmole-runs"), path.join(workspaceRoot, "qa/valkyrie-runs"))),
|
|
463
|
-
projectId:
|
|
598
|
+
projectId: suiteProject,
|
|
464
599
|
headless: args.headed !== true,
|
|
465
600
|
timeoutMs: typeof args.timeout === "number" ? args.timeout : undefined,
|
|
466
601
|
grep: typeof args.grep === "string" ? args.grep : undefined,
|
|
467
602
|
shareSession: args.sharesession === true,
|
|
468
603
|
targets: targetsFromFlags(args.browsers, args.devices),
|
|
604
|
+
wrapDriver: await flowSecrets(suiteProject),
|
|
469
605
|
});
|
|
470
606
|
const { total, passed, failed, blocked } = report.totals;
|
|
471
607
|
console.log(`[Bugmole] Suite ${passed}/${total} passed (${failed} failed, ${blocked} blocked). Report: ${report.reportPath}`);
|
|
@@ -489,7 +625,8 @@ async function main() {
|
|
|
489
625
|
if (typeof args.signin === "string") {
|
|
490
626
|
const { PlaywrightDriver } = await import("../src/runtime/playwright-driver.js");
|
|
491
627
|
storageState = path.join(sessionDir, "session-state.json");
|
|
492
|
-
const
|
|
628
|
+
const projectId = typeof args.project === "string" ? args.project : path.basename(workspaceRoot);
|
|
629
|
+
const signIn = await (await flowSecrets(projectId))(new PlaywrightDriver({ headless: args.headed !== true })).run({
|
|
493
630
|
runId: `discover-${Date.now()}`,
|
|
494
631
|
projectId: typeof args.project === "string" ? args.project : path.basename(workspaceRoot),
|
|
495
632
|
journeyId: "sign-in",
|
|
@@ -520,10 +657,25 @@ async function main() {
|
|
|
520
657
|
}
|
|
521
658
|
await fs.promises.writeFile(
|
|
522
659
|
path.join(outDir, "discovery.json"),
|
|
523
|
-
`${JSON.stringify({
|
|
660
|
+
`${JSON.stringify({
|
|
661
|
+
baseUrl: result.baseUrl,
|
|
662
|
+
pages: result.pages.map(({ route, heading, title, path: via }) => ({ route, heading, title, via })),
|
|
663
|
+
limits: result.limits,
|
|
664
|
+
events: result.events,
|
|
665
|
+
}, null, 2)}\n`,
|
|
524
666
|
"utf8",
|
|
525
667
|
);
|
|
526
|
-
|
|
668
|
+
// Flows found signed in only pass signed in: put the sign-in flow first
|
|
669
|
+
// in the folder, so a suite with --share-session signs in, then runs them.
|
|
670
|
+
if (typeof args.signin === "string") {
|
|
671
|
+
await fs.promises.copyFile(path.resolve(workspaceRoot, args.signin), path.join(outDir, "00-sign-in.yaml"));
|
|
672
|
+
}
|
|
673
|
+
const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
674
|
+
const outName = path.relative(workspaceRoot, outDir) || ".";
|
|
675
|
+
console.log(`[Bugmole] Discovered ${plural(result.pages.length, "page")} and wrote ${plural(result.flows.length, "flow")} to ${outName}`);
|
|
676
|
+
if (result.flows.length) {
|
|
677
|
+
console.log(`[Bugmole] Run them: bugmole --mode suite --flows ${outName} --base-url ${baseUrl}${typeof args.signin === "string" ? " --share-session" : ""}`);
|
|
678
|
+
}
|
|
527
679
|
if (result.pages.length === 0) process.exitCode = 1;
|
|
528
680
|
} finally {
|
|
529
681
|
// Holds the app's cookies and tokens: never outlives the exploration.
|
|
@@ -549,7 +701,13 @@ async function main() {
|
|
|
549
701
|
// Serve only needs the local runtime/storage and registry settings. Full
|
|
550
702
|
// execution validation belongs to plan/run modes, which require LLM and
|
|
551
703
|
// browser configuration that an idle worker does not use.
|
|
552
|
-
if (mode
|
|
704
|
+
if (mode === "secrets") {
|
|
705
|
+
const { runSecretsCommand } = await import("../src/runtime/secrets-cli.js");
|
|
706
|
+
process.exitCode = await runSecretsCommand(cfg, process.argv, { workspaceRoot });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (mode !== "serve" && mode !== "artifacts" && mode !== "explore" && mode !== "roles") {
|
|
553
711
|
const { validateConfigOrThrow } = await import("../src/runtime/config-validate.js");
|
|
554
712
|
validateConfigOrThrow(cfg);
|
|
555
713
|
}
|
|
@@ -563,7 +721,36 @@ async function main() {
|
|
|
563
721
|
|
|
564
722
|
// Dispatch modes
|
|
565
723
|
switch (mode) {
|
|
724
|
+
case "roles": {
|
|
725
|
+
const { parseRolesCommand, runRolesCommand } = await import("../src/runtime/roles-cli.js");
|
|
726
|
+
const command = parseRolesCommand(process.argv, args);
|
|
727
|
+
if ("error" in command) {
|
|
728
|
+
console.error(command.error);
|
|
729
|
+
process.exit(2);
|
|
730
|
+
}
|
|
731
|
+
for (const line of await runRolesCommand(cfg, command)) console.log(`[Bugmole] ${line}`);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
case "propose": {
|
|
735
|
+
if (typeof args.proposefile !== "string") {
|
|
736
|
+
console.error("Usage: bugmole propose <changes.json>");
|
|
737
|
+
process.exit(2);
|
|
738
|
+
}
|
|
739
|
+
const { proposeFromFile } = await import("../src/runtime/propose-cli.js");
|
|
740
|
+
const proposals = await proposeFromFile(cfg, args.proposefile);
|
|
741
|
+
for (const proposal of proposals) {
|
|
742
|
+
const state = proposal.status === "pending" ? "waiting for a manager's review" : proposal.status;
|
|
743
|
+
console.log(`[Bugmole] ${proposal.action} ${proposal.caseSetId}/${proposal.caseId}: ${state} (${proposal.id})`);
|
|
744
|
+
}
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
566
747
|
case "serve": {
|
|
748
|
+
const approval = applyApprovalFlag(args);
|
|
749
|
+
if (approval === null) {
|
|
750
|
+
console.error("[Bugmole] --approval must be manual, auto or extreme");
|
|
751
|
+
process.exit(2);
|
|
752
|
+
}
|
|
753
|
+
if (approval) console.log(`[Bugmole] Approval: ${approval}. This worker waits for a person's approval on ${approval === "manual" ? "every task" : approval === "auto" ? "tasks that change code" : "nothing"}, whatever the project allows.`);
|
|
567
754
|
const remoteStorage = Boolean(cfg.storage?.provider && cfg.storage.provider !== "local");
|
|
568
755
|
let closeBridge: () => void = () => {};
|
|
569
756
|
if (remoteStorage) {
|
|
@@ -602,7 +789,7 @@ async function main() {
|
|
|
602
789
|
console.log(`[Bugmole] MCP will start on the configured HTTP port`);
|
|
603
790
|
console.log(`[Bugmole] Capability expires at ${bridge.expiresAt}`);
|
|
604
791
|
if (!process.env.BUGMOLE_AEGIS_EMAIL?.trim()) {
|
|
605
|
-
console.warn("[Bugmole]
|
|
792
|
+
console.warn("[Bugmole] Sign in with your Bugmole account email on the page that opens.");
|
|
606
793
|
}
|
|
607
794
|
console.log(
|
|
608
795
|
`[Bugmole] Dashboard handoff: ${buildDashboardHandoffUrl(dashboardHandoff.toString(), cfg.project?.id ?? "default", fragment)}`,
|
|
@@ -711,7 +898,7 @@ async function startMcpServerForServe(cfg: any): Promise<void> {
|
|
|
711
898
|
|
|
712
899
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
713
900
|
main().catch((err) => {
|
|
714
|
-
console.error(`[Bugmole
|
|
901
|
+
console.error(`[Bugmole] ${err?.message ?? err}`);
|
|
715
902
|
process.exit(1);
|
|
716
903
|
});
|
|
717
904
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Vendors Arcana's bring-your-own-vault resolver (packages/byok/src/byok.ts
|
|
2
|
+
// in theaiinc/arcana) into src/vendor/byok.ts, verbatim under a header. It is
|
|
3
|
+
// one dependency-free file, so the CLI ships it without the Arcana app.
|
|
4
|
+
// ARCANA_DIR=~/arcana node scripts/sync-byok.mjs
|
|
5
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
10
|
+
export const byokCopy = path.join(root, "src/vendor/byok.ts");
|
|
11
|
+
export const BYOK_HEADER = "// VENDORED from theaiinc/arcana packages/byok/src/byok.ts by `node scripts/sync-byok.mjs` — change it there, then sync.\n";
|
|
12
|
+
|
|
13
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
14
|
+
const arcana = process.env.ARCANA_DIR ?? path.join(os.homedir(), "arcana");
|
|
15
|
+
const source = await readFile(path.join(arcana, "packages/byok/src/byok.ts"), "utf8");
|
|
16
|
+
await writeFile(byokCopy, BYOK_HEADER + source);
|
|
17
|
+
console.log(`Wrote ${path.relative(root, byokCopy)} from ${arcana}`);
|
|
18
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Copies the
|
|
2
|
-
// without importing files from outside
|
|
3
|
-
//
|
|
1
|
+
// Copies files the dashboard shares with the CLI and registry into
|
|
2
|
+
// apps/qa, which deliberately builds without importing files from outside
|
|
3
|
+
// it. Run after editing a source below; the root test suite fails if a copy
|
|
4
|
+
// drifts.
|
|
4
5
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
|
|
@@ -8,9 +9,18 @@ const root = path.resolve(import.meta.dirname, "..");
|
|
|
8
9
|
export const catalogSource = path.join(root, "src/billing/plan-catalog.ts");
|
|
9
10
|
export const catalogCopy = path.join(root, "apps/qa/src/lib/plan-catalog.ts");
|
|
10
11
|
export const COPY_HEADER = "// GENERATED by `npm run sync:catalog` from src/billing/plan-catalog.ts — edit that file instead.\n";
|
|
12
|
+
export const scenarioSource = path.join(root, "src/runtime/scenario-matrix.ts");
|
|
13
|
+
export const scenarioCopy = path.join(root, "apps/qa/src/lib/scenario-matrix.ts");
|
|
14
|
+
export const SCENARIO_COPY_HEADER = "// GENERATED by `npm run sync:catalog` from src/runtime/scenario-matrix.ts — edit that file instead.\n";
|
|
15
|
+
|
|
16
|
+
const COPIES = [
|
|
17
|
+
[catalogSource, catalogCopy, COPY_HEADER],
|
|
18
|
+
[scenarioSource, scenarioCopy, SCENARIO_COPY_HEADER],
|
|
19
|
+
];
|
|
11
20
|
|
|
12
21
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
13
|
-
const source
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
for (const [source, copy, header] of COPIES) {
|
|
23
|
+
await writeFile(copy, header + await readFile(source, "utf8"));
|
|
24
|
+
console.log(`Wrote ${path.relative(root, copy)}`);
|
|
25
|
+
}
|
|
16
26
|
}
|