cli / mcp for bitbucket
1import {
2 configureClient,
3 type Result,
4 resolveAuth,
5 resolveWorkspaceAndRepo,
6} from '@bitbucket-tool/core';
7import type { Command } from 'commander';
8
9interface ArgumentConfig {
10 syntax: string;
11 description: string;
12 // biome-ignore lint/suspicious/noExplicitAny: parser can return any type based on command definition
13 parser?: (value: string) => any;
14}
15
16interface OptionConfig {
17 flag: string;
18 description: string;
19 // biome-ignore lint/suspicious/noExplicitAny: option defaults can be any type
20 default?: any;
21}
22
23export interface CommandParams {
24 workspace: string;
25 repoSlug: string;
26 // biome-ignore lint/suspicious/noExplicitAny: args are dynamically typed based on parser functions
27 args: any[];
28 // biome-ignore lint/suspicious/noExplicitAny: options are dynamically typed based on command definition
29 options: Record<string, any>;
30}
31
32interface CommandConfig {
33 name: string;
34 description: string;
35 alias?: string;
36 arguments?: ArgumentConfig[];
37 options?: OptionConfig[];
38 action: (params: CommandParams) => Promise<void>;
39}
40
41export const unwrapOrExit = <T>(result: Result<T>): T => {
42 if (!result.ok) {
43 console.error(`Error: ${result.error.message}`);
44 if (result.error.detail) {
45 console.error(JSON.stringify(result.error.detail, null, 2));
46 }
47 process.exit(1);
48 }
49 return result.data;
50};
51
52const wrapAction = (handler: (params: CommandParams) => Promise<void>) => {
53 // biome-ignore lint/suspicious/noExplicitAny: commander.js passes arguments dynamically
54 return async (...rawArgs: any[]) => {
55 try {
56 const command = rawArgs[rawArgs.length - 1];
57 const options = command.opts();
58 const args = rawArgs.slice(0, -2);
59
60 const { workspace, repoSlug } = resolveWorkspaceAndRepo({
61 workspace: options.workspace,
62 repoSlug: options.repo,
63 });
64
65 const auth = resolveAuth();
66 await configureClient(auth);
67
68 await handler({ workspace, repoSlug, args, options });
69 } catch (error) {
70 console.error('Error:', error instanceof Error ? error.message : String(error));
71 process.exit(1);
72 }
73 };
74};
75
76export const defineCommand = (config: CommandConfig) => {
77 return (program: Command): Command => {
78 const cmd = program.command(config.name).description(config.description);
79
80 if (config.alias) {
81 cmd.alias(config.alias);
82 }
83
84 config.arguments?.forEach((arg) => {
85 cmd.argument(arg.syntax, arg.description, arg.parser);
86 });
87
88 config.options?.forEach((opt) => {
89 if (opt.default !== undefined) {
90 cmd.option(opt.flag, opt.description, opt.default);
91 } else {
92 cmd.option(opt.flag, opt.description);
93 }
94 });
95
96 cmd.option('-w, --workspace <workspace>', 'Bitbucket workspace');
97 cmd.option('-r, --repo <repo>', 'Repository slug');
98
99 cmd.action(wrapAction(config.action));
100
101 return cmd;
102 };
103};