@bugbug-io/cli 13.39.1
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/AGENTS.md +103 -0
- package/README.md +229 -0
- package/bin/bugbug.mjs +3 -0
- package/dist/NavigatorApp-WSJMM3OT.js +2410 -0
- package/dist/app-A4ZLLFP5.js +21 -0
- package/dist/chunk-NTNHB6R6.js +6098 -0
- package/dist/chunk-UZVYEMEZ.js +37 -0
- package/dist/index.js +1442 -0
- package/dist/render-UGRPBJOT.js +7 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1442 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
render
|
|
4
|
+
} from "./chunk-UZVYEMEZ.js";
|
|
5
|
+
import {
|
|
6
|
+
BaseSuiteRunDetails,
|
|
7
|
+
BaseTestRunDetails,
|
|
8
|
+
DEFAULT_MCP_URL,
|
|
9
|
+
EXIT_GENERAL_ERROR,
|
|
10
|
+
EXIT_USAGE_ERROR,
|
|
11
|
+
MCP_SERVER_PACKAGE,
|
|
12
|
+
PLUGIN_PACKAGE_NAME,
|
|
13
|
+
PLUGIN_REPO_SOURCE,
|
|
14
|
+
addBreadcrumb,
|
|
15
|
+
analytics,
|
|
16
|
+
captureException,
|
|
17
|
+
closeSentry,
|
|
18
|
+
defaultJunitReportPath,
|
|
19
|
+
detectFormat,
|
|
20
|
+
emitError,
|
|
21
|
+
exitCodeForStatus,
|
|
22
|
+
exitWith,
|
|
23
|
+
exportProject,
|
|
24
|
+
exportTest,
|
|
25
|
+
formatErrorMessage,
|
|
26
|
+
formatInlineRunReport,
|
|
27
|
+
getCliConfig,
|
|
28
|
+
getSuite,
|
|
29
|
+
getSuiteRun,
|
|
30
|
+
getSuiteRunJunitReport,
|
|
31
|
+
getTest,
|
|
32
|
+
getTestRun,
|
|
33
|
+
getTestRunJunitReport,
|
|
34
|
+
getTestRunLogs,
|
|
35
|
+
getVariablesMap,
|
|
36
|
+
getVersion,
|
|
37
|
+
importProject,
|
|
38
|
+
importTest,
|
|
39
|
+
initProject,
|
|
40
|
+
initSentry,
|
|
41
|
+
listProfiles,
|
|
42
|
+
listSuites,
|
|
43
|
+
listTests,
|
|
44
|
+
login,
|
|
45
|
+
logout,
|
|
46
|
+
resolveOutputMode,
|
|
47
|
+
runClientCommand,
|
|
48
|
+
runSuite,
|
|
49
|
+
runTest,
|
|
50
|
+
setSentryTag,
|
|
51
|
+
showError,
|
|
52
|
+
startSentrySpan,
|
|
53
|
+
stopSuiteRun,
|
|
54
|
+
stopTestRun,
|
|
55
|
+
validateReporterOptions,
|
|
56
|
+
waitForSuiteRun,
|
|
57
|
+
waitForTestRun,
|
|
58
|
+
writeReportXml
|
|
59
|
+
} from "./chunk-NTNHB6R6.js";
|
|
60
|
+
|
|
61
|
+
// src/instrument.ts
|
|
62
|
+
initSentry({ telemetryEnabled: getCliConfig().telemetryEnabled, env: process.env });
|
|
63
|
+
|
|
64
|
+
// src/index.ts
|
|
65
|
+
import { Command, CommanderError } from "commander";
|
|
66
|
+
|
|
67
|
+
// src/utils/output.ts
|
|
68
|
+
var printJson = (value) => {
|
|
69
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
70
|
+
`);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/features/auth/auth.command.ts
|
|
74
|
+
var registerLoginCommand = (program2, commandName = "login") => program2.command(commandName).description("Log in to BugBug and store a user token in the global config.").action(async () => {
|
|
75
|
+
const mode = resolveOutputMode();
|
|
76
|
+
try {
|
|
77
|
+
const result = await login();
|
|
78
|
+
if (mode === "json") {
|
|
79
|
+
printJson(result);
|
|
80
|
+
} else {
|
|
81
|
+
process.stdout.write(`Logged in. Token saved to "${result.credentialsFile}"
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
const shouldBackfillSessionStart = !analytics.get().isEnabled;
|
|
85
|
+
analytics.refreshWithToken(result.token);
|
|
86
|
+
if (shouldBackfillSessionStart) {
|
|
87
|
+
analytics.trackEvent("cli_session_started");
|
|
88
|
+
}
|
|
89
|
+
analytics.trackEvent("cli_logged_in", { success: true, outputMode: mode });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
analytics.trackEvent("cli_logged_in", { success: false, outputMode: mode });
|
|
92
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
93
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
var registerLogoutCommand = (program2, commandName = "logout") => program2.command(commandName).description("Log out of BugBug and clear the user token and current project.").action(() => {
|
|
97
|
+
const mode = resolveOutputMode();
|
|
98
|
+
try {
|
|
99
|
+
const result = logout();
|
|
100
|
+
if (mode === "json") {
|
|
101
|
+
printJson(result);
|
|
102
|
+
} else if (result.cleared) {
|
|
103
|
+
process.stdout.write("Logged out.\n");
|
|
104
|
+
} else {
|
|
105
|
+
process.stdout.write("Already logged out.\n");
|
|
106
|
+
}
|
|
107
|
+
analytics.trackEvent("cli_logged_out", { success: true, outputMode: mode });
|
|
108
|
+
} catch (err) {
|
|
109
|
+
analytics.trackEvent("cli_logged_out", { success: false, outputMode: mode });
|
|
110
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
111
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
var registerAuthCommands = (program2) => {
|
|
115
|
+
registerLoginCommand(program2);
|
|
116
|
+
registerLogoutCommand(program2);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/features/install/install.service.ts
|
|
120
|
+
import { homedir } from "os";
|
|
121
|
+
|
|
122
|
+
// ../core/dist/install/clients.js
|
|
123
|
+
import { join } from "path";
|
|
124
|
+
|
|
125
|
+
// ../core/dist/install/skills.js
|
|
126
|
+
import { existsSync } from "fs";
|
|
127
|
+
var SKILLS_AGENT = {
|
|
128
|
+
windsurf: "windsurf",
|
|
129
|
+
cursor: "cursor",
|
|
130
|
+
vscode: "github-copilot"
|
|
131
|
+
};
|
|
132
|
+
var skillsAddArgv = (agent) => [
|
|
133
|
+
"npx",
|
|
134
|
+
"skills",
|
|
135
|
+
"add",
|
|
136
|
+
"bugbug-io/agent-plugin/skills",
|
|
137
|
+
"--agent",
|
|
138
|
+
agent,
|
|
139
|
+
"--global",
|
|
140
|
+
"--copy",
|
|
141
|
+
"--yes"
|
|
142
|
+
];
|
|
143
|
+
var installSkillsViaCli = async (clientId, finalDir, dryRun = false, options = {}) => {
|
|
144
|
+
const existedBefore = existsSync(finalDir);
|
|
145
|
+
if (dryRun)
|
|
146
|
+
return existedBefore ? "updated" : "created";
|
|
147
|
+
const agent = SKILLS_AGENT[clientId];
|
|
148
|
+
if (!agent) {
|
|
149
|
+
throw new Error(`BugBug skills are not supported for ${clientId}.`);
|
|
150
|
+
}
|
|
151
|
+
const argv = skillsAddArgv(agent);
|
|
152
|
+
const { ok, code } = runClientCommand(argv, options);
|
|
153
|
+
if (!ok) {
|
|
154
|
+
throw new Error(`Failed to install BugBug skills. (exit ${code}).`);
|
|
155
|
+
}
|
|
156
|
+
return existedBefore ? "updated" : "created";
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ../core/dist/install/writers.js
|
|
160
|
+
import { existsSync as existsSync3 } from "fs";
|
|
161
|
+
|
|
162
|
+
// ../core/dist/install/jsonConfig.js
|
|
163
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
164
|
+
import { dirname } from "path";
|
|
165
|
+
var readJsonObject = (path) => {
|
|
166
|
+
if (!existsSync2(path))
|
|
167
|
+
return {};
|
|
168
|
+
try {
|
|
169
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
170
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
171
|
+
} catch {
|
|
172
|
+
return {};
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var writeJsonObject = (path, value) => {
|
|
176
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
177
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
178
|
+
`, "utf8");
|
|
179
|
+
};
|
|
180
|
+
var getObject = (parent, key) => {
|
|
181
|
+
const existing = parent[key];
|
|
182
|
+
return existing && typeof existing === "object" ? existing : {};
|
|
183
|
+
};
|
|
184
|
+
var sameEntry = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
185
|
+
|
|
186
|
+
// ../core/dist/install/writers.js
|
|
187
|
+
var MCP_SERVER_KEY = "bugbug";
|
|
188
|
+
var buildMcpEntry = (shape, endpoint) => {
|
|
189
|
+
if (endpoint.transport === "stdio") {
|
|
190
|
+
const env = {};
|
|
191
|
+
if (endpoint.token)
|
|
192
|
+
env.API_TOKEN = endpoint.token;
|
|
193
|
+
if (endpoint.apiUrl)
|
|
194
|
+
env.BUGBUG_API_URL = endpoint.apiUrl;
|
|
195
|
+
if (endpoint.publicBaseUrl)
|
|
196
|
+
env.MCP_PUBLIC_BASE_URL = endpoint.publicBaseUrl;
|
|
197
|
+
const entry2 = {
|
|
198
|
+
command: "npx",
|
|
199
|
+
args: ["-y", MCP_SERVER_PACKAGE]
|
|
200
|
+
};
|
|
201
|
+
if (Object.keys(env).length > 0)
|
|
202
|
+
entry2.env = env;
|
|
203
|
+
return entry2;
|
|
204
|
+
}
|
|
205
|
+
const entry = { url: endpoint.url };
|
|
206
|
+
if (!shape.omitsHttpType)
|
|
207
|
+
entry.type = "http";
|
|
208
|
+
if (shape.oauthResource)
|
|
209
|
+
entry.oauth_resource = endpoint.url;
|
|
210
|
+
if (endpoint.token) {
|
|
211
|
+
entry.headers = { Authorization: `Bearer ${endpoint.token}` };
|
|
212
|
+
}
|
|
213
|
+
return entry;
|
|
214
|
+
};
|
|
215
|
+
var mergeMcpEntry = (existing, built) => {
|
|
216
|
+
const previous = existing && typeof existing === "object" ? { ...existing } : {};
|
|
217
|
+
if (!("headers" in built))
|
|
218
|
+
delete previous.headers;
|
|
219
|
+
return { ...previous, ...built };
|
|
220
|
+
};
|
|
221
|
+
var writeMcpConfig = (path, shape, endpoint, dryRun = false) => {
|
|
222
|
+
const existed = existsSync3(path);
|
|
223
|
+
const config = readJsonObject(path);
|
|
224
|
+
const servers = getObject(config, shape.serversKey);
|
|
225
|
+
const entry = mergeMcpEntry(servers[MCP_SERVER_KEY], buildMcpEntry(shape, endpoint));
|
|
226
|
+
if (existed && sameEntry(servers[MCP_SERVER_KEY], entry)) {
|
|
227
|
+
return "skipped";
|
|
228
|
+
}
|
|
229
|
+
if (dryRun)
|
|
230
|
+
return existed ? "updated" : "created";
|
|
231
|
+
servers[MCP_SERVER_KEY] = entry;
|
|
232
|
+
config[shape.serversKey] = servers;
|
|
233
|
+
writeJsonObject(path, config);
|
|
234
|
+
return existed ? "updated" : "created";
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// ../core/dist/install/manualPlugin.js
|
|
238
|
+
var RANK = {
|
|
239
|
+
skipped: 0,
|
|
240
|
+
executed: 1,
|
|
241
|
+
unsupported: 2,
|
|
242
|
+
updated: 3,
|
|
243
|
+
created: 4
|
|
244
|
+
};
|
|
245
|
+
var strongest = (a, b) => RANK[a] >= RANK[b] ? a : b;
|
|
246
|
+
var manualPlugin = async (client, ctx, { shape, mcpPath, skillsClient, skillsDir }) => {
|
|
247
|
+
const mcpAction = writeMcpConfig(mcpPath, shape, ctx.endpoint, ctx.dryRun);
|
|
248
|
+
let skillsAction;
|
|
249
|
+
try {
|
|
250
|
+
skillsAction = await installSkillsViaCli(skillsClient, skillsDir, ctx.dryRun, ctx.runnerOptions);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
253
|
+
return {
|
|
254
|
+
client,
|
|
255
|
+
path: `${mcpPath} (mcp: ${mcpAction})`,
|
|
256
|
+
action: "unsupported",
|
|
257
|
+
message: `Wrote MCP config but could not install skills: ${message}`
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
client,
|
|
262
|
+
path: `${mcpPath}, ${skillsDir}`,
|
|
263
|
+
action: strongest(mcpAction, skillsAction)
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// ../core/dist/install/clients.js
|
|
268
|
+
var windsurfMcpPath = (ctx) => join(ctx.home, ".codeium", "windsurf", "mcp_config.json");
|
|
269
|
+
var windsurfSkillsDir = (ctx) => join(ctx.home, ".codeium", "windsurf", "skills");
|
|
270
|
+
var CLIENTS = {
|
|
271
|
+
cursor: {
|
|
272
|
+
id: "cursor",
|
|
273
|
+
displayName: "Cursor",
|
|
274
|
+
cliName: "cursor",
|
|
275
|
+
detectDir: ".cursor",
|
|
276
|
+
// The CLI owns Cursor's skills layout, avoiding the stale, renamed skills a
|
|
277
|
+
// manual copy left behind.
|
|
278
|
+
pluginsTarget: "cursor"
|
|
279
|
+
},
|
|
280
|
+
claude: {
|
|
281
|
+
id: "claude",
|
|
282
|
+
displayName: "Claude Code",
|
|
283
|
+
cliName: "claude",
|
|
284
|
+
detectDir: ".claude",
|
|
285
|
+
pluginsTarget: "claude-code"
|
|
286
|
+
},
|
|
287
|
+
vscode: {
|
|
288
|
+
id: "vscode",
|
|
289
|
+
displayName: "VS Code",
|
|
290
|
+
cliName: "code",
|
|
291
|
+
detectDir: ".vscode",
|
|
292
|
+
// Registers the staged bundle in VS Code's user-level `chat.pluginLocations`.
|
|
293
|
+
// Agent plugins are Preview there, so `chat.plugins.enabled` may be needed.
|
|
294
|
+
pluginsTarget: "vscode"
|
|
295
|
+
},
|
|
296
|
+
codex: {
|
|
297
|
+
id: "codex",
|
|
298
|
+
displayName: "Codex",
|
|
299
|
+
cliName: "codex",
|
|
300
|
+
detectDir: ".codex",
|
|
301
|
+
pluginsTarget: "codex",
|
|
302
|
+
postInstallAuth: {
|
|
303
|
+
command: "codex",
|
|
304
|
+
resetArgs: ["mcp", "logout", "bugbug"],
|
|
305
|
+
args: ["mcp", "login", "bugbug"]
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
copilot: {
|
|
309
|
+
id: "copilot",
|
|
310
|
+
displayName: "GitHub Copilot CLI",
|
|
311
|
+
cliName: "copilot",
|
|
312
|
+
detectDir: ".copilot",
|
|
313
|
+
// Registers the source as a marketplace then installs `plugin@marketplace`,
|
|
314
|
+
// avoiding Copilot's deprecated direct local-path installs.
|
|
315
|
+
pluginsTarget: "github-copilot"
|
|
316
|
+
},
|
|
317
|
+
windsurf: {
|
|
318
|
+
id: "windsurf",
|
|
319
|
+
displayName: "Windsurf",
|
|
320
|
+
detectDir: join(".codeium", "windsurf"),
|
|
321
|
+
// The one client the `plugins` CLI has no target for, so it keeps the
|
|
322
|
+
// hand-written install: the MCP server written into its own config plus the
|
|
323
|
+
// BugBug skills installed via the `skills` CLI. This is also the only path
|
|
324
|
+
// that can carry a bearer token, since delegated clients authenticate
|
|
325
|
+
// through their own MCP OAuth flow.
|
|
326
|
+
installPlugin: (ctx) => manualPlugin("windsurf", ctx, {
|
|
327
|
+
shape: { serversKey: "mcpServers" },
|
|
328
|
+
mcpPath: windsurfMcpPath(ctx),
|
|
329
|
+
skillsClient: "windsurf",
|
|
330
|
+
skillsDir: windsurfSkillsDir(ctx)
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// ../core/dist/install/pluginsCli.js
|
|
336
|
+
var PLUGINS_CLI_ENV = {
|
|
337
|
+
DISABLE_TELEMETRY: "1",
|
|
338
|
+
DO_NOT_TRACK: "1"
|
|
339
|
+
};
|
|
340
|
+
var buildPluginsCliArgv = (target, source) => [
|
|
341
|
+
"npx",
|
|
342
|
+
"-y",
|
|
343
|
+
"plugins",
|
|
344
|
+
"add",
|
|
345
|
+
source,
|
|
346
|
+
"--target",
|
|
347
|
+
target,
|
|
348
|
+
"--yes"
|
|
349
|
+
];
|
|
350
|
+
var runPluginsCli = ({ target, source, dryRun = false, runnerOptions }) => {
|
|
351
|
+
const argv = buildPluginsCliArgv(target, source);
|
|
352
|
+
const command = argv.join(" ");
|
|
353
|
+
if (dryRun)
|
|
354
|
+
return { action: "executed", command };
|
|
355
|
+
const { ok, code } = runClientCommand(argv, {
|
|
356
|
+
...runnerOptions,
|
|
357
|
+
env: { ...PLUGINS_CLI_ENV }
|
|
358
|
+
});
|
|
359
|
+
if (!ok) {
|
|
360
|
+
return {
|
|
361
|
+
action: "unsupported",
|
|
362
|
+
command,
|
|
363
|
+
message: `\`${command}\` failed (exit ${code}).`
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return { action: "executed", command };
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// ../core/dist/install/installPlugin.js
|
|
370
|
+
var runPostInstallAuth = (ctx, auth) => {
|
|
371
|
+
const resetCommand = auth.resetArgs ? [auth.command, ...auth.resetArgs].join(" ") : void 0;
|
|
372
|
+
const argv = [auth.command, ...auth.args];
|
|
373
|
+
const command = argv.join(" ");
|
|
374
|
+
const fullCommand = resetCommand ? `${resetCommand} && ${command}` : command;
|
|
375
|
+
if (ctx.dryRun)
|
|
376
|
+
return { command: fullCommand };
|
|
377
|
+
if (auth.resetArgs) {
|
|
378
|
+
runClientCommand([auth.command, ...auth.resetArgs], { ...ctx.runnerOptions, stdio: "inherit" });
|
|
379
|
+
}
|
|
380
|
+
const { ok, code } = runClientCommand(argv, { ...ctx.runnerOptions, stdio: "inherit" });
|
|
381
|
+
if (!ok) {
|
|
382
|
+
return { command: fullCommand, message: `\`${command}\` failed (exit ${code}).` };
|
|
383
|
+
}
|
|
384
|
+
return { command: fullCommand };
|
|
385
|
+
};
|
|
386
|
+
var installPlugin = (client, ctx) => {
|
|
387
|
+
const definition = CLIENTS[client];
|
|
388
|
+
if (definition.pluginsTarget) {
|
|
389
|
+
const ref = ctx.pluginRef;
|
|
390
|
+
const source = ref?.kind === "local" ? ref.value : PLUGIN_REPO_SOURCE;
|
|
391
|
+
const { action, command, message } = runPluginsCli({
|
|
392
|
+
target: definition.pluginsTarget,
|
|
393
|
+
source,
|
|
394
|
+
dryRun: ctx.dryRun,
|
|
395
|
+
runnerOptions: ctx.runnerOptions
|
|
396
|
+
});
|
|
397
|
+
if (!definition.postInstallAuth || action !== "executed") {
|
|
398
|
+
return { client, path: command, action, message };
|
|
399
|
+
}
|
|
400
|
+
const login2 = runPostInstallAuth(ctx, definition.postInstallAuth);
|
|
401
|
+
return {
|
|
402
|
+
client,
|
|
403
|
+
path: `${command} && ${login2.command}`,
|
|
404
|
+
action,
|
|
405
|
+
message: [message, login2.message].filter(Boolean).join(" ") || void 0
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (!definition.installPlugin) {
|
|
409
|
+
throw new Error(`Client ${client} has neither a plugins target nor an installer.`);
|
|
410
|
+
}
|
|
411
|
+
return definition.installPlugin(ctx);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// ../core/dist/install/pluginRef.js
|
|
415
|
+
var PLUGIN_PACKAGE = PLUGIN_PACKAGE_NAME;
|
|
416
|
+
var resolvePluginRef = () => ({ kind: "package", value: PLUGIN_PACKAGE });
|
|
417
|
+
|
|
418
|
+
// src/utils/toolbox.ts
|
|
419
|
+
var padRight = (value, width) => {
|
|
420
|
+
if (value.length >= width) return value;
|
|
421
|
+
return value + " ".repeat(width - value.length);
|
|
422
|
+
};
|
|
423
|
+
var capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
|
|
424
|
+
var stripTrailingSlash = (value) => value.replace(/\/+$/, "");
|
|
425
|
+
|
|
426
|
+
// src/features/install/install.service.ts
|
|
427
|
+
var userHome = () => homedir();
|
|
428
|
+
var resolveEndpoint = ({
|
|
429
|
+
transport,
|
|
430
|
+
env = process.env,
|
|
431
|
+
token
|
|
432
|
+
}) => {
|
|
433
|
+
const apiUrl = env.BUGBUG_API_URL?.trim() || void 0;
|
|
434
|
+
const publicBaseUrl = env.MCP_PUBLIC_BASE_URL?.trim() || void 0;
|
|
435
|
+
const url = publicBaseUrl ? `${stripTrailingSlash(publicBaseUrl)}/mcp` : DEFAULT_MCP_URL;
|
|
436
|
+
return { transport, url, apiUrl, publicBaseUrl, token };
|
|
437
|
+
};
|
|
438
|
+
var resolveTargetClients = (opts) => {
|
|
439
|
+
if (opts.client) return [opts.client];
|
|
440
|
+
throw new Error("Agent is required. Pass --agent=<name>.");
|
|
441
|
+
};
|
|
442
|
+
var install = async (opts) => {
|
|
443
|
+
const home = opts.home ?? userHome();
|
|
444
|
+
const token = opts.token ?? getCliConfig().token;
|
|
445
|
+
const dryRun = opts.dryRun ?? false;
|
|
446
|
+
const clients = resolveTargetClients({
|
|
447
|
+
client: opts.client,
|
|
448
|
+
home,
|
|
449
|
+
env: opts.env,
|
|
450
|
+
isCliAvailable: opts.isCliAvailable
|
|
451
|
+
});
|
|
452
|
+
const pluginRef = resolvePluginRef();
|
|
453
|
+
const endpoint = resolveEndpoint({ transport: "http", env: opts.env, token });
|
|
454
|
+
return Promise.all(
|
|
455
|
+
clients.map(
|
|
456
|
+
(client) => installPlugin(client, {
|
|
457
|
+
home,
|
|
458
|
+
dryRun,
|
|
459
|
+
runnerOptions: opts.runnerOptions,
|
|
460
|
+
pluginRef,
|
|
461
|
+
endpoint
|
|
462
|
+
})
|
|
463
|
+
)
|
|
464
|
+
);
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// ../core/dist/install/install.types.js
|
|
468
|
+
var CLIENT_IDS = ["cursor", "claude", "vscode", "codex", "windsurf", "copilot"];
|
|
469
|
+
var isKnownClient = (value) => CLIENT_IDS.includes(value);
|
|
470
|
+
|
|
471
|
+
// src/features/install/install.utils.ts
|
|
472
|
+
var AGENT_LIST = CLIENT_IDS.join(", ");
|
|
473
|
+
var validateAgent = (value) => {
|
|
474
|
+
if (isKnownClient(value)) return value;
|
|
475
|
+
const mode = resolveOutputMode();
|
|
476
|
+
emitError(mode === "json" ? "json" : "plain", {
|
|
477
|
+
error: `Unknown agent "${value}". Supported agents: ${AGENT_LIST}.`
|
|
478
|
+
});
|
|
479
|
+
return exitWith(EXIT_USAGE_ERROR);
|
|
480
|
+
};
|
|
481
|
+
var describeResult = (result) => {
|
|
482
|
+
const name = CLIENTS[result.client].displayName;
|
|
483
|
+
switch (result.action) {
|
|
484
|
+
case "created":
|
|
485
|
+
return `\u2713 ${name}: created ${result.path}`;
|
|
486
|
+
case "updated":
|
|
487
|
+
return `\u2713 ${name}: updated ${result.path}`;
|
|
488
|
+
case "executed":
|
|
489
|
+
return `\u2713 ${name}: installed via CLI`;
|
|
490
|
+
case "skipped":
|
|
491
|
+
return `\u2022 ${name}: already configured (${result.path})`;
|
|
492
|
+
case "unsupported":
|
|
493
|
+
return `\u2013 ${name}: ${result.message ?? "not supported"}`;
|
|
494
|
+
default:
|
|
495
|
+
return `${name}: ${result.action}`;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// src/features/install/install.command.ts
|
|
500
|
+
var reportResults = (mode, results, dryRun) => {
|
|
501
|
+
if (mode === "json") {
|
|
502
|
+
printJson({ dryRun, results });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (dryRun) {
|
|
506
|
+
process.stdout.write("Dry run \u2014 no files were written.\n");
|
|
507
|
+
}
|
|
508
|
+
if (results.length === 0) {
|
|
509
|
+
process.stdout.write("No install results.\n");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
for (const result of results) {
|
|
513
|
+
process.stdout.write(`${describeResult(result)}
|
|
514
|
+
`);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
var runInstall = async (opts) => {
|
|
518
|
+
const mode = resolveOutputMode();
|
|
519
|
+
const dryRun = Boolean(opts.dryRun);
|
|
520
|
+
try {
|
|
521
|
+
if (!opts.agent) {
|
|
522
|
+
throw new Error(`Agent is required. Pass --agent=<${AGENT_LIST}>.`);
|
|
523
|
+
}
|
|
524
|
+
const results = await install({ client: opts.agent, dryRun });
|
|
525
|
+
reportResults(mode, results, dryRun);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
528
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
var addCommonInstallOptions = (command) => command.option("--agent <agent>", `Target a single agent: ${AGENT_LIST}`, validateAgent).option("--dry-run", "Show what would be installed without writing any files");
|
|
532
|
+
var registerPluginCommand = (program2, commandName = "plugin") => addCommonInstallOptions(program2.command(commandName)).description("Install the BugBug plugin (skills + MCP) into one AI client.").action((opts) => runInstall(opts));
|
|
533
|
+
var registerMcpAlias = (program2, commandName = "mcp") => addCommonInstallOptions(program2.command(commandName, { hidden: true })).description("Alias for `bugbug plugin`.").action((opts) => runInstall(opts));
|
|
534
|
+
var registerInstallCommands = (program2) => {
|
|
535
|
+
registerPluginCommand(program2);
|
|
536
|
+
registerMcpAlias(program2);
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/utils/render.ts
|
|
540
|
+
var renderOutput = async (mode, value, formatters) => {
|
|
541
|
+
if (mode === "json") {
|
|
542
|
+
printJson(formatters.toJson(value));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
process.stdout.write(`${formatters.toPlain(value)}
|
|
546
|
+
`);
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// src/utils/table.ts
|
|
550
|
+
var renderTable = (rows, columns, options = {}) => {
|
|
551
|
+
if (rows.length === 0) {
|
|
552
|
+
return options.emptyMessage ?? "No results.";
|
|
553
|
+
}
|
|
554
|
+
const cells = rows.map((row) => columns.map((col) => col.get(row) ?? ""));
|
|
555
|
+
const widths = columns.map((col, i) => {
|
|
556
|
+
const dataMax = cells.reduce((max, row) => Math.max(max, row[i]?.length ?? 0), 0);
|
|
557
|
+
const headerMax = col.header.length;
|
|
558
|
+
return Math.max(col.width ?? 0, headerMax, dataMax);
|
|
559
|
+
});
|
|
560
|
+
const formatRow = (values) => values.map((value, i) => padRight(value, widths[i])).join(" ").trimEnd();
|
|
561
|
+
const lines = [];
|
|
562
|
+
lines.push(formatRow(columns.map((c) => c.header.toUpperCase())));
|
|
563
|
+
lines.push(formatRow(widths.map((w) => "\u2500".repeat(w))));
|
|
564
|
+
for (const row of cells) {
|
|
565
|
+
lines.push(formatRow(row));
|
|
566
|
+
}
|
|
567
|
+
return lines.join("\n");
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/features/profiles/profiles.formatters.tsx
|
|
571
|
+
var profilesListOutputFormatters = {
|
|
572
|
+
toPlain(profiles) {
|
|
573
|
+
return renderTable(
|
|
574
|
+
profiles,
|
|
575
|
+
[
|
|
576
|
+
{ header: "Id", get: (p) => p.id },
|
|
577
|
+
{ header: "Name", get: (p) => p.name },
|
|
578
|
+
{ header: "Default", get: (p) => p.isDefault ? "yes" : "" }
|
|
579
|
+
],
|
|
580
|
+
{ emptyMessage: "No profiles found." }
|
|
581
|
+
);
|
|
582
|
+
},
|
|
583
|
+
toJson(profiles) {
|
|
584
|
+
return profiles;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// src/features/profiles/profiles.command.ts
|
|
589
|
+
var registerProfilesListCommand = (parent, commandName = "list") => parent.command(commandName).description("List all available profiles").action(async function() {
|
|
590
|
+
const mode = resolveOutputMode();
|
|
591
|
+
try {
|
|
592
|
+
const data = await listProfiles();
|
|
593
|
+
await renderOutput(mode, data, profilesListOutputFormatters);
|
|
594
|
+
} catch (err) {
|
|
595
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
596
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
var registerProfilesCommand = (program2) => {
|
|
600
|
+
const profiles = program2.command("profiles").description("Profile commands");
|
|
601
|
+
registerProfilesListCommand(profiles, "list");
|
|
602
|
+
return profiles;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// src/features/project/project.command.ts
|
|
606
|
+
var registerProjectInitCommand = (parent, commandName = "init") => parent.command(commandName).description("Initialize a new BugBug project in the current directory.").action(async () => {
|
|
607
|
+
const mode = resolveOutputMode();
|
|
608
|
+
const { token, projectId } = getCliConfig();
|
|
609
|
+
const trimmedToken = token?.trim();
|
|
610
|
+
const trimmedProjectId = projectId?.trim();
|
|
611
|
+
if (!trimmedToken || !trimmedProjectId) {
|
|
612
|
+
emitError(mode, {
|
|
613
|
+
error: "init requires --token and --project-id in non-interactive mode."
|
|
614
|
+
});
|
|
615
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
const result = await initProject(trimmedToken, trimmedProjectId);
|
|
620
|
+
process.stdout.write(`Project initialized at "${result.configFile}"
|
|
621
|
+
`);
|
|
622
|
+
process.stdout.write(`Token saved to "${result.credentialsFile}"
|
|
623
|
+
`);
|
|
624
|
+
} catch (err) {
|
|
625
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
626
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
var registerProjectExportCommand = (parent, commandName = "export") => parent.command(commandName).description("Export project data as a ZIP archive").option(
|
|
630
|
+
"-o, --output <file>",
|
|
631
|
+
"Output file path (defaults to bugbug-project-export-<timestamp>.zip)"
|
|
632
|
+
).action(async function(opts) {
|
|
633
|
+
const mode = resolveOutputMode();
|
|
634
|
+
try {
|
|
635
|
+
const result = await exportProject(opts.output);
|
|
636
|
+
if (mode === "json") {
|
|
637
|
+
printJson(result);
|
|
638
|
+
} else {
|
|
639
|
+
process.stdout.write(`Exported project to ${result.outPath} (${result.size} bytes)
|
|
640
|
+
`);
|
|
641
|
+
}
|
|
642
|
+
} catch (err) {
|
|
643
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
644
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
var registerProjectImportCommand = (parent, commandName = "import") => parent.command(`${commandName} <file>`).description("Import a project from a ZIP archive").action(async function(file) {
|
|
648
|
+
const mode = resolveOutputMode();
|
|
649
|
+
try {
|
|
650
|
+
if (mode === "plain") {
|
|
651
|
+
process.stdout.write(`Importing project from ${file}...
|
|
652
|
+
`);
|
|
653
|
+
}
|
|
654
|
+
const result = await importProject(file);
|
|
655
|
+
if (mode === "json") {
|
|
656
|
+
printJson(result);
|
|
657
|
+
} else {
|
|
658
|
+
process.stdout.write(
|
|
659
|
+
`Project import started from ${result.path} (${result.size} bytes)
|
|
660
|
+
`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
} catch (err) {
|
|
664
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
665
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
var registerProjectCommand = (program2) => {
|
|
669
|
+
const project = program2.command("project").description("Project commands");
|
|
670
|
+
registerProjectInitCommand(project, "init");
|
|
671
|
+
registerProjectExportCommand(project, "export");
|
|
672
|
+
registerProjectImportCommand(project, "import");
|
|
673
|
+
return project;
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// src/utils/existingRunCommand.ts
|
|
677
|
+
var getExistingRunResultAction = async (runId, opts, adapter) => {
|
|
678
|
+
const mode = resolveOutputMode();
|
|
679
|
+
try {
|
|
680
|
+
if (opts.reporter === "junit") {
|
|
681
|
+
const xml = await adapter.getJunitReport(runId);
|
|
682
|
+
const result = writeReportXml(xml, opts.outputPath);
|
|
683
|
+
if (mode === "json") printJson({ runId, kind: adapter.kind, report: result });
|
|
684
|
+
else if (result.outPath) {
|
|
685
|
+
process.stdout.write(
|
|
686
|
+
`Saved JUnit report for ${adapter.runLabel} ${runId} to ${result.outPath} (${result.size} chars)
|
|
687
|
+
`
|
|
688
|
+
);
|
|
689
|
+
} else {
|
|
690
|
+
process.stdout.write(xml);
|
|
691
|
+
if (!xml.endsWith("\n")) process.stdout.write("\n");
|
|
692
|
+
}
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (opts.reporter !== void 0 && opts.reporter !== "inline") {
|
|
696
|
+
throw new Error(`Invalid --reporter: ${opts.reporter}. Expected 'inline' or 'junit'.`);
|
|
697
|
+
}
|
|
698
|
+
const status = await adapter.getRun(runId);
|
|
699
|
+
if (mode === "json") printJson(status);
|
|
700
|
+
else process.stdout.write(formatInlineRunReport(adapter.kind, status));
|
|
701
|
+
const code = exitCodeForStatus(status.status);
|
|
702
|
+
if (code !== 0) exitWith(code);
|
|
703
|
+
} catch (err) {
|
|
704
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
705
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
var stopExistingRunAction = async (runId, adapter) => {
|
|
709
|
+
const mode = resolveOutputMode();
|
|
710
|
+
try {
|
|
711
|
+
const status = await adapter.stopRun(runId);
|
|
712
|
+
if (mode === "json") printJson(status);
|
|
713
|
+
else process.stdout.write(`Status: ${status.status}
|
|
714
|
+
`);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
717
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
var getExistingRunReportAction = async (runId, opts, adapter) => {
|
|
721
|
+
const mode = resolveOutputMode();
|
|
722
|
+
try {
|
|
723
|
+
const xml = await adapter.getJunitReport(runId);
|
|
724
|
+
const result = writeReportXml(xml, opts.output);
|
|
725
|
+
if (mode === "json") {
|
|
726
|
+
printJson({ runId, kind: adapter.kind, ...result });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (result.outPath) {
|
|
730
|
+
process.stdout.write(
|
|
731
|
+
`Saved JUnit report for ${adapter.runLabel} ${runId} to ${result.outPath} (${result.size} chars)
|
|
732
|
+
`
|
|
733
|
+
);
|
|
734
|
+
} else {
|
|
735
|
+
process.stdout.write(xml);
|
|
736
|
+
if (!xml.endsWith("\n")) process.stdout.write("\n");
|
|
737
|
+
}
|
|
738
|
+
} catch (err) {
|
|
739
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
740
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// src/features/suiteRuns/suiteRuns.command.ts
|
|
745
|
+
var SUITE_RUN_ACTION_ADAPTER = {
|
|
746
|
+
kind: "suite",
|
|
747
|
+
runLabel: "suite run",
|
|
748
|
+
getRun: getSuiteRun,
|
|
749
|
+
getJunitReport: getSuiteRunJunitReport,
|
|
750
|
+
stopRun: stopSuiteRun
|
|
751
|
+
};
|
|
752
|
+
var registerSuiteRunShowCommand = (parent, commandName = "show") => parent.command(`${commandName} <runId>`).description("Show suite run details").option("--reporter <name>", "Reporter to use: inline or junit", "inline").option("--output-path <path>", "Path to save the suite report").action(
|
|
753
|
+
(runId, opts) => getExistingRunResultAction(runId, opts, SUITE_RUN_ACTION_ADAPTER)
|
|
754
|
+
);
|
|
755
|
+
var registerSuiteRunStopCommand = (parent, commandName = "stop") => parent.command(`${commandName} <runId>`).description("Stop a suite run").action((runId) => stopExistingRunAction(runId, SUITE_RUN_ACTION_ADAPTER));
|
|
756
|
+
var registerSuiteRunReportCommand = (parent, commandName = "report") => parent.command(`${commandName} <runId>`).description("Get JUnit XML report for a suite run").option("-o, --output <file>", "Output file path (prints to stdout if omitted)").action(
|
|
757
|
+
(runId, opts) => getExistingRunReportAction(runId, opts, SUITE_RUN_ACTION_ADAPTER)
|
|
758
|
+
);
|
|
759
|
+
var registerSuiteRunsCommand = (program2) => {
|
|
760
|
+
const suiteruns = program2.command("suiteruns").description("Suite run commands");
|
|
761
|
+
registerSuiteRunShowCommand(suiteruns, "show");
|
|
762
|
+
registerSuiteRunStopCommand(suiteruns, "stop");
|
|
763
|
+
registerSuiteRunReportCommand(suiteruns, "report");
|
|
764
|
+
return suiteruns;
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
// src/utils/runAction.ts
|
|
768
|
+
import { createElement } from "react";
|
|
769
|
+
var collectRunVariableOption = (val, prev = []) => prev.concat(val);
|
|
770
|
+
var parseTimeoutMinutes = (value) => {
|
|
771
|
+
const minutes = Number(value);
|
|
772
|
+
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
773
|
+
throw new Error(`Invalid --timeout: ${value}. Expected a positive number of minutes.`);
|
|
774
|
+
}
|
|
775
|
+
return minutes * 6e4;
|
|
776
|
+
};
|
|
777
|
+
var runResourceAction = async (resourceId, opts, adapter) => {
|
|
778
|
+
const mode = resolveOutputMode();
|
|
779
|
+
try {
|
|
780
|
+
const reporter = validateReporterOptions(opts);
|
|
781
|
+
const variables = getVariablesMap(opts.variables ?? []);
|
|
782
|
+
const runResult = await adapter.runResource(resourceId, {
|
|
783
|
+
profile: opts.profile,
|
|
784
|
+
variables: Object.keys(variables).length ? variables : void 0
|
|
785
|
+
});
|
|
786
|
+
if (mode === "plain") {
|
|
787
|
+
process.stdout.write(
|
|
788
|
+
`Starting ${adapter.runLabel} for ${adapter.entityLabel} id: ${resourceId}
|
|
789
|
+
`
|
|
790
|
+
);
|
|
791
|
+
if (opts.profile) process.stdout.write(`Using profile: ${opts.profile}
|
|
792
|
+
`);
|
|
793
|
+
if (Object.keys(variables).length) {
|
|
794
|
+
process.stdout.write(`Variables: ${JSON.stringify(variables)}
|
|
795
|
+
`);
|
|
796
|
+
}
|
|
797
|
+
process.stdout.write(`${capitalize(adapter.runLabel)} id: ${runResult.id}
|
|
798
|
+
`);
|
|
799
|
+
}
|
|
800
|
+
if (opts.wait === false) {
|
|
801
|
+
if (mode === "json") {
|
|
802
|
+
printJson(runResult.state);
|
|
803
|
+
}
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
let finalState;
|
|
807
|
+
if (mode === "interactive") {
|
|
808
|
+
let interactiveState;
|
|
809
|
+
let interactiveError;
|
|
810
|
+
const RunDetailsComponent = adapter.kind === "suite" ? BaseSuiteRunDetails : BaseTestRunDetails;
|
|
811
|
+
const { waitUntilExit } = await render(
|
|
812
|
+
createElement(RunDetailsComponent, {
|
|
813
|
+
runId: runResult.id,
|
|
814
|
+
headless: true,
|
|
815
|
+
timeoutMs: opts.timeout,
|
|
816
|
+
onComplete: (status) => {
|
|
817
|
+
interactiveState = status;
|
|
818
|
+
},
|
|
819
|
+
onError: (err) => {
|
|
820
|
+
interactiveError = err;
|
|
821
|
+
}
|
|
822
|
+
})
|
|
823
|
+
);
|
|
824
|
+
await waitUntilExit();
|
|
825
|
+
if (interactiveError) {
|
|
826
|
+
const error = interactiveError;
|
|
827
|
+
throw error;
|
|
828
|
+
}
|
|
829
|
+
if (!interactiveState) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`${capitalize(adapter.runLabel)} ${runResult.id} finished without a final status.`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
finalState = interactiveState;
|
|
835
|
+
} else {
|
|
836
|
+
let lastStatus = "";
|
|
837
|
+
finalState = await adapter.waitForRun(
|
|
838
|
+
runResult.id,
|
|
839
|
+
4e3,
|
|
840
|
+
(status) => {
|
|
841
|
+
if (status.status !== lastStatus) {
|
|
842
|
+
lastStatus = status.status;
|
|
843
|
+
if (mode === "plain") {
|
|
844
|
+
process.stdout.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] status: ${status.status}
|
|
845
|
+
`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
opts.timeout
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
let reportResult;
|
|
853
|
+
if (reporter === "junit") {
|
|
854
|
+
const xml = await adapter.getJunitReport(runResult.id);
|
|
855
|
+
const writeReport = adapter.writeReport ?? writeReportXml;
|
|
856
|
+
reportResult = writeReport(
|
|
857
|
+
xml,
|
|
858
|
+
opts.outputPath ?? defaultJunitReportPath(adapter.kind, runResult.id)
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
if (mode === "json") {
|
|
862
|
+
const payload = reportResult ? { ...finalState, report: { kind: "junit", ...reportResult } } : finalState;
|
|
863
|
+
printJson(payload);
|
|
864
|
+
} else if (mode === "plain" || mode === "interactive") {
|
|
865
|
+
if (reportResult?.outPath) {
|
|
866
|
+
process.stdout.write(
|
|
867
|
+
`Saved JUnit report for ${adapter.runLabel} ${runResult.id} to ${reportResult.outPath} (${reportResult.size} chars)
|
|
868
|
+
`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
if (mode === "plain") {
|
|
872
|
+
if (reporter === "inline") {
|
|
873
|
+
process.stdout.write(formatInlineRunReport(adapter.kind, finalState));
|
|
874
|
+
} else {
|
|
875
|
+
process.stdout.write(`Final status: ${finalState.status}
|
|
876
|
+
`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const code = exitCodeForStatus(finalState.status);
|
|
881
|
+
if (code !== 0) exitWith(code);
|
|
882
|
+
} catch (err) {
|
|
883
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
884
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// src/features/suites/suites.formatters.tsx
|
|
889
|
+
var suitesListOutputFormatters = {
|
|
890
|
+
toPlain(suites) {
|
|
891
|
+
return renderTable(
|
|
892
|
+
suites,
|
|
893
|
+
[
|
|
894
|
+
{ header: "Id", get: (s) => s.id },
|
|
895
|
+
{ header: "Name", get: (s) => s.name ?? "" },
|
|
896
|
+
{ header: "Tests", get: (s) => s.testsCount != null ? String(s.testsCount) : "" }
|
|
897
|
+
],
|
|
898
|
+
{ emptyMessage: "No suites found." }
|
|
899
|
+
);
|
|
900
|
+
},
|
|
901
|
+
toJson(suites) {
|
|
902
|
+
return suites;
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
var suiteDetailsOutputFormatters = {
|
|
906
|
+
toPlain(suite) {
|
|
907
|
+
return renderTable(
|
|
908
|
+
[suite],
|
|
909
|
+
[
|
|
910
|
+
{ header: "Id", get: (s) => s.id },
|
|
911
|
+
{ header: "Name", get: (s) => s.name ?? "" },
|
|
912
|
+
{ header: "Tests", get: (s) => s.testsCount != null ? String(s.testsCount) : "" },
|
|
913
|
+
{ header: "URL", get: (s) => s.webappUrl ?? "" }
|
|
914
|
+
]
|
|
915
|
+
);
|
|
916
|
+
},
|
|
917
|
+
toJson(suite) {
|
|
918
|
+
return suite;
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
// src/features/suites/suites.command.ts
|
|
923
|
+
var registerSuitesListCommand = (parent, commandName = "list") => parent.command(commandName).description("List all available test suites").option("-s, --search <query>", "Filter suites by name").action(async (opts) => {
|
|
924
|
+
const mode = resolveOutputMode();
|
|
925
|
+
try {
|
|
926
|
+
const query = opts.search?.trim() || void 0;
|
|
927
|
+
const data = await listSuites({ query });
|
|
928
|
+
await renderOutput(mode, data, suitesListOutputFormatters);
|
|
929
|
+
} catch (err) {
|
|
930
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
931
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
var registerSuiteShowCommand = (parent, commandName = "show") => parent.command(`${commandName} <suiteId>`).description("Show suite details").action(async (suiteId) => {
|
|
935
|
+
const mode = resolveOutputMode();
|
|
936
|
+
try {
|
|
937
|
+
const data = await getSuite(suiteId);
|
|
938
|
+
await renderOutput(mode, data, suiteDetailsOutputFormatters);
|
|
939
|
+
} catch (err) {
|
|
940
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
941
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
var registerSuiteRunCommand = (parent, commandName = "run") => parent.command(`${commandName} <suiteId>`).description("Execute a test suite by ID").option("-p, --profile <name>", "Profile name to use").option("--reporter <name>", "Reporter to use: inline or junit", "inline").option("--output-path <path>", "Path to save the test report").option(
|
|
945
|
+
"--variable <key=value>",
|
|
946
|
+
"Override variables (repeatable)",
|
|
947
|
+
collectRunVariableOption,
|
|
948
|
+
[]
|
|
949
|
+
).option("--no-wait", "Queue the run and exit without waiting for completion").option("--timeout <minutes>", "Maximum wait time in minutes", parseTimeoutMinutes).action(
|
|
950
|
+
(suiteId, opts) => runResourceAction(
|
|
951
|
+
suiteId,
|
|
952
|
+
{ ...opts, variables: opts.variable ?? [] },
|
|
953
|
+
{
|
|
954
|
+
kind: "suite",
|
|
955
|
+
entityLabel: "suite",
|
|
956
|
+
runLabel: "suite run",
|
|
957
|
+
runResource: runSuite,
|
|
958
|
+
waitForRun: waitForSuiteRun,
|
|
959
|
+
getJunitReport: getSuiteRunJunitReport
|
|
960
|
+
}
|
|
961
|
+
)
|
|
962
|
+
);
|
|
963
|
+
var registerSuitesCommand = (program2) => {
|
|
964
|
+
const suites = program2.command("suites").description("Test suite commands");
|
|
965
|
+
registerSuiteShowCommand(suites, "show");
|
|
966
|
+
registerSuiteRunCommand(suites, "run");
|
|
967
|
+
registerSuitesListCommand(suites, "list");
|
|
968
|
+
return suites;
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
// src/features/testRuns/testRuns.command.ts
|
|
972
|
+
var TEST_RUN_ACTION_ADAPTER = {
|
|
973
|
+
kind: "test",
|
|
974
|
+
runLabel: "test run",
|
|
975
|
+
getRun: getTestRun,
|
|
976
|
+
getJunitReport: getTestRunJunitReport,
|
|
977
|
+
stopRun: stopTestRun
|
|
978
|
+
};
|
|
979
|
+
var registerTestRunLogsCommand = (parent, commandName = "logs") => parent.command(`${commandName} <runId>`).description("Get logs for a test run").action(async (runId) => {
|
|
980
|
+
const mode = resolveOutputMode();
|
|
981
|
+
try {
|
|
982
|
+
const logs = await getTestRunLogs(runId);
|
|
983
|
+
if (mode === "json") {
|
|
984
|
+
printJson({ runId, logs });
|
|
985
|
+
} else {
|
|
986
|
+
if (mode === "plain") process.stdout.write(`Logs for test run ${runId}:
|
|
987
|
+
|
|
988
|
+
`);
|
|
989
|
+
process.stdout.write(logs);
|
|
990
|
+
if (!logs.endsWith("\n")) process.stdout.write("\n");
|
|
991
|
+
}
|
|
992
|
+
} catch (err) {
|
|
993
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
994
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
var registerTestRunShowCommand = (parent, commandName = "show") => parent.command(`${commandName} <runId>`).description("Show test run details").option("--reporter <name>", "Reporter to use: inline or junit", "inline").option("--output-path <path>", "Path to save the test report").action(
|
|
998
|
+
(runId, opts) => getExistingRunResultAction(runId, opts, TEST_RUN_ACTION_ADAPTER)
|
|
999
|
+
);
|
|
1000
|
+
var registerTestRunStopCommand = (parent, commandName = "stop") => parent.command(`${commandName} <runId>`).description("Stop a test run").action((runId) => stopExistingRunAction(runId, TEST_RUN_ACTION_ADAPTER));
|
|
1001
|
+
var registerTestRunReportCommand = (parent, commandName = "report") => parent.command(`${commandName} <runId>`).description("Get JUnit XML report for a test run").option("-o, --output <file>", "Output file path (prints to stdout if omitted)").action(
|
|
1002
|
+
(runId, opts) => getExistingRunReportAction(runId, opts, TEST_RUN_ACTION_ADAPTER)
|
|
1003
|
+
);
|
|
1004
|
+
var registerTestRunsCommand = (program2) => {
|
|
1005
|
+
const testruns = program2.command("testruns").description("Test run commands");
|
|
1006
|
+
registerTestRunShowCommand(testruns, "show");
|
|
1007
|
+
registerTestRunStopCommand(testruns, "stop");
|
|
1008
|
+
registerTestRunLogsCommand(testruns, "logs");
|
|
1009
|
+
registerTestRunReportCommand(testruns, "report");
|
|
1010
|
+
return testruns;
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
// src/features/tests/tests.formatters.tsx
|
|
1014
|
+
var testsListOutputFormatters = {
|
|
1015
|
+
toPlain(tests) {
|
|
1016
|
+
return renderTable(
|
|
1017
|
+
tests,
|
|
1018
|
+
[
|
|
1019
|
+
{ header: "Id", get: (t) => t.id },
|
|
1020
|
+
{ header: "Name", get: (t) => t.name ?? "" }
|
|
1021
|
+
],
|
|
1022
|
+
{ emptyMessage: "No tests found." }
|
|
1023
|
+
);
|
|
1024
|
+
},
|
|
1025
|
+
toJson(tests) {
|
|
1026
|
+
return tests;
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
var testDetailsOutputFormatters = {
|
|
1030
|
+
toPlain(test) {
|
|
1031
|
+
return renderTable(
|
|
1032
|
+
[test],
|
|
1033
|
+
[
|
|
1034
|
+
{ header: "Id", get: (t) => t.id },
|
|
1035
|
+
{ header: "Name", get: (t) => t.name ?? "" },
|
|
1036
|
+
{ header: "URL", get: (t) => t.webappUrl ?? "" }
|
|
1037
|
+
]
|
|
1038
|
+
);
|
|
1039
|
+
},
|
|
1040
|
+
toJson(test) {
|
|
1041
|
+
return test;
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
// src/features/tests/tests.command.ts
|
|
1046
|
+
var registerTestsListCommand = (parent, commandName = "list") => parent.command(commandName).description("List all available tests").option("-s, --search <query>", "Filter tests by name").action(async (opts) => {
|
|
1047
|
+
const mode = resolveOutputMode();
|
|
1048
|
+
try {
|
|
1049
|
+
const query = opts.search?.trim() || void 0;
|
|
1050
|
+
const data = await listTests({ query });
|
|
1051
|
+
await renderOutput(mode, data, testsListOutputFormatters);
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
1054
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
var registerTestShowCommand = (parent, commandName = "show") => parent.command(`${commandName} <testId>`).description("Show test details").action(async (testId) => {
|
|
1058
|
+
const mode = resolveOutputMode();
|
|
1059
|
+
try {
|
|
1060
|
+
const data = await getTest(testId);
|
|
1061
|
+
await renderOutput(mode, data, testDetailsOutputFormatters);
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
1064
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
var registerTestRunCommand = (parent, commandName = "run") => parent.command(`${commandName} <testId>`).description("Execute a test by ID").option("-p, --profile <name>", "Profile name to use").option("--reporter <name>", "Reporter to use: inline or junit", "inline").option("--output-path <path>", "Path to save the test report").option(
|
|
1068
|
+
"--variable <key=value>",
|
|
1069
|
+
"Override variables (repeatable)",
|
|
1070
|
+
collectRunVariableOption,
|
|
1071
|
+
[]
|
|
1072
|
+
).option("--no-wait", "Queue the run and exit without waiting for completion").option("--timeout <minutes>", "Maximum wait time in minutes", parseTimeoutMinutes).action(
|
|
1073
|
+
(testId, opts) => runResourceAction(
|
|
1074
|
+
testId,
|
|
1075
|
+
{ ...opts, variables: opts.variable ?? [] },
|
|
1076
|
+
{
|
|
1077
|
+
kind: "test",
|
|
1078
|
+
entityLabel: "test",
|
|
1079
|
+
runLabel: "test run",
|
|
1080
|
+
runResource: runTest,
|
|
1081
|
+
waitForRun: waitForTestRun,
|
|
1082
|
+
getJunitReport: getTestRunJunitReport
|
|
1083
|
+
}
|
|
1084
|
+
)
|
|
1085
|
+
);
|
|
1086
|
+
var registerTestExportCommand = (parent, commandName = "export") => parent.command(`${commandName} <testId>`).description("Export a single test as YAML or ZIP").option("--format <fmt>", "Output format: yaml or zip", "yaml").option("-o, --output <file>", "Output file path (defaults to bugbug-test-<testId>.<fmt>)").action(async (testId, opts) => {
|
|
1087
|
+
const mode = resolveOutputMode();
|
|
1088
|
+
try {
|
|
1089
|
+
const fmt = (opts.format ?? "yaml").toLowerCase();
|
|
1090
|
+
if (fmt !== "yaml" && fmt !== "zip") {
|
|
1091
|
+
throw new Error(`Invalid --format: ${opts.format}. Expected 'yaml' or 'zip'.`);
|
|
1092
|
+
}
|
|
1093
|
+
const result = await exportTest(testId, fmt, opts.output);
|
|
1094
|
+
if (mode === "json") {
|
|
1095
|
+
printJson(result);
|
|
1096
|
+
} else {
|
|
1097
|
+
process.stdout.write(
|
|
1098
|
+
`Exported test ${testId} to ${result.outPath} (${result.size} ${fmt === "yaml" ? "chars" : "bytes"})
|
|
1099
|
+
`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
1104
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
var registerTestImportCommand = (parent, commandName = "import") => parent.command(`${commandName} <file>`).description("Import a test from a YAML or ZIP file").option("--conflict-mode <mode>", "Conflict handling mode for existing imported resources").action(async (file, opts) => {
|
|
1108
|
+
const mode = resolveOutputMode();
|
|
1109
|
+
try {
|
|
1110
|
+
const fmt = detectFormat(file);
|
|
1111
|
+
if (mode === "plain") {
|
|
1112
|
+
process.stdout.write(`Importing test from ${file} as ${fmt}...
|
|
1113
|
+
`);
|
|
1114
|
+
}
|
|
1115
|
+
const result = await importTest(file, {
|
|
1116
|
+
conflictMode: opts.conflictMode
|
|
1117
|
+
});
|
|
1118
|
+
if (mode === "json") {
|
|
1119
|
+
printJson(result);
|
|
1120
|
+
} else {
|
|
1121
|
+
const id = result.id ?? "(unknown)";
|
|
1122
|
+
const name = result.name ?? "";
|
|
1123
|
+
process.stdout.write(`Imported test: ${id}${name ? ` (${name})` : ""}
|
|
1124
|
+
`);
|
|
1125
|
+
}
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
emitError(mode, { error: err instanceof Error ? err.message : String(err) });
|
|
1128
|
+
exitWith(EXIT_GENERAL_ERROR);
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
var registerTestsCommand = (program2) => {
|
|
1132
|
+
const tests = program2.command("tests").description("Test commands");
|
|
1133
|
+
registerTestShowCommand(tests, "show");
|
|
1134
|
+
registerTestRunCommand(tests, "run");
|
|
1135
|
+
registerTestsListCommand(tests, "list");
|
|
1136
|
+
registerTestExportCommand(tests, "export");
|
|
1137
|
+
registerTestImportCommand(tests, "import");
|
|
1138
|
+
return tests;
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
// src/features/shorthands/shorthands.command.ts
|
|
1142
|
+
var registerRunShorthand = (program2) => {
|
|
1143
|
+
const run = program2.command("run").description("Run tests or suites");
|
|
1144
|
+
registerTestRunCommand(run, "test");
|
|
1145
|
+
registerSuiteRunCommand(run, "suite");
|
|
1146
|
+
return run;
|
|
1147
|
+
};
|
|
1148
|
+
var registerListShorthand = (program2) => {
|
|
1149
|
+
const list = program2.command("list").description("List BugBug resources");
|
|
1150
|
+
registerTestsListCommand(list, "test");
|
|
1151
|
+
registerSuitesListCommand(list, "suite");
|
|
1152
|
+
registerProfilesListCommand(list, "profile");
|
|
1153
|
+
return list;
|
|
1154
|
+
};
|
|
1155
|
+
var registerStopShorthand = (program2) => {
|
|
1156
|
+
const stop = program2.command("stop").description("Stop a running test or suite");
|
|
1157
|
+
registerTestRunStopCommand(stop, "test");
|
|
1158
|
+
registerSuiteRunStopCommand(stop, "suite");
|
|
1159
|
+
return stop;
|
|
1160
|
+
};
|
|
1161
|
+
var registerLogsShorthand = (program2) => {
|
|
1162
|
+
const logs = program2.command("logs").description("Get logs for test runs");
|
|
1163
|
+
registerTestRunLogsCommand(logs, "test");
|
|
1164
|
+
return logs;
|
|
1165
|
+
};
|
|
1166
|
+
var registerExportShorthand = (program2) => {
|
|
1167
|
+
const exportCmd = program2.command("export").description("Export commands");
|
|
1168
|
+
registerProjectExportCommand(exportCmd, "project");
|
|
1169
|
+
registerTestExportCommand(exportCmd, "test");
|
|
1170
|
+
return exportCmd;
|
|
1171
|
+
};
|
|
1172
|
+
var registerImportShorthand = (program2) => {
|
|
1173
|
+
const importCmd = program2.command("import").description("Import commands");
|
|
1174
|
+
registerProjectImportCommand(importCmd, "project");
|
|
1175
|
+
registerTestImportCommand(importCmd, "test");
|
|
1176
|
+
return importCmd;
|
|
1177
|
+
};
|
|
1178
|
+
var registerShorthandCommands = (program2) => {
|
|
1179
|
+
registerProjectInitCommand(program2, "init");
|
|
1180
|
+
registerRunShorthand(program2);
|
|
1181
|
+
registerListShorthand(program2);
|
|
1182
|
+
registerStopShorthand(program2);
|
|
1183
|
+
registerLogsShorthand(program2);
|
|
1184
|
+
registerExportShorthand(program2);
|
|
1185
|
+
registerImportShorthand(program2);
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
// src/utils/command.ts
|
|
1189
|
+
var getFullCommandPath = (command) => {
|
|
1190
|
+
const names = [command.name()];
|
|
1191
|
+
let current = command.parent;
|
|
1192
|
+
while (current) {
|
|
1193
|
+
names.unshift(current.name());
|
|
1194
|
+
current = current.parent;
|
|
1195
|
+
}
|
|
1196
|
+
return names.join(" ");
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
// src/utils/startup.ts
|
|
1200
|
+
var GLOBAL_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set(["-t", "--token", "-p", "--project-id"]);
|
|
1201
|
+
var GLOBAL_FLAGS = /* @__PURE__ */ new Set(["-v", "--verbose", "--disable-telemetry", "--ci", "--json"]);
|
|
1202
|
+
var GLOBAL_FLAGS_WITH_EQUALS_VALUE = ["--token=", "--project-id="];
|
|
1203
|
+
var AUTH_REDIRECT_EXEMPT_TOP_LEVEL_COMMANDS = /* @__PURE__ */ new Set([
|
|
1204
|
+
"init",
|
|
1205
|
+
"plugin",
|
|
1206
|
+
"mcp",
|
|
1207
|
+
"login",
|
|
1208
|
+
"logout"
|
|
1209
|
+
]);
|
|
1210
|
+
var TOP_LEVEL_ROUTES = /* @__PURE__ */ new Set([
|
|
1211
|
+
"tests",
|
|
1212
|
+
"testruns",
|
|
1213
|
+
"suites",
|
|
1214
|
+
"suiteruns",
|
|
1215
|
+
"profiles",
|
|
1216
|
+
"project",
|
|
1217
|
+
"settings",
|
|
1218
|
+
"init"
|
|
1219
|
+
]);
|
|
1220
|
+
var routeWithId = (build) => (id) => {
|
|
1221
|
+
if (!id) throw new Error("Route command requires an id");
|
|
1222
|
+
return build(id);
|
|
1223
|
+
};
|
|
1224
|
+
var RESOURCE_COMMAND_ROUTES = {
|
|
1225
|
+
tests: {
|
|
1226
|
+
list: { args: 0, build: () => "/tests/list" },
|
|
1227
|
+
show: { args: 1, build: routeWithId((id) => `/tests/${id}`) },
|
|
1228
|
+
run: { args: 1, build: routeWithId((id) => `/tests/${id}/run`) },
|
|
1229
|
+
export: { args: 1, build: routeWithId((id) => `/tests/${id}/export`) }
|
|
1230
|
+
},
|
|
1231
|
+
suites: {
|
|
1232
|
+
list: { args: 0, build: () => "/suites/list" },
|
|
1233
|
+
show: { args: 1, build: routeWithId((id) => `/suites/${id}`) },
|
|
1234
|
+
run: { args: 1, build: routeWithId((id) => `/suites/${id}/run`) }
|
|
1235
|
+
},
|
|
1236
|
+
profiles: {
|
|
1237
|
+
list: { args: 0, build: () => "/profiles/list" }
|
|
1238
|
+
},
|
|
1239
|
+
testruns: {
|
|
1240
|
+
show: { args: 1, build: routeWithId((id) => `/testruns/${id}`) }
|
|
1241
|
+
},
|
|
1242
|
+
suiteruns: {
|
|
1243
|
+
show: { args: 1, build: routeWithId((id) => `/suiteruns/${id}`) }
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
var SHORTHAND_COMMAND_ROUTES = {
|
|
1247
|
+
list: {
|
|
1248
|
+
test: { args: 0, build: () => "/tests/list" },
|
|
1249
|
+
suite: { args: 0, build: () => "/suites/list" },
|
|
1250
|
+
profile: { args: 0, build: () => "/profiles/list" }
|
|
1251
|
+
},
|
|
1252
|
+
run: {
|
|
1253
|
+
test: { args: 1, build: routeWithId((id) => `/tests/${id}/run`) },
|
|
1254
|
+
suite: { args: 1, build: routeWithId((id) => `/suites/${id}/run`) }
|
|
1255
|
+
},
|
|
1256
|
+
export: {
|
|
1257
|
+
test: { args: 1, build: routeWithId((id) => `/tests/${id}/export`) },
|
|
1258
|
+
project: { args: 0, build: () => "/project/export" }
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
var getTableRoute = (positionals, routes) => {
|
|
1262
|
+
const [resource, action, id] = positionals;
|
|
1263
|
+
if (!resource || !action) return void 0;
|
|
1264
|
+
const routeCommand = routes[resource]?.[action];
|
|
1265
|
+
if (!routeCommand || positionals.length !== 2 + routeCommand.args) return void 0;
|
|
1266
|
+
return routeCommand.build(id);
|
|
1267
|
+
};
|
|
1268
|
+
var getCommandRoute = (positionals, hasCommandOptions) => {
|
|
1269
|
+
if (hasCommandOptions) return void 0;
|
|
1270
|
+
const [resource, action] = positionals;
|
|
1271
|
+
if (positionals.length === 1 && resource && TOP_LEVEL_ROUTES.has(resource)) {
|
|
1272
|
+
return `/${resource}`;
|
|
1273
|
+
}
|
|
1274
|
+
if (resource === "run" && action && positionals.length === 2) return `/tests/${action}/run`;
|
|
1275
|
+
return getTableRoute(positionals, RESOURCE_COMMAND_ROUTES) ?? getTableRoute(positionals, SHORTHAND_COMMAND_ROUTES);
|
|
1276
|
+
};
|
|
1277
|
+
var parseStartupArgs = (argv) => {
|
|
1278
|
+
const args = argv.slice(2);
|
|
1279
|
+
const hasExplicitHelp = args.includes("--help") || args.includes("-h");
|
|
1280
|
+
const hasExplicitVersion = args.includes("--version") || args.includes("-V");
|
|
1281
|
+
const positionals = [];
|
|
1282
|
+
let forceHelp = false;
|
|
1283
|
+
let forceJson = false;
|
|
1284
|
+
let hasCommandOptions = false;
|
|
1285
|
+
for (let i = 0; i < args.length; i++) {
|
|
1286
|
+
const arg = args[i];
|
|
1287
|
+
if (arg === void 0) continue;
|
|
1288
|
+
if (GLOBAL_FLAGS.has(arg)) {
|
|
1289
|
+
if (arg === "--ci") forceHelp = true;
|
|
1290
|
+
if (arg === "--json") forceJson = true;
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
if (GLOBAL_FLAGS_WITH_VALUE.has(arg)) {
|
|
1294
|
+
i++;
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if (GLOBAL_FLAGS_WITH_EQUALS_VALUE.some((flag) => arg.startsWith(flag))) continue;
|
|
1298
|
+
if (arg.startsWith("-")) {
|
|
1299
|
+
hasCommandOptions = true;
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
positionals.push(arg);
|
|
1303
|
+
}
|
|
1304
|
+
return {
|
|
1305
|
+
positionals,
|
|
1306
|
+
forceHelp,
|
|
1307
|
+
forceJson,
|
|
1308
|
+
hasCommandOptions,
|
|
1309
|
+
hasExplicitHelpOrVersion: hasExplicitHelp || hasExplicitVersion
|
|
1310
|
+
};
|
|
1311
|
+
};
|
|
1312
|
+
var isInteractiveStartup = (args) => Boolean(process.stdout.isTTY) && process.env.CI !== "true" && !args.forceHelp && !args.forceJson;
|
|
1313
|
+
var isAuthRedirectExemptCommand = (positionals) => {
|
|
1314
|
+
const [command, subcommand] = positionals;
|
|
1315
|
+
if (!command) return true;
|
|
1316
|
+
if (AUTH_REDIRECT_EXEMPT_TOP_LEVEL_COMMANDS.has(command)) return true;
|
|
1317
|
+
return command === "project" && subcommand === "init";
|
|
1318
|
+
};
|
|
1319
|
+
var shouldRedirectUnauthenticatedCommandToInit = (argv, authenticated) => {
|
|
1320
|
+
if (authenticated) return false;
|
|
1321
|
+
const args = parseStartupArgs(argv);
|
|
1322
|
+
if (args.hasExplicitHelpOrVersion || !isInteractiveStartup(args)) return false;
|
|
1323
|
+
if (args.positionals.length === 0) return false;
|
|
1324
|
+
if (isAuthRedirectExemptCommand(args.positionals)) return false;
|
|
1325
|
+
return true;
|
|
1326
|
+
};
|
|
1327
|
+
var getStartupMode = (argv) => {
|
|
1328
|
+
const args = parseStartupArgs(argv);
|
|
1329
|
+
if (args.hasExplicitHelpOrVersion) return { type: "commandline" };
|
|
1330
|
+
const interactive = isInteractiveStartup(args);
|
|
1331
|
+
const commandRoute = interactive ? getCommandRoute(args.positionals, args.hasCommandOptions) : void 0;
|
|
1332
|
+
if (commandRoute) return { type: "navigator", route: commandRoute };
|
|
1333
|
+
if (args.positionals.length > 0 || args.forceJson) return { type: "commandline" };
|
|
1334
|
+
if (args.forceHelp || !interactive) return { type: "help" };
|
|
1335
|
+
return { type: "navigator" };
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
// src/index.ts
|
|
1339
|
+
process.on("uncaughtException", async (err) => {
|
|
1340
|
+
captureException(err);
|
|
1341
|
+
await closeSentry();
|
|
1342
|
+
showError(formatErrorMessage(err));
|
|
1343
|
+
process.exit(1);
|
|
1344
|
+
});
|
|
1345
|
+
process.on("unhandledRejection", async (reason) => {
|
|
1346
|
+
captureException(reason);
|
|
1347
|
+
await closeSentry();
|
|
1348
|
+
showError(formatErrorMessage(reason));
|
|
1349
|
+
process.exit(1);
|
|
1350
|
+
});
|
|
1351
|
+
var version = getVersion();
|
|
1352
|
+
var program = new Command();
|
|
1353
|
+
program.name("bugbug").description(`BugBug CLI ${version}`).version(version).showSuggestionAfterError().option("-t, --token <token>", "API token for BugBug (overrides config)").option(
|
|
1354
|
+
"-p, --project-id <projectId>",
|
|
1355
|
+
"Required only for organization tokens in non-interactive mode"
|
|
1356
|
+
).option("-v, --verbose", "Enable verbose logging including HTTP requests").option("--ci", "CI-friendly output (plain logs, no interactive UI, exit 1 on failure)").option("--json", "Emit JSON output (implies non-interactive)").option("--disable-telemetry", "Disable telemetry");
|
|
1357
|
+
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
1358
|
+
const commandName = actionCommand.name();
|
|
1359
|
+
const parent = actionCommand.parent?.name();
|
|
1360
|
+
const fullCommand = getFullCommandPath(actionCommand);
|
|
1361
|
+
setSentryTag("Command", commandName);
|
|
1362
|
+
if (parent && parent !== "bugbug") {
|
|
1363
|
+
setSentryTag("Full command", fullCommand);
|
|
1364
|
+
setSentryTag("Command", parent);
|
|
1365
|
+
}
|
|
1366
|
+
addBreadcrumb(`Run "${fullCommand}" command`, "Command");
|
|
1367
|
+
analytics.trackEvent("cli_command_used", {
|
|
1368
|
+
command: fullCommand
|
|
1369
|
+
});
|
|
1370
|
+
});
|
|
1371
|
+
registerTestsCommand(program);
|
|
1372
|
+
registerTestRunsCommand(program);
|
|
1373
|
+
registerSuitesCommand(program);
|
|
1374
|
+
registerSuiteRunsCommand(program);
|
|
1375
|
+
registerProfilesCommand(program);
|
|
1376
|
+
registerProjectCommand(program);
|
|
1377
|
+
registerAuthCommands(program);
|
|
1378
|
+
registerInstallCommands(program);
|
|
1379
|
+
registerShorthandCommands(program);
|
|
1380
|
+
program.exitOverride();
|
|
1381
|
+
(async () => {
|
|
1382
|
+
try {
|
|
1383
|
+
const config = getCliConfig();
|
|
1384
|
+
const startupMode = getStartupMode(process.argv);
|
|
1385
|
+
analytics.setGlobalEventData({
|
|
1386
|
+
outputMode: config.outputMode,
|
|
1387
|
+
startupMode: startupMode.type
|
|
1388
|
+
});
|
|
1389
|
+
analytics.trackEvent("cli_session_started");
|
|
1390
|
+
if (startupMode.type === "commandline" && shouldRedirectUnauthenticatedCommandToInit(
|
|
1391
|
+
process.argv,
|
|
1392
|
+
Boolean(config.token && config.projectId)
|
|
1393
|
+
)) {
|
|
1394
|
+
const { startNavigator } = await import("./app-A4ZLLFP5.js");
|
|
1395
|
+
await startNavigator({
|
|
1396
|
+
initialRoute: "/init",
|
|
1397
|
+
initialRouteState: {
|
|
1398
|
+
redirectUrl: "/",
|
|
1399
|
+
onlyThisSession: true
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
analytics.trackEvent("cli_session_ended", { success: true });
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
if (startupMode.type === "navigator") {
|
|
1406
|
+
const { startNavigator } = await import("./app-A4ZLLFP5.js");
|
|
1407
|
+
await startNavigator({
|
|
1408
|
+
initialRoute: startupMode.route,
|
|
1409
|
+
token: config.token,
|
|
1410
|
+
projectId: config.projectId
|
|
1411
|
+
});
|
|
1412
|
+
analytics.trackEvent("cli_session_ended", { success: true });
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (startupMode.type === "help") {
|
|
1416
|
+
program.outputHelp();
|
|
1417
|
+
analytics.trackEvent("cli_session_ended", { success: true });
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
await startSentrySpan(
|
|
1421
|
+
{
|
|
1422
|
+
op: "cli.command",
|
|
1423
|
+
name: `bugbug ${startupMode.type}`
|
|
1424
|
+
},
|
|
1425
|
+
async (span) => {
|
|
1426
|
+
span.setAttribute("startup_mode", startupMode.type);
|
|
1427
|
+
await program.parseAsync(process.argv);
|
|
1428
|
+
}
|
|
1429
|
+
);
|
|
1430
|
+
analytics.trackEvent("cli_session_ended", { success: true });
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
analytics.trackEvent("cli_session_ended", { success: false });
|
|
1433
|
+
if (err instanceof CommanderError) {
|
|
1434
|
+
process.exit(err.exitCode);
|
|
1435
|
+
}
|
|
1436
|
+
captureException(err);
|
|
1437
|
+
await closeSentry();
|
|
1438
|
+
showError(formatErrorMessage(err));
|
|
1439
|
+
process.exit(1);
|
|
1440
|
+
}
|
|
1441
|
+
})();
|
|
1442
|
+
//# sourceMappingURL=index.js.map
|