@oasissys/bitbucket-cli 0.1.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.
@@ -0,0 +1,660 @@
1
+ /**
2
+ * oasis-bitbucket — AXI CLI for the Oasis Bitbucket Server (git + pull requests).
3
+ *
4
+ * Shell-invokable equivalent of the `bitbucket` MCP server. Reuses the same
5
+ * BitbucketClient, credential storage, and service config so behaviour never
6
+ * drifts between the two surfaces. Output is TOON on stdout; diagnostics on
7
+ * stderr; exit codes 0/1/2 (success/error/usage).
8
+ *
9
+ * AXI standards: https://toonformat.dev/reference/spec.html
10
+ */
11
+ import { BitbucketClient } from "./client.js";
12
+ import { loadCreds } from "./storage.js";
13
+ import { getServiceBaseUrl } from "./config.js";
14
+ import { EXIT, emit, emitRaw, fail, binPath, parseArgs, flagStr, flagBool, flagInt, flagList, fieldList, resolveColumns, pick, renderSkillDoc, runSetup, runAuth, } from "./axi.js";
15
+ const DESCRIPTION = "Browse Oasis Bitbucket repos, branches, and pull requests, and manage PRs";
16
+ const BIN = "oasis-bitbucket";
17
+ const DIFF_LINES_DEFAULT = 400; // diff lines shown before truncation
18
+ const DESC_TRUNCATE = 600; // chars of a PR description shown in detail views
19
+ // ─── Shared helpers ─────────────────────────────────────────────────
20
+ function getClient() {
21
+ const creds = loadCreds("bitbucket");
22
+ const baseUrl = getServiceBaseUrl("bitbucket", creds?.url);
23
+ if (!creds?.username || !creds?.password || !baseUrl) {
24
+ fail("Bitbucket credentials not configured", `Run \`${BIN} auth --user <u> --password <p>\` to sign in`);
25
+ }
26
+ return new BitbucketClient(baseUrl, creds.username, creds.password);
27
+ }
28
+ /** Parse the `PROJECT/repo` positional into its parts (slugs never contain `/`). */
29
+ function parseRepoRef(token, usage) {
30
+ if (!token)
31
+ fail("a PROJECT/repo reference is required", usage, EXIT.USAGE);
32
+ const slash = token.indexOf("/");
33
+ if (slash === -1) {
34
+ fail(`'${token}' is not a PROJECT/repo reference`, `use PROJECT/repo, e.g. \`${BIN} ${usage.split(" ")[1] ?? "prs"} OCL/common-web-components\``, EXIT.USAGE);
35
+ }
36
+ const projectKey = token.slice(0, slash);
37
+ const repoSlug = token.slice(slash + 1);
38
+ if (!projectKey || !repoSlug)
39
+ fail(`'${token}' is not a valid PROJECT/repo reference`, usage, EXIT.USAGE);
40
+ return { projectKey, repoSlug };
41
+ }
42
+ function requireInt(raw, label, usage) {
43
+ if (!raw)
44
+ fail(`${label} is required`, usage, EXIT.USAGE);
45
+ const n = Number.parseInt(raw, 10);
46
+ if (!Number.isFinite(n) || n <= 0)
47
+ fail(`invalid ${label} '${raw}' (expected a positive number)`, usage, EXIT.USAGE);
48
+ return n;
49
+ }
50
+ /** Translate BitbucketClient errors into an AXI error and exit. */
51
+ function bail(err, ctx) {
52
+ const msg = err instanceof Error ? err.message : String(err);
53
+ const status = /\((\d{3})\)/.exec(msg)?.[1];
54
+ if (status === "401" || status === "403") {
55
+ fail("Bitbucket rejected the credentials (401/403)", `Run \`${BIN} auth --user <u> --password <p>\` to update credentials`);
56
+ }
57
+ if (status === "404") {
58
+ if (ctx.prId !== undefined && ctx.ref) {
59
+ fail(`PR #${ctx.prId} not found in ${ctx.ref.projectKey}/${ctx.ref.repoSlug}`, `Run \`${BIN} prs ${ctx.ref.projectKey}/${ctx.ref.repoSlug}\` to list PRs`);
60
+ }
61
+ if (ctx.ref) {
62
+ fail(`repo '${ctx.ref.projectKey}/${ctx.ref.repoSlug}' not found`, `Run \`${BIN} repos ${ctx.ref.projectKey}\` to list repos in the project`);
63
+ }
64
+ if (ctx.project)
65
+ fail(`project '${ctx.project}' not found or has no repos`);
66
+ }
67
+ // Strip the "Bitbucket API error (NNN): " prefix — keep the actionable message.
68
+ fail(msg.replace(/^Bitbucket API error \(\d+\):\s*/, ""));
69
+ }
70
+ /** Extract `PROJECT/repo` from a PR self-link so home rows are drillable. */
71
+ function repoFromLink(link) {
72
+ if (!link)
73
+ return "";
74
+ const m = /\/projects\/([^/]+)\/repos\/([^/]+)\//.exec(link);
75
+ return m ? `${m[1]}/${m[2]}` : "";
76
+ }
77
+ function reviewerSummary(reviewers) {
78
+ if (reviewers.length === 0)
79
+ return "none";
80
+ const approved = reviewers.filter(r => r.approved).length;
81
+ return `${approved}/${reviewers.length} approved`;
82
+ }
83
+ function truncate(text, limit) {
84
+ const total = text.length;
85
+ if (total <= limit)
86
+ return { text, truncated: false, total };
87
+ return { text: text.slice(0, limit), truncated: true, total };
88
+ }
89
+ function withTimeout(p, ms) {
90
+ return new Promise((resolve, reject) => {
91
+ const timer = setTimeout(() => reject(new Error("timeout")), ms);
92
+ p.then(v => { clearTimeout(timer); resolve(v); }, e => { clearTimeout(timer); reject(e); });
93
+ });
94
+ }
95
+ // ─── Commands ───────────────────────────────────────────────────────
96
+ async function cmdHome() {
97
+ const client = getClient();
98
+ let prs;
99
+ try {
100
+ ({ values: prs } = await client.dashboardPullRequests({ state: "OPEN" }));
101
+ }
102
+ catch (err) {
103
+ bail(err, {});
104
+ }
105
+ const doc = {
106
+ bin: binPath(),
107
+ description: DESCRIPTION,
108
+ server: getServiceBaseUrl("bitbucket", loadCreds("bitbucket")?.url),
109
+ prs: "your open pull requests (as author or reviewer)",
110
+ count: prs.length,
111
+ };
112
+ if (prs.length === 0) {
113
+ doc.open = [];
114
+ emit(doc);
115
+ emitRaw("open: 0 open pull requests involving you");
116
+ emit({ help: [`Run \`${BIN} prs <project>/<repo>\` to list a repo's PRs`] });
117
+ return;
118
+ }
119
+ doc.open = prs.map(pr => ({
120
+ id: pr.id,
121
+ repo: repoFromLink(pr.link),
122
+ title: pr.title,
123
+ reviews: reviewerSummary(pr.reviewers),
124
+ }));
125
+ doc.help = [
126
+ `Run \`${BIN} pr <project>/<repo> <id>\` to see a PR's details`,
127
+ `Run \`${BIN} diff <project>/<repo> <id>\` to review its changes`,
128
+ `Run \`${BIN} prs <project>/<repo>\` to list a repo's PRs`,
129
+ ];
130
+ emit(doc);
131
+ }
132
+ const REPO_FIELDS = ["slug", "name", "state", "cloneUrl"];
133
+ const REPO_FIELDS_DEFAULT = ["slug", "name", "state"];
134
+ const PR_FIELDS = ["id", "title", "author", "reviews", "from", "to", "updated", "url"];
135
+ const PR_FIELDS_DEFAULT = ["id", "title", "author", "reviews"];
136
+ async function cmdRepos(parsed) {
137
+ const project = parsed.positionals[0];
138
+ if (!project)
139
+ fail("project key is required", `${BIN} repos <PROJECT>`, EXIT.USAGE);
140
+ const client = getClient();
141
+ let result;
142
+ try {
143
+ result = await client.listRepos(project);
144
+ }
145
+ catch (err) {
146
+ bail(err, { project });
147
+ }
148
+ if (result.values.length === 0) {
149
+ emit({ project, repos: [] });
150
+ emitRaw(`repos: 0 repositories found in project '${project}'`);
151
+ return;
152
+ }
153
+ const cols = resolveColumns(REPO_FIELDS, REPO_FIELDS_DEFAULT, fieldList(parsed), `${BIN} repos`);
154
+ emit({
155
+ project,
156
+ count: result.values.length,
157
+ complete: result.isLastPage,
158
+ repos: result.values.map(r => pick({ slug: r.slug, name: r.name, state: r.state, cloneUrl: r.cloneUrl ?? "" }, cols)),
159
+ help: [
160
+ `Run \`${BIN} prs ${project}/<repo>\` to list a repo's pull requests`,
161
+ `Run \`${BIN} branches ${project}/<repo>\` to list its branches`,
162
+ ],
163
+ });
164
+ }
165
+ async function cmdBranches(parsed) {
166
+ const ref = parseRepoRef(parsed.positionals[0], `${BIN} branches PROJECT/repo`);
167
+ const client = getClient();
168
+ let result;
169
+ try {
170
+ result = await client.listBranches(ref.projectKey, ref.repoSlug);
171
+ }
172
+ catch (err) {
173
+ bail(err, { ref });
174
+ }
175
+ if (result.values.length === 0) {
176
+ emit({ repo: `${ref.projectKey}/${ref.repoSlug}`, branches: [] });
177
+ emitRaw(`branches: 0 branches found in ${ref.projectKey}/${ref.repoSlug}`);
178
+ return;
179
+ }
180
+ emit({
181
+ repo: `${ref.projectKey}/${ref.repoSlug}`,
182
+ count: result.values.length,
183
+ complete: result.isLastPage,
184
+ branches: result.values.map(b => ({
185
+ name: b.displayId,
186
+ default: b.isDefault,
187
+ commit: b.latestCommit.slice(0, 11),
188
+ })),
189
+ });
190
+ }
191
+ async function cmdPrs(parsed) {
192
+ const ref = parseRepoRef(parsed.positionals[0], `${BIN} prs PROJECT/repo`);
193
+ const state = (flagStr(parsed, "--state") ?? "OPEN").toUpperCase();
194
+ const allowed = ["OPEN", "MERGED", "DECLINED", "ALL"];
195
+ if (!allowed.includes(state)) {
196
+ fail(`invalid --state '${state}'`, `valid values: ${allowed.join(", ")}`, EXIT.USAGE);
197
+ }
198
+ const client = getClient();
199
+ let result;
200
+ try {
201
+ result = await client.listPullRequests(ref.projectKey, ref.repoSlug, state);
202
+ }
203
+ catch (err) {
204
+ bail(err, { ref });
205
+ }
206
+ const repo = `${ref.projectKey}/${ref.repoSlug}`;
207
+ if (result.values.length === 0) {
208
+ emit({ repo, state, pullRequests: [] });
209
+ emitRaw(`pullRequests: 0 ${state.toLowerCase()} pull requests in ${repo}`);
210
+ return;
211
+ }
212
+ const cols = resolveColumns(PR_FIELDS, PR_FIELDS_DEFAULT, fieldList(parsed), `${BIN} prs`);
213
+ emit({
214
+ repo,
215
+ state,
216
+ count: result.values.length,
217
+ complete: result.isLastPage,
218
+ pullRequests: result.values.map(pr => pick({
219
+ id: pr.id,
220
+ title: pr.title,
221
+ author: pr.author,
222
+ reviews: reviewerSummary(pr.reviewers),
223
+ from: pr.fromBranch,
224
+ to: pr.toBranch,
225
+ updated: pr.updatedDate,
226
+ url: pr.link ?? "",
227
+ }, cols)),
228
+ help: [
229
+ `Run \`${BIN} pr ${repo} <id>\` for a PR's details`,
230
+ `Run \`${BIN} diff ${repo} <id>\` to review its changes`,
231
+ ...(cols.length === PR_FIELDS_DEFAULT.length ? [`Run with \`--fields from,to,url\` for more columns`] : []),
232
+ ],
233
+ });
234
+ }
235
+ async function cmdPr(parsed) {
236
+ const ref = parseRepoRef(parsed.positionals[0], `${BIN} pr PROJECT/repo <id>`);
237
+ const prId = requireInt(parsed.positionals[1], "PR id", `${BIN} pr PROJECT/repo <id>`);
238
+ const client = getClient();
239
+ let pr;
240
+ try {
241
+ pr = await client.getPullRequest(ref.projectKey, ref.repoSlug, prId);
242
+ }
243
+ catch (err) {
244
+ bail(err, { ref, prId });
245
+ }
246
+ const repo = `${ref.projectKey}/${ref.repoSlug}`;
247
+ const doc = {
248
+ repo,
249
+ id: pr.id,
250
+ title: pr.title,
251
+ state: pr.state,
252
+ author: pr.author,
253
+ from: pr.fromBranch,
254
+ to: pr.toBranch,
255
+ version: pr.version,
256
+ updated: pr.updatedDate,
257
+ url: pr.link,
258
+ };
259
+ if (pr.reviewers.length > 0) {
260
+ doc.reviewers = pr.reviewers.map(r => ({ user: r.user, status: r.status }));
261
+ }
262
+ if (pr.description) {
263
+ const { text, truncated, total } = truncate(pr.description.trim(), DESC_TRUNCATE);
264
+ doc.description = truncated ? `${text} … (truncated, ${total} chars)` : text;
265
+ }
266
+ // Detail view is self-contained — no help block (§9).
267
+ emit(doc);
268
+ }
269
+ async function cmdDiff(parsed) {
270
+ const ref = parseRepoRef(parsed.positionals[0], `${BIN} diff PROJECT/repo <id>`);
271
+ const prId = requireInt(parsed.positionals[1], "PR id", `${BIN} diff PROJECT/repo <id>`);
272
+ const context = flagInt(parsed, "--context");
273
+ const full = flagBool(parsed, "--full");
274
+ const client = getClient();
275
+ let diff;
276
+ try {
277
+ diff = await client.getPrDiff(ref.projectKey, ref.repoSlug, prId, context);
278
+ }
279
+ catch (err) {
280
+ bail(err, { ref, prId });
281
+ }
282
+ const allLines = diff.replace(/\s+$/, "").split(/\r?\n/);
283
+ const total = allLines.length;
284
+ let shown = allLines;
285
+ let truncated = false;
286
+ if (!full && total > DIFF_LINES_DEFAULT) {
287
+ shown = allLines.slice(0, DIFF_LINES_DEFAULT);
288
+ truncated = true;
289
+ }
290
+ emit({
291
+ repo: `${ref.projectKey}/${ref.repoSlug}`,
292
+ pr: prId,
293
+ lines: truncated ? `${shown.length} of ${total} (head)` : `${total}`,
294
+ });
295
+ emitRaw("");
296
+ emitRaw(shown.join("\n"));
297
+ if (truncated) {
298
+ emitRaw("");
299
+ emit({ help: [`Run \`${BIN} diff ${ref.projectKey}/${ref.repoSlug} ${prId} --full\` for the complete diff`] });
300
+ }
301
+ }
302
+ async function cmdCreatePr(parsed) {
303
+ const ref = parseRepoRef(parsed.positionals[0], `${BIN} create-pr PROJECT/repo --title … --from … --to …`);
304
+ const title = flagStr(parsed, "--title");
305
+ const from = flagStr(parsed, "--from");
306
+ const to = flagStr(parsed, "--to");
307
+ const missing = [
308
+ !title && "--title",
309
+ !from && "--from",
310
+ !to && "--to",
311
+ ].filter(Boolean);
312
+ if (missing.length > 0) {
313
+ fail(`missing required flag(s): ${missing.join(", ")}`, `${BIN} create-pr ${ref.projectKey}/${ref.repoSlug} --title "<t>" --from <branch> --to <branch> [--reviewer <slug>]`, EXIT.USAGE);
314
+ }
315
+ const client = getClient();
316
+ let pr;
317
+ try {
318
+ pr = await client.createPullRequest(ref.projectKey, ref.repoSlug, title, from, to, flagStr(parsed, "--description"), flagList(parsed, "--reviewer"));
319
+ }
320
+ catch (err) {
321
+ bail(err, { ref });
322
+ }
323
+ emit({
324
+ created: `${ref.projectKey}/${ref.repoSlug} #${pr.id}`,
325
+ title: pr.title,
326
+ from: pr.fromBranch,
327
+ to: pr.toBranch,
328
+ url: pr.link,
329
+ help: [`Run \`${BIN} pr ${ref.projectKey}/${ref.repoSlug} ${pr.id}\` to view it`],
330
+ });
331
+ }
332
+ async function cmdMerge(parsed) {
333
+ const ref = parseRepoRef(parsed.positionals[0], `${BIN} merge PROJECT/repo <id>`);
334
+ const prId = requireInt(parsed.positionals[1], "PR id", `${BIN} merge PROJECT/repo <id>`);
335
+ let version = flagInt(parsed, "--version");
336
+ const client = getClient();
337
+ try {
338
+ // Version drives optimistic locking. If omitted, resolve the current one so
339
+ // the agent doesn't need a separate `pr` call first (AXI: avoid follow-ups).
340
+ // Fetching also lets us treat an already-merged PR as a no-op (§6).
341
+ if (version === undefined) {
342
+ const current = await client.getPullRequest(ref.projectKey, ref.repoSlug, prId);
343
+ if (current.state === "MERGED") {
344
+ emit({ merged: `${ref.projectKey}/${ref.repoSlug} #${prId}`, state: "MERGED", note: "already merged (no-op)" });
345
+ return; // exit 0 — desired state already holds
346
+ }
347
+ version = current.version;
348
+ }
349
+ const pr = await client.mergePullRequest(ref.projectKey, ref.repoSlug, prId, version);
350
+ emit({
351
+ merged: `${ref.projectKey}/${ref.repoSlug} #${prId}`,
352
+ title: pr.title,
353
+ state: pr.state,
354
+ into: pr.toBranch,
355
+ });
356
+ }
357
+ catch (err) {
358
+ const msg = err instanceof Error ? err.message : String(err);
359
+ if (/conflict|409|not mergeable|declined|version/i.test(msg)) {
360
+ fail(msg.replace(/^Bitbucket API error \(\d+\):\s*/, ""), `Run \`${BIN} pr ${ref.projectKey}/${ref.repoSlug} ${prId}\` to check its state and current version`);
361
+ }
362
+ bail(err, { ref, prId });
363
+ }
364
+ }
365
+ async function cmdComment(parsed) {
366
+ const ref = parseRepoRef(parsed.positionals[0], `${BIN} comment PROJECT/repo <id> --text …`);
367
+ const prId = requireInt(parsed.positionals[1], "PR id", `${BIN} comment PROJECT/repo <id> --text …`);
368
+ const text = flagStr(parsed, "--text");
369
+ if (!text)
370
+ fail("--text is required", `${BIN} comment ${ref.projectKey}/${ref.repoSlug} ${prId} --text "<msg>"`, EXIT.USAGE);
371
+ const filePath = flagStr(parsed, "--file");
372
+ const line = flagInt(parsed, "--line");
373
+ if (line !== undefined && !filePath) {
374
+ fail("--line requires --file", `${BIN} comment … --file <path> --line <n>`, EXIT.USAGE);
375
+ }
376
+ const lineType = flagStr(parsed, "--line-type");
377
+ const fileType = flagStr(parsed, "--file-type");
378
+ const client = getClient();
379
+ let comment;
380
+ try {
381
+ comment = await client.addPrComment(ref.projectKey, ref.repoSlug, prId, { text, filePath, line, lineType, fileType });
382
+ }
383
+ catch (err) {
384
+ bail(err, { ref, prId });
385
+ }
386
+ emit({
387
+ commented: `${ref.projectKey}/${ref.repoSlug} #${prId}`,
388
+ commentId: comment.id,
389
+ inline: filePath ? `${filePath}${line !== undefined ? `:${line}` : ""}` : undefined,
390
+ });
391
+ }
392
+ // ─── Ambient (SessionStart) ─────────────────────────────────────────
393
+ /**
394
+ * Ambient session-start snapshot (AXI §7): your open PRs. Runs on every
395
+ * session, so it is minimal and NEVER fails loudly — on missing creds or a
396
+ * slow/unreachable server it prints nothing and exits 0.
397
+ */
398
+ async function cmdAmbient() {
399
+ const creds = loadCreds("bitbucket");
400
+ const baseUrl = getServiceBaseUrl("bitbucket", creds?.url);
401
+ if (!creds?.username || !creds?.password || !baseUrl)
402
+ return;
403
+ let prs;
404
+ try {
405
+ const client = new BitbucketClient(baseUrl, creds.username, creds.password);
406
+ ({ values: prs } = await withTimeout(client.dashboardPullRequests({ state: "OPEN" }), 2500));
407
+ }
408
+ catch {
409
+ return;
410
+ }
411
+ if (prs.length === 0)
412
+ return; // nothing actionable — stay quiet
413
+ const bin = binPath();
414
+ emitRaw(`oasis-bitbucket CLI ready — invoke: ${bin} (append \`help\` for all commands)`);
415
+ emit({
416
+ openPrs: prs.length,
417
+ prs: prs.slice(0, 15).map(pr => ({ id: pr.id, repo: repoFromLink(pr.link), title: pr.title, reviews: reviewerSummary(pr.reviewers) })),
418
+ help: [
419
+ `Run \`${bin} pr <project>/<repo> <id>\` to see a PR`,
420
+ `Run \`${bin} diff <project>/<repo> <id>\` to review its changes`,
421
+ ],
422
+ });
423
+ }
424
+ // ─── Help ───────────────────────────────────────────────────────────
425
+ const HELP = {
426
+ auth: [
427
+ `${BIN} auth — configure Bitbucket credentials (stored in ~/.oasis.json, verified first)`,
428
+ "",
429
+ "flags:",
430
+ " --user <u> username",
431
+ " --password <p> password",
432
+ " --clear remove stored credentials",
433
+ "",
434
+ "with no flags: print current auth status",
435
+ "",
436
+ "examples:",
437
+ ` ${BIN} auth`,
438
+ ` ${BIN} auth --user alsaheb --password <pw>`,
439
+ ` ${BIN} auth --clear`,
440
+ ].join("\n"),
441
+ repos: [
442
+ `${BIN} repos <PROJECT> — list repositories in a project`,
443
+ "",
444
+ "flags:",
445
+ " --fields <l> add columns (available: slug,name,state,cloneUrl; default: slug,name,state)",
446
+ "",
447
+ "examples:",
448
+ ` ${BIN} repos OCL`,
449
+ ` ${BIN} repos OCL --fields cloneUrl`,
450
+ ].join("\n"),
451
+ branches: [
452
+ `${BIN} branches <PROJECT/repo> — list branches`,
453
+ "",
454
+ "examples:",
455
+ ` ${BIN} branches OCL/common-web-components`,
456
+ ].join("\n"),
457
+ prs: [
458
+ `${BIN} prs <PROJECT/repo> — list pull requests`,
459
+ "",
460
+ "flags:",
461
+ " --state <s> OPEN (default) | MERGED | DECLINED | ALL",
462
+ " --fields <l> add columns (available: id,title,author,reviews,from,to,updated,url; default: id,title,author,reviews)",
463
+ "",
464
+ "examples:",
465
+ ` ${BIN} prs OCL/common-web-components`,
466
+ ` ${BIN} prs OCL/common-web-components --state MERGED --fields from,to`,
467
+ ].join("\n"),
468
+ pr: [
469
+ `${BIN} pr <PROJECT/repo> <id> — PR details, reviewers, and version (for merge)`,
470
+ "",
471
+ "examples:",
472
+ ` ${BIN} pr OCL/common-web-components 671`,
473
+ ].join("\n"),
474
+ diff: [
475
+ `${BIN} diff <PROJECT/repo> <id> — unified diff (first ${DIFF_LINES_DEFAULT} lines by default)`,
476
+ "",
477
+ "flags:",
478
+ " --context <n> context lines around each change",
479
+ " --full print the entire diff",
480
+ "",
481
+ "examples:",
482
+ ` ${BIN} diff OCL/common-web-components 671`,
483
+ ].join("\n"),
484
+ "create-pr": [
485
+ `${BIN} create-pr <PROJECT/repo> — open a pull request (source branch must be pushed)`,
486
+ "",
487
+ "flags:",
488
+ " --title <t> required",
489
+ " --from <branch> required, source branch",
490
+ " --to <branch> required, target branch",
491
+ " --description <d> optional, markdown",
492
+ " --reviewer <slug> optional, repeatable",
493
+ "",
494
+ "examples:",
495
+ ` ${BIN} create-pr OCL/common-web-components --title "Fix X" --from feature/x --to master --reviewer alsaheb`,
496
+ ].join("\n"),
497
+ merge: [
498
+ `${BIN} merge <PROJECT/repo> <id> — merge an open PR (destructive)`,
499
+ "",
500
+ "flags:",
501
+ " --version <n> optimistic-lock version; auto-resolved from the PR if omitted",
502
+ "",
503
+ "examples:",
504
+ ` ${BIN} merge OCL/common-web-components 671`,
505
+ ].join("\n"),
506
+ comment: [
507
+ `${BIN} comment <PROJECT/repo> <id> — add a PR comment (general or inline)`,
508
+ "",
509
+ "flags:",
510
+ " --text <msg> required, markdown",
511
+ " --file <path> file path for an inline comment",
512
+ " --line <n> line number (requires --file)",
513
+ " --line-type <t> ADDED (default) | REMOVED | CONTEXT",
514
+ " --file-type <t> TO (default) | FROM",
515
+ "",
516
+ "examples:",
517
+ ` ${BIN} comment OCL/common-web-components 671 --text "LGTM"`,
518
+ ` ${BIN} comment OCL/common-web-components 671 --text "typo" --file src/a.ts --line 42`,
519
+ ].join("\n"),
520
+ };
521
+ const SKILL_SPEC = {
522
+ name: "bitbucket-cli",
523
+ binName: BIN,
524
+ description: "Browse Oasis Bitbucket (git server) and manage pull requests from the shell — list repos/branches/PRs, read a PR's details and reviewers, review its diff, create a PR, add comments, and merge. Use when the user asks about Bitbucket repos, branches, pull requests, PR reviews, diffs, or wants to open/comment on/merge a PR on the Oasis git server.",
525
+ intro: `Shell CLI for the Oasis Bitbucket Server (git + pull requests). Auth: \`${BIN} auth --user <u> --password <p>\`.`,
526
+ extra: [
527
+ "## Repo references",
528
+ "",
529
+ "Repos are addressed as `PROJECT/repo` (project key + repository slug), e.g.",
530
+ "`OAS_PLS/oasis-common`. `PROJECT` alone addresses a project.",
531
+ ],
532
+ commands: [
533
+ { cmd: "(no args)", summary: "live view: your open pull requests (author or reviewer)" },
534
+ { cmd: "repos <PROJECT> [--fields <l>]", summary: "list repositories in a project" },
535
+ { cmd: "branches <PROJECT/repo>", summary: "list branches (marks the default)" },
536
+ { cmd: "prs <PROJECT/repo> [--state <s>] [--fields <l>]", summary: "list pull requests (default OPEN)" },
537
+ { cmd: "pr <PROJECT/repo> <id>", summary: "PR details, reviewers, and version (needed to merge)" },
538
+ { cmd: "diff <PROJECT/repo> <id> [--context <n>] [--full]", summary: "unified diff — first 400 lines by default" },
539
+ { cmd: "create-pr <PROJECT/repo> --title <t> --from <b> --to <b> [--reviewer <s>]...", summary: "open a PR (source branch must be pushed)" },
540
+ { cmd: "comment <PROJECT/repo> <id> --text <m> [--file <p> --line <n>]", summary: "add a general or inline PR comment" },
541
+ { cmd: "merge <PROJECT/repo> <id> [--version <n>]", summary: "merge an open PR (version auto-resolved if omitted)" },
542
+ { cmd: "auth [--user <u> --password <p>]", summary: "configure credentials (no flags = status; --clear = sign out)" },
543
+ ],
544
+ examples: [
545
+ "# Review workflow for a PR",
546
+ `${BIN} pr OAS_PLS/oasis-common 681`,
547
+ `${BIN} diff OAS_PLS/oasis-common 681`,
548
+ `${BIN} comment OAS_PLS/oasis-common 681 --text "LGTM"`,
549
+ "",
550
+ "# Open a PR",
551
+ `${BIN} create-pr OAS_PLS/oasis-common --title "Fix X" --from feature/x --to master --reviewer alsaheb`,
552
+ "",
553
+ "# Merge (version fetched automatically)",
554
+ `${BIN} merge OAS_PLS/oasis-common 681`,
555
+ ],
556
+ notes: [
557
+ "`merge` is destructive; version auto-resolves if omitted; already-merged is a no-op.",
558
+ "`reviews` = approval count (e.g. `2/3 approved`). Diffs truncated to 400 lines (`--full`).",
559
+ "Lists default to a minimal schema; `--fields` adds columns.",
560
+ ],
561
+ };
562
+ function topHelp() {
563
+ return [
564
+ `${BIN} — ${DESCRIPTION}`,
565
+ `bin: ${binPath()}`,
566
+ "",
567
+ "commands:",
568
+ " (no args) live view: your open pull requests",
569
+ " repos <PROJECT> list repositories in a project",
570
+ " branches <PROJECT/repo> list branches",
571
+ " prs <PROJECT/repo> list pull requests (--state)",
572
+ " pr <PROJECT/repo> <id> PR details + version",
573
+ " diff <PROJECT/repo> <id> unified diff (head by default)",
574
+ " create-pr <PROJECT/repo> open a PR (--title --from --to)",
575
+ " merge <PROJECT/repo> <id> merge a PR",
576
+ " comment <PROJECT/repo> <id> add a PR comment (--text)",
577
+ " auth configure credentials (--user --password)",
578
+ " setup register ambient context for Codex + OpenCode",
579
+ "",
580
+ `Run \`${BIN} <command> --help\` for flags and examples.`,
581
+ ].join("\n");
582
+ }
583
+ // ─── Dispatch ───────────────────────────────────────────────────────
584
+ const COMMAND_FLAGS = {
585
+ repos: { "--fields": "multi" },
586
+ branches: {},
587
+ prs: { "--state": "value", "--fields": "multi" },
588
+ pr: {},
589
+ diff: { "--context": "value", "--full": "bool" },
590
+ "create-pr": { "--title": "value", "--from": "value", "--to": "value", "--description": "value", "--reviewer": "multi" },
591
+ merge: { "--version": "value" },
592
+ comment: { "--text": "value", "--file": "value", "--line": "value", "--line-type": "value", "--file-type": "value" },
593
+ };
594
+ export async function main() {
595
+ const [, , command, ...rest] = process.argv;
596
+ if (command === "--ambient") {
597
+ await cmdAmbient().catch(() => { });
598
+ return;
599
+ }
600
+ if (command === "--skill-doc") {
601
+ emitRaw(renderSkillDoc(SKILL_SPEC));
602
+ return;
603
+ }
604
+ if (command === "setup") {
605
+ runSetup(BIN);
606
+ return;
607
+ }
608
+ if (command === "auth") {
609
+ if (rest.includes("--help")) {
610
+ emitRaw(HELP.auth);
611
+ return;
612
+ }
613
+ const parsed = parseArgs(rest, { "--user": "value", "--password": "value", "--clear": "bool" }, `${BIN} auth`);
614
+ await runAuth("bitbucket", parsed, {
615
+ binName: BIN,
616
+ label: "Bitbucket",
617
+ verify: async (c) => { await new BitbucketClient(getServiceBaseUrl("bitbucket", c.url), c.username, c.password).getUser(c.username); },
618
+ });
619
+ return;
620
+ }
621
+ if (command === undefined) {
622
+ await cmdHome();
623
+ return;
624
+ }
625
+ if (command === "help" || command === "--help" || command === "-h") {
626
+ const topic = rest[0];
627
+ emitRaw(topic && HELP[topic] ? HELP[topic] : topHelp());
628
+ return;
629
+ }
630
+ if (!(command in COMMAND_FLAGS)) {
631
+ fail(`unknown command '${command}'`, `valid commands: ${Object.keys(COMMAND_FLAGS).join(", ")} (run \`${BIN} help\`)`, EXIT.USAGE);
632
+ }
633
+ const known = COMMAND_FLAGS[command];
634
+ const parsed = parseArgs(rest, known, `${BIN} ${command}`);
635
+ if (flagBool(parsed, "--help")) {
636
+ emitRaw(HELP[command] ?? topHelp());
637
+ return;
638
+ }
639
+ switch (command) {
640
+ case "repos": return cmdRepos(parsed);
641
+ case "branches": return cmdBranches(parsed);
642
+ case "prs": return cmdPrs(parsed);
643
+ case "pr": return cmdPr(parsed);
644
+ case "diff": return cmdDiff(parsed);
645
+ case "create-pr": return cmdCreatePr(parsed);
646
+ case "merge": return cmdMerge(parsed);
647
+ case "comment": return cmdComment(parsed);
648
+ }
649
+ }
650
+ /** Entry used by bin/: run main() and convert any escaped error into an AXI error (§6). */
651
+ export function run() {
652
+ main().catch(err => {
653
+ fail(err instanceof Error ? err.message : String(err));
654
+ });
655
+ }
656
+ /** Single source of truth for skills/bitbucket-cli/SKILL.md (§7). */
657
+ export function skillMarkdown() {
658
+ return renderSkillDoc(SKILL_SPEC);
659
+ }
660
+ //# sourceMappingURL=cli.js.map