@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bugmole/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"runner:dev": "wrangler dev -c wrangler.runner.jsonc",
|
|
47
47
|
"runner:deploy": "wrangler deploy -c wrangler.runner.jsonc",
|
|
48
48
|
"sync:catalog": "node scripts/sync-plan-catalog.mjs",
|
|
49
|
-
"test": "NODE_OPTIONS=--max-old-space-size=512 tsx --test --test-concurrency=1 scripts/bugmole.test.ts scripts/ensure-playwright.test.ts scripts/sync-launcher-version.test.ts src/mcp/server.test.ts projects/registry.test.ts src/integrations/testflight-feedback.test.ts src/integrations/temp-email.test.ts src/storage/object-store.test.ts src/storage/local-artifact-server.test.ts src/storage/storage-directory.test.ts src/runtime/reset.test.ts src/runtime/run-job.test.ts src/runtime/serve-gateway.test.ts src/registry/task-scheduling.test.ts src/registry-worker/artifacts.test.ts src/registry-worker/runs.test.ts src/registry-worker/org/routes.test.ts src/registry-worker/billing/billing-core.test.ts src/registry-worker/billing/paypal.test.ts src/registry-worker/ai/routes.test.ts src/registry-worker/runner/runner.test.ts src/registry-worker/signup/signup.test.ts src/registry-worker/signup/attribution.test.ts src/registry-worker/notifications/notifications.test.ts src/registry-worker/github/checks.test.ts src/registry-worker/gitlab/checks.test.ts src/registry-worker/personas/personas.test.ts src/registry-worker/feedback/feedback.test.ts src/registry-worker/devices/devices.test.ts src/registry-worker/domains/domains.test.ts src/registry-worker/sso/sso.test.ts src/registry-worker/mcp/mcp.test.ts src/integrations/github-app.test.ts src/runtime/run-once.test.ts src/runtime/appium-driver.test.ts src/integrations/aws-sigv4.test.ts src/runtime/ai-work.test.ts src/billing/plan-catalog.test.ts src/mcp/write-test-cases.test.ts src/runtime/pipeline.test.ts src/runtime/planner.test.ts src/runtime/test-case-verdicts.test.ts src/runtime/journey-graph.test.ts src/runtime/journey-evidence.test.ts src/runtime/local-browser.test.ts src/runtime/serve-worker.test.ts src/runtime/cursor-driver-run.test.ts src/runtime/explorer.test.ts src/runtime/nav-crawl.test.ts src/runtime/app-understanding.test.ts src/runtime/environment.test.ts src/runtime/blocker-resolution.test.ts src/runtime/ui-reverse-engineering/coordinator.test.ts src/runtime/maestro-driver.test.ts src/runtime/playwright-driver.test.ts src/runtime/flow-language.test.ts src/runtime/web-suite.test.ts src/runtime/canonical-flow.test.ts src/runtime/executor.test.ts src/runtime/ios-screen-record.test.ts src/runtime/android-screen-record.test.ts src/runtime/reviewer.test.ts src/runtime/reporter.test.ts src/runtime/cursor-cli-driver.test.ts src/runtime/local-registry-stub.test.ts src/runtime/browser-matrix.test.ts src/runtime/browser-matrix.integration.test.ts src/runtime/failure-analysis.test.ts src/runtime/site-discovery.test.ts src/runtime/ai-exploration.test.ts src/runtime/discovery-task.test.ts",
|
|
49
|
+
"test": "NODE_OPTIONS=--max-old-space-size=512 tsx --test --test-concurrency=1 scripts/bugmole.test.ts scripts/ensure-playwright.test.ts scripts/sync-launcher-version.test.ts src/mcp/server.test.ts projects/registry.test.ts src/integrations/testflight-feedback.test.ts src/integrations/temp-email.test.ts src/storage/object-store.test.ts src/storage/local-artifact-server.test.ts src/storage/storage-directory.test.ts src/storage/test-case-results.test.ts src/runtime/reset.test.ts src/runtime/run-job.test.ts src/runtime/apply-proposals.test.ts src/runtime/serve-gateway.test.ts src/registry/task-scheduling.test.ts src/registry/test-case-revisions.test.ts src/registry-worker/task-approval.test.ts src/registry-worker/proposals/proposals.test.ts src/registry-worker/artifacts.test.ts src/registry-worker/runs.test.ts src/registry-worker/org/routes.test.ts src/registry-worker/billing/billing-core.test.ts src/registry-worker/billing/paypal.test.ts src/registry-worker/billing/plan-gaps.test.ts src/registry-worker/ai/routes.test.ts src/registry-worker/runner/runner.test.ts src/registry-worker/signup/signup.test.ts src/registry-worker/signup/attribution.test.ts src/registry-worker/notifications/notifications.test.ts src/registry-worker/github/checks.test.ts src/registry-worker/gitlab/checks.test.ts src/registry-worker/personas/personas.test.ts src/registry-worker/feedback/feedback.test.ts src/registry-worker/devices/devices.test.ts src/registry-worker/domains/domains.test.ts src/registry-worker/sso/sso.test.ts src/registry-worker/mcp/mcp.test.ts src/registry-worker/feature-flags/feature-flags.test.ts scripts/bugmole-admin.test.mjs src/integrations/github-app.test.ts src/runtime/run-once.test.ts src/runtime/appium-driver.test.ts src/integrations/aws-sigv4.test.ts src/runtime/ai-work.test.ts src/billing/plan-catalog.test.ts src/runtime/scenario-matrix.test.ts src/mcp/write-test-cases.test.ts src/runtime/pipeline.test.ts src/runtime/planner.test.ts src/runtime/test-case-verdicts.test.ts src/runtime/journey-graph.test.ts src/runtime/journey-evidence.test.ts src/runtime/local-browser.test.ts src/runtime/serve-worker.test.ts src/runtime/cursor-driver-run.test.ts src/runtime/explorer.test.ts src/runtime/nav-crawl.test.ts src/runtime/app-understanding.test.ts src/runtime/environment.test.ts src/runtime/blocker-resolution.test.ts src/runtime/ui-reverse-engineering/coordinator.test.ts src/runtime/maestro-driver.test.ts src/runtime/playwright-driver.test.ts src/runtime/flow-language.test.ts src/runtime/web-suite.test.ts src/runtime/canonical-flow.test.ts src/runtime/executor.test.ts src/runtime/ios-screen-record.test.ts src/runtime/android-screen-record.test.ts src/runtime/reviewer.test.ts src/runtime/reporter.test.ts src/runtime/cursor-cli-driver.test.ts src/runtime/local-registry-stub.test.ts src/runtime/browser-matrix.test.ts src/runtime/browser-matrix.integration.test.ts src/runtime/failure-analysis.test.ts src/runtime/site-discovery.test.ts src/runtime/ai-exploration.test.ts src/registry-worker/task-resume.test.ts src/registry-worker/secrets/secrets.test.ts src/registry-worker/jira/jira.test.ts src/runtime/record-run-verdicts.test.ts src/runtime/cloud-secrets.test.ts src/runtime/secret-sources.test.ts src/runtime/vault-federation.test.ts src/registry-worker/identity/oidc.test.ts src/registry-worker/task-review.test.ts src/registry-worker/roles/roles.test.ts src/registry/roles.test.ts src/runtime/project-roles.test.ts src/runtime/roles-cli.test.ts src/mcp/roles-and-review.test.ts src/runtime/discovery-task.test.ts src/registry/project-identity.test.ts src/runtime/project-identity.test.ts src/registry-worker/projects/projects.test.ts",
|
|
50
50
|
"test:mcp": "NODE_OPTIONS=--max-old-space-size=512 tsx --test --test-concurrency=1 scripts/bugmole.test.ts src/mcp/server.test.ts projects/registry.test.ts src/integrations/testflight-feedback.test.ts src/storage/object-store.test.ts src/runtime/maestro-driver.test.ts src/runtime/canonical-flow.test.ts",
|
|
51
51
|
"typecheck": "NODE_OPTIONS=--max-old-space-size=512 tsc --noEmit"
|
|
52
52
|
},
|
|
@@ -81,5 +81,8 @@
|
|
|
81
81
|
"gaxios": {
|
|
82
82
|
"uuid": "^11.1.1"
|
|
83
83
|
}
|
|
84
|
+
},
|
|
85
|
+
"optionalDependencies": {
|
|
86
|
+
"@theaiinc/arcana-vault": "^0.1.0"
|
|
84
87
|
}
|
|
85
88
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Platform-operator CLI for feature overrides, beta opt-ins and contract plans.
|
|
3
|
+
// It calls the registry's /api/platform routes (src/registry-worker/feature-flags/admin.ts).
|
|
4
|
+
//
|
|
5
|
+
// PLATFORM_ADMIN_TOKEN=… node scripts/bugmole-admin.mjs flags list [workspace]
|
|
6
|
+
// node scripts/bugmole-admin.mjs flags set <workspace> <feature> on|off [--until 2026-12-31] [--reason "…"]
|
|
7
|
+
// node scripts/bugmole-admin.mjs flags clear <workspace> <feature>
|
|
8
|
+
// node scripts/bugmole-admin.mjs flags beta <workspace> <feature> on|off|clear
|
|
9
|
+
// node scripts/bugmole-admin.mjs plan show <workspace>
|
|
10
|
+
// node scripts/bugmole-admin.mjs plan set <workspace> enterprise [--projects N] [--minutes N] [--credits N]
|
|
11
|
+
// [--parallel N] [--retention-days N] [--feature scim=off ...] [--period-end 2027-09-30] [--note "…"]
|
|
12
|
+
// node scripts/bugmole-admin.mjs plan set <workspace> free
|
|
13
|
+
//
|
|
14
|
+
// Environment: PLATFORM_ADMIN_TOKEN (required; never printed), BUGMOLE_REGISTRY_URL
|
|
15
|
+
// (default https://api.bugmole.com), BUGMOLE_OPERATOR (who you are, for the audit log;
|
|
16
|
+
// defaults to $USER). Limits accept "unlimited". See docs/feature-flags.md.
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import { pathToFileURL } from "node:url";
|
|
19
|
+
|
|
20
|
+
export const USAGE = `Usage:
|
|
21
|
+
bugmole-admin flags list [workspace]
|
|
22
|
+
bugmole-admin flags set <workspace> <feature> on|off [--until <date>] [--reason <text>]
|
|
23
|
+
bugmole-admin flags clear <workspace> <feature>
|
|
24
|
+
bugmole-admin flags beta <workspace> <feature> on|off|clear
|
|
25
|
+
bugmole-admin plan show <workspace>
|
|
26
|
+
bugmole-admin plan set <workspace> enterprise [--projects N] [--minutes N] [--credits N] [--parallel N]
|
|
27
|
+
[--retention-days N] [--feature <name>=on|off]... [--period-end <date>] [--note <text>]
|
|
28
|
+
bugmole-admin plan set <workspace> free`;
|
|
29
|
+
|
|
30
|
+
const LIMIT_OPTIONS = {
|
|
31
|
+
projects: "projects",
|
|
32
|
+
minutes: "cloudMinutes",
|
|
33
|
+
credits: "aiCredits",
|
|
34
|
+
parallel: "parallel",
|
|
35
|
+
"retention-days": "retentionDays",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
class UsageError extends Error {}
|
|
39
|
+
|
|
40
|
+
function splitOptions(args) {
|
|
41
|
+
const positional = [];
|
|
42
|
+
const options = {};
|
|
43
|
+
for (let index = 0; index < args.length; index++) {
|
|
44
|
+
const arg = args[index];
|
|
45
|
+
if (!arg.startsWith("--")) {
|
|
46
|
+
positional.push(arg);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const [rawName, inline] = arg.slice(2).split(/=(.*)/s, 2);
|
|
50
|
+
const value = inline ?? args[++index];
|
|
51
|
+
if (value === undefined) throw new UsageError(`--${rawName} needs a value`);
|
|
52
|
+
if (rawName === "feature") (options.feature ??= []).push(value);
|
|
53
|
+
else options[rawName] = value;
|
|
54
|
+
}
|
|
55
|
+
return { positional, options };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onOff(value, what) {
|
|
59
|
+
if (value === "on") return true;
|
|
60
|
+
if (value === "off") return false;
|
|
61
|
+
throw new UsageError(`${what} must be on or off`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function limitValue(name, value) {
|
|
65
|
+
if (value === "unlimited") return null;
|
|
66
|
+
if (!/^\d+$/.test(value)) throw new UsageError(`--${name} must be a whole number or "unlimited"`);
|
|
67
|
+
return Number(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const segment = (value) => encodeURIComponent(value);
|
|
71
|
+
|
|
72
|
+
/** Turns command-line arguments into one registry request. Exported for tests. */
|
|
73
|
+
export function buildRequest(argv) {
|
|
74
|
+
const { positional, options } = splitOptions(argv);
|
|
75
|
+
const [group, action, workspace, ...rest] = positional;
|
|
76
|
+
const need = (value, name) => {
|
|
77
|
+
if (!value) throw new UsageError(`Missing ${name}`);
|
|
78
|
+
return value;
|
|
79
|
+
};
|
|
80
|
+
const known = (allowed) => {
|
|
81
|
+
for (const name of Object.keys(options)) if (!allowed.includes(name)) throw new UsageError(`Unknown option --${name}`);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (group === "flags") {
|
|
85
|
+
if (action === "list") {
|
|
86
|
+
known([]);
|
|
87
|
+
return workspace
|
|
88
|
+
? { method: "GET", path: `/api/platform/workspaces/${segment(workspace)}/features` }
|
|
89
|
+
: { method: "GET", path: "/api/platform/feature-overrides" };
|
|
90
|
+
}
|
|
91
|
+
const feature = need(rest[0], "feature");
|
|
92
|
+
const base = `/api/platform/workspaces/${segment(need(workspace, "workspace"))}`;
|
|
93
|
+
if (action === "set") {
|
|
94
|
+
known(["until", "reason"]);
|
|
95
|
+
const body = { enabled: onOff(need(rest[1], "on|off"), "The value") };
|
|
96
|
+
if (options.until) body.expiresAt = options.until;
|
|
97
|
+
if (options.reason) body.reason = options.reason;
|
|
98
|
+
return { method: "PUT", path: `${base}/features/${segment(feature)}`, body };
|
|
99
|
+
}
|
|
100
|
+
if (action === "clear") {
|
|
101
|
+
known([]);
|
|
102
|
+
return { method: "DELETE", path: `${base}/features/${segment(feature)}` };
|
|
103
|
+
}
|
|
104
|
+
if (action === "beta") {
|
|
105
|
+
known([]);
|
|
106
|
+
const value = need(rest[1], "on|off|clear");
|
|
107
|
+
if (value === "clear") return { method: "DELETE", path: `${base}/beta/${segment(feature)}` };
|
|
108
|
+
return { method: "PUT", path: `${base}/beta/${segment(feature)}`, body: { enabled: onOff(value, "The value") } };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (group === "plan") {
|
|
113
|
+
const base = `/api/platform/workspaces/${segment(need(workspace, "workspace"))}`;
|
|
114
|
+
if (action === "show") {
|
|
115
|
+
known([]);
|
|
116
|
+
return { method: "GET", path: `${base}/features` };
|
|
117
|
+
}
|
|
118
|
+
if (action === "set") {
|
|
119
|
+
const plan = need(rest[0], "plan (enterprise or free)");
|
|
120
|
+
if (plan === "free") {
|
|
121
|
+
known(["note"]);
|
|
122
|
+
return { method: "PUT", path: `${base}/plan`, body: { plan, ...(options.note ? { note: options.note } : {}) } };
|
|
123
|
+
}
|
|
124
|
+
if (plan !== "enterprise") throw new UsageError("plan must be enterprise or free; Team and Business are bought through checkout");
|
|
125
|
+
known([...Object.keys(LIMIT_OPTIONS), "feature", "period-end", "note"]);
|
|
126
|
+
const limits = {};
|
|
127
|
+
for (const [name, key] of Object.entries(LIMIT_OPTIONS)) {
|
|
128
|
+
if (options[name] !== undefined) limits[key] = limitValue(name, options[name]);
|
|
129
|
+
}
|
|
130
|
+
const features = {};
|
|
131
|
+
for (const entry of options.feature ?? []) {
|
|
132
|
+
const [name, value] = entry.split("=");
|
|
133
|
+
if (!name || value === undefined) throw new UsageError("--feature takes <name>=on|off");
|
|
134
|
+
features[name] = onOff(value, `--feature ${name}`);
|
|
135
|
+
}
|
|
136
|
+
const body = { plan, limits, features };
|
|
137
|
+
if (options["period-end"]) body.periodEnd = options["period-end"];
|
|
138
|
+
if (options.note) body.note = options.note;
|
|
139
|
+
return { method: "PUT", path: `${base}/plan`, body };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw new UsageError("Unknown command");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function main(argv = process.argv.slice(2), env = process.env, doFetch = fetch, out = console) {
|
|
146
|
+
if (!argv.length || argv[0] === "help" || argv.includes("--help")) {
|
|
147
|
+
out.log(USAGE);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
let request;
|
|
151
|
+
try {
|
|
152
|
+
request = buildRequest(argv);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (!(error instanceof UsageError)) throw error;
|
|
155
|
+
out.error(`${error.message}\n\n${USAGE}`);
|
|
156
|
+
return 2;
|
|
157
|
+
}
|
|
158
|
+
const token = env.PLATFORM_ADMIN_TOKEN?.trim();
|
|
159
|
+
if (!token) {
|
|
160
|
+
out.error("Set PLATFORM_ADMIN_TOKEN (the registry's platform secret) in the environment.");
|
|
161
|
+
return 2;
|
|
162
|
+
}
|
|
163
|
+
const origin = (env.BUGMOLE_REGISTRY_URL || "https://api.bugmole.com").replace(/\/+$/, "");
|
|
164
|
+
const operator = env.BUGMOLE_OPERATOR || env.USER || os.userInfo().username || "unknown";
|
|
165
|
+
const result = await doFetch(`${origin}${request.path}`, {
|
|
166
|
+
method: request.method,
|
|
167
|
+
headers: {
|
|
168
|
+
authorization: `Bearer ${token}`,
|
|
169
|
+
"x-bugmole-operator": operator,
|
|
170
|
+
...(request.body ? { "content-type": "application/json" } : {}),
|
|
171
|
+
},
|
|
172
|
+
body: request.body ? JSON.stringify(request.body) : undefined,
|
|
173
|
+
});
|
|
174
|
+
const text = await result.text();
|
|
175
|
+
let body = text;
|
|
176
|
+
try {
|
|
177
|
+
body = JSON.stringify(JSON.parse(text), null, 2);
|
|
178
|
+
} catch {
|
|
179
|
+
// not JSON; print as is
|
|
180
|
+
}
|
|
181
|
+
if (!result.ok) {
|
|
182
|
+
out.error(`${request.method} ${request.path} → ${result.status}\n${body}`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
out.log(body);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
190
|
+
main().then((code) => { process.exitCode = code; }, (error) => {
|
|
191
|
+
console.error(error instanceof Error ? error.message : error);
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildRequest, main } from "./bugmole-admin.mjs";
|
|
4
|
+
|
|
5
|
+
test("flags commands map to the platform routes", () => {
|
|
6
|
+
assert.deepEqual(buildRequest(["flags", "list"]), { method: "GET", path: "/api/platform/feature-overrides" });
|
|
7
|
+
assert.deepEqual(buildRequest(["flags", "list", "acme"]), { method: "GET", path: "/api/platform/workspaces/acme/features" });
|
|
8
|
+
assert.deepEqual(buildRequest(["flags", "set", "acme", "sso", "on", "--until", "2026-12-31", "--reason", "Trial"]), {
|
|
9
|
+
method: "PUT",
|
|
10
|
+
path: "/api/platform/workspaces/acme/features/sso",
|
|
11
|
+
body: { enabled: true, expiresAt: "2026-12-31", reason: "Trial" },
|
|
12
|
+
});
|
|
13
|
+
assert.deepEqual(buildRequest(["flags", "set", "acme", "slack", "off"]).body, { enabled: false });
|
|
14
|
+
assert.deepEqual(buildRequest(["flags", "clear", "acme", "sso"]), { method: "DELETE", path: "/api/platform/workspaces/acme/features/sso" });
|
|
15
|
+
assert.deepEqual(buildRequest(["flags", "beta", "acme", "mcp", "on"]).body, { enabled: true });
|
|
16
|
+
assert.equal(buildRequest(["flags", "beta", "acme", "mcp", "clear"]).method, "DELETE");
|
|
17
|
+
assert.throws(() => buildRequest(["flags", "set", "acme", "sso", "maybe"]), /on or off/);
|
|
18
|
+
assert.throws(() => buildRequest(["flags", "set", "acme", "sso", "on", "--forever", "1"]), /Unknown option/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("plan set builds a contract", () => {
|
|
22
|
+
const request = buildRequest([
|
|
23
|
+
"plan", "set", "acme", "enterprise",
|
|
24
|
+
"--projects", "40", "--minutes", "unlimited", "--credits=20000", "--parallel", "60", "--retention-days", "400",
|
|
25
|
+
"--feature", "scim=off", "--feature", "self_hosted_workers=on", "--period-end", "2027-09-30", "--note", "C-17",
|
|
26
|
+
]);
|
|
27
|
+
assert.deepEqual(request, {
|
|
28
|
+
method: "PUT",
|
|
29
|
+
path: "/api/platform/workspaces/acme/plan",
|
|
30
|
+
body: {
|
|
31
|
+
plan: "enterprise",
|
|
32
|
+
limits: { projects: 40, cloudMinutes: null, aiCredits: 20000, parallel: 60, retentionDays: 400 },
|
|
33
|
+
features: { scim: false, self_hosted_workers: true },
|
|
34
|
+
periodEnd: "2027-09-30",
|
|
35
|
+
note: "C-17",
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
assert.deepEqual(buildRequest(["plan", "set", "acme", "free"]).body, { plan: "free" });
|
|
39
|
+
assert.throws(() => buildRequest(["plan", "set", "acme", "team"]), /checkout/);
|
|
40
|
+
assert.throws(() => buildRequest(["plan", "set", "acme", "enterprise", "--projects", "-3"]), /whole number/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("main sends the token and operator, and refuses without a token", async () => {
|
|
44
|
+
const logs = [];
|
|
45
|
+
const out = { log: (line) => logs.push(line), error: (line) => logs.push(line) };
|
|
46
|
+
assert.equal(await main(["flags", "list"], {}, fetch, out), 2);
|
|
47
|
+
assert.match(logs.pop(), /PLATFORM_ADMIN_TOKEN/);
|
|
48
|
+
|
|
49
|
+
let seen;
|
|
50
|
+
const fakeFetch = async (url, init) => {
|
|
51
|
+
seen = { url, init };
|
|
52
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
53
|
+
};
|
|
54
|
+
const code = await main(["flags", "set", "acme", "sso", "on"], {
|
|
55
|
+
PLATFORM_ADMIN_TOKEN: "tok", BUGMOLE_REGISTRY_URL: "https://registry.test/", BUGMOLE_OPERATOR: "steve",
|
|
56
|
+
}, fakeFetch, out);
|
|
57
|
+
assert.equal(code, 0);
|
|
58
|
+
assert.equal(seen.url, "https://registry.test/api/platform/workspaces/acme/features/sso");
|
|
59
|
+
assert.equal(seen.init.headers.authorization, "Bearer tok");
|
|
60
|
+
assert.equal(seen.init.headers["x-bugmole-operator"], "steve");
|
|
61
|
+
assert.ok(!logs.some((line) => line.includes("tok\"")), "the token is never printed");
|
|
62
|
+
});
|
package/scripts/bugmole.test.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { describe, it } from "node:test";
|
|
5
5
|
import {
|
|
6
|
+
applyApprovalFlag,
|
|
6
7
|
buildDashboardHandoffUrl,
|
|
7
8
|
parseArgs,
|
|
8
9
|
printHelp,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
} from "./bugmole.js";
|
|
18
19
|
import os from "node:os";
|
|
19
20
|
import path from "node:path";
|
|
20
|
-
import { runInitWizard, validateStorageProvider } from "../src/runtime/init-wizard.js";
|
|
21
|
+
import { PlanLimitError, describePlanLimit, planLimitFromRegistry, runInitWizard, validateStorageProvider } from "../src/runtime/init-wizard.js";
|
|
21
22
|
|
|
22
23
|
describe("Bugmole CLI contract", () => {
|
|
23
24
|
it("starts MCP for the canonical --mcp flag", () => {
|
|
@@ -65,6 +66,7 @@ describe("Bugmole CLI contract", () => {
|
|
|
65
66
|
it("leaves other modes' exit code alone regardless of their result", () => {
|
|
66
67
|
assert.equal(runModeExitCode("explore", { status: "partial" }), 0);
|
|
67
68
|
assert.equal(runModeExitCode("report", undefined), 0);
|
|
69
|
+
assert.equal(runModeExitCode("explore", { status: "error" }), 1);
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
it("accepts a dashboard URL override for serve handoffs", () => {
|
|
@@ -189,6 +191,53 @@ describe("Bugmole CLI contract", () => {
|
|
|
189
191
|
}
|
|
190
192
|
});
|
|
191
193
|
|
|
194
|
+
it("turns a registry plan refusal into its message and a billing link", () => {
|
|
195
|
+
const refusal = planLimitFromRegistry(402, {
|
|
196
|
+
error: "Your plan includes 1 project and this workspace has 1. Upgrade to Team to add more.",
|
|
197
|
+
code: "plan_limit",
|
|
198
|
+
upgradeUrl: "/billing?workspace=acme&plan=team",
|
|
199
|
+
}, "https://dash.test/");
|
|
200
|
+
assert.ok(refusal instanceof PlanLimitError);
|
|
201
|
+
assert.equal(refusal.billingUrl, "https://dash.test/billing?workspace=acme&plan=team");
|
|
202
|
+
assert.equal(
|
|
203
|
+
describePlanLimit(refusal),
|
|
204
|
+
"Your plan includes 1 project and this workspace has 1. Upgrade to Team to add more.\nSee plans and upgrade: https://dash.test/billing?workspace=acme&plan=team",
|
|
205
|
+
);
|
|
206
|
+
assert.equal(
|
|
207
|
+
planLimitFromRegistry(402, { error: "x", code: "free_workspace_exists", billingUrl: "https://app.bugmole.com/billing?plan=team" }, "https://dash.test")?.billingUrl,
|
|
208
|
+
"https://app.bugmole.com/billing?plan=team",
|
|
209
|
+
"the registry's absolute link wins",
|
|
210
|
+
);
|
|
211
|
+
assert.equal(planLimitFromRegistry(409, { error: "A project with this id already exists", code: "project_exists" }, "https://dash.test"), null);
|
|
212
|
+
assert.equal(planLimitFromRegistry(402, { code: "plan_limit", upgradeUrl: "https://evil.test/" }, "https://dash.test")?.billingUrl, "https://dash.test/billing", "only a relative Billing path is joined");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("stops init with the plan message instead of a generic registration failure", async () => {
|
|
216
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "bugmole-init-limit-"));
|
|
217
|
+
const saved = { key: process.env.BUGMOLE_API_KEY, registry: process.env.BUGMOLE_REGISTRY_URL, dashboard: process.env.BUGMOLE_DASHBOARD_URL };
|
|
218
|
+
const realFetch = globalThis.fetch;
|
|
219
|
+
process.env.BUGMOLE_API_KEY = "bm_live_test";
|
|
220
|
+
process.env.BUGMOLE_REGISTRY_URL = "https://registry.test";
|
|
221
|
+
process.env.BUGMOLE_DASHBOARD_URL = "https://dash.test";
|
|
222
|
+
globalThis.fetch = (async () => Response.json({
|
|
223
|
+
error: "Your plan includes 1 project and this workspace has 1. Upgrade to Team to add more.",
|
|
224
|
+
code: "plan_limit",
|
|
225
|
+
limit: "projects",
|
|
226
|
+
upgradeUrl: "/billing?workspace=acme&plan=team",
|
|
227
|
+
}, { status: 402 })) as typeof fetch;
|
|
228
|
+
try {
|
|
229
|
+
await assert.rejects(runInitWizard({ cwd, nonInteractive: true }), (error: unknown) =>
|
|
230
|
+
error instanceof PlanLimitError && error.billingUrl === "https://dash.test/billing?workspace=acme&plan=team" && /Upgrade to Team/.test(error.message));
|
|
231
|
+
} finally {
|
|
232
|
+
globalThis.fetch = realFetch;
|
|
233
|
+
for (const [name, value] of [["BUGMOLE_API_KEY", saved.key], ["BUGMOLE_REGISTRY_URL", saved.registry], ["BUGMOLE_DASHBOARD_URL", saved.dashboard]] as const) {
|
|
234
|
+
if (value === undefined) delete process.env[name];
|
|
235
|
+
else process.env[name] = value;
|
|
236
|
+
}
|
|
237
|
+
await rm(cwd, { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
192
241
|
it("accepts config alongside a boolean MCP flag", () => {
|
|
193
242
|
assert.deepEqual(parseArgs(["node", "bugmole", "--mcp", "--config", "custom.yaml"]), {
|
|
194
243
|
mcp: true,
|
|
@@ -342,3 +391,46 @@ describe("Bugmole CLI contract", () => {
|
|
|
342
391
|
}
|
|
343
392
|
});
|
|
344
393
|
});
|
|
394
|
+
|
|
395
|
+
describe("bugmole serve --approval", () => {
|
|
396
|
+
it("sets BUGMOLE_APPROVAL for the whole process, and refuses an unknown mode", () => {
|
|
397
|
+
const env: Record<string, string | undefined> = {};
|
|
398
|
+
assert.equal(applyApprovalFlag({ approval: "manual" }, env), "manual");
|
|
399
|
+
assert.equal(env.BUGMOLE_APPROVAL, "manual");
|
|
400
|
+
assert.equal(applyApprovalFlag({ approval: "yolo" }, {}), null);
|
|
401
|
+
assert.equal(applyApprovalFlag({}, { BUGMOLE_APPROVAL: "extreme" }), "extreme", "no flag: the environment decides");
|
|
402
|
+
assert.equal(applyApprovalFlag({}, {}), undefined, "neither: the project decides");
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("flag values that look like numbers stay names unless the flag is a count", () => {
|
|
407
|
+
const args = parseArgs(["node", "bugmole", "--mode", "suite", "--grep", "404", "--project", "2024", "--max-pages", "20", "--timeout", "5000"]);
|
|
408
|
+
assert.equal(args.grep, "404");
|
|
409
|
+
assert.equal(args.project, "2024");
|
|
410
|
+
assert.equal(args.maxpages, 20);
|
|
411
|
+
assert.equal(args.timeout, 5000);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("dashed flags are readable in both spellings", () => {
|
|
415
|
+
const args = parseArgs(["node", "bugmole", "--mode", "review", "--file-path", "a.ts", "--change-summary", "b", "--diff", "c"]);
|
|
416
|
+
assert.equal(args.filepath, "a.ts");
|
|
417
|
+
assert.equal(args.filePath, "a.ts");
|
|
418
|
+
assert.equal(args.changeSummary, "b");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe("init output", () => {
|
|
422
|
+
it("keeps .bugmole.env out of git and writes a config that passes validation", async () => {
|
|
423
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "bugmole-init-git-"));
|
|
424
|
+
try {
|
|
425
|
+
await mkdir(path.join(cwd, ".git"));
|
|
426
|
+
const result = await runInitWizard({ cwd, nonInteractive: true });
|
|
427
|
+
assert.match(await readFile(path.join(cwd, ".gitignore"), "utf8"), /^\/\.bugmole\.env$/m);
|
|
428
|
+
const YAML = (await import("yaml")).default;
|
|
429
|
+
const { validateConfig } = await import("../src/runtime/config-validate.js");
|
|
430
|
+
const outcome = validateConfig(YAML.parse(await readFile(path.join(cwd, result.configPath), "utf8")));
|
|
431
|
+
assert.deepEqual(outcome.errors, []);
|
|
432
|
+
} finally {
|
|
433
|
+
await rm(cwd, { recursive: true, force: true });
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
});
|