@pilatos/bitbucket-cli 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +53 -8
  2. package/dist/index.js +766 -139
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -17440,6 +17440,8 @@ var ServiceTokens = {
17440
17440
  AddDefaultReviewerCommand: "AddDefaultReviewerCommand",
17441
17441
  RemoveDefaultReviewerCommand: "RemoveDefaultReviewerCommand",
17442
17442
  DefaultReviewerService: "DefaultReviewerService",
17443
+ UrlBuilderService: "UrlBuilderService",
17444
+ BrowseCommand: "BrowseCommand",
17443
17445
  CreatePRCommand: "CreatePRCommand",
17444
17446
  ListPRsCommand: "ListPRsCommand",
17445
17447
  ViewPRCommand: "ViewPRCommand",
@@ -17483,6 +17485,7 @@ var ServiceTokens = {
17483
17485
  // src/services/config.service.ts
17484
17486
  import { posix, win32 } from "path";
17485
17487
  import { homedir } from "os";
17488
+ import { randomUUID } from "crypto";
17486
17489
 
17487
17490
  // src/types/errors.ts
17488
17491
  class BBError extends Error {
@@ -17531,6 +17534,12 @@ class APIError extends BBError {
17531
17534
  }
17532
17535
  }
17533
17536
  }
17537
+ function rethrowWithNotFoundContext(error, notFoundMessage) {
17538
+ if (error instanceof APIError && error.statusCode === 404) {
17539
+ throw new APIError(notFoundMessage, 404, error.response, error.context);
17540
+ }
17541
+ throw error;
17542
+ }
17534
17543
 
17535
17544
  class GitError extends BBError {
17536
17545
  command;
@@ -17548,13 +17557,19 @@ class GitError extends BBError {
17548
17557
  }
17549
17558
 
17550
17559
  // src/services/config.service.ts
17560
+ var CONFIG_FILE_MODE = 384;
17561
+ var CONFIG_DIR_MODE = 448;
17562
+ var INSECURE_MODE_MASK = 63;
17563
+
17551
17564
  class ConfigService {
17552
17565
  configDir;
17553
17566
  configFile;
17567
+ platform;
17554
17568
  configCache = null;
17555
17569
  constructor(configDir, options = {}) {
17556
17570
  const platform = options.platform ?? process.platform;
17557
17571
  const joinPath = platform === "win32" ? win32.join : posix.join;
17572
+ this.platform = platform;
17558
17573
  this.configDir = configDir ?? this.resolveDefaultConfigDir({ ...options, platform });
17559
17574
  this.configFile = joinPath(this.configDir, "config.json");
17560
17575
  }
@@ -17575,7 +17590,10 @@ class ConfigService {
17575
17590
  async ensureConfigDir() {
17576
17591
  try {
17577
17592
  const fs = await import("fs/promises");
17578
- await fs.mkdir(this.configDir, { recursive: true });
17593
+ await fs.mkdir(this.configDir, {
17594
+ recursive: true,
17595
+ mode: CONFIG_DIR_MODE
17596
+ });
17579
17597
  } catch (error) {
17580
17598
  throw new BBError({
17581
17599
  code: 4002 /* CONFIG_WRITE_FAILED */,
@@ -17584,12 +17602,36 @@ class ConfigService {
17584
17602
  });
17585
17603
  }
17586
17604
  }
17605
+ async verifyPermissions(path, expectedMode, kind) {
17606
+ if (this.platform === "win32")
17607
+ return;
17608
+ const fs = await import("fs/promises");
17609
+ let stats;
17610
+ try {
17611
+ stats = await fs.stat(path);
17612
+ } catch (error) {
17613
+ if (error.code === "ENOENT")
17614
+ return;
17615
+ throw error;
17616
+ }
17617
+ const mode = stats.mode & 511;
17618
+ if (mode & INSECURE_MODE_MASK) {
17619
+ const actual = mode.toString(8).padStart(3, "0");
17620
+ const expected = expectedMode.toString(8).padStart(3, "0");
17621
+ throw new BBError({
17622
+ code: 4001 /* CONFIG_READ_FAILED */,
17623
+ message: `Config ${kind} has insecure permissions (${actual}); expected ${expected}. Run: chmod ${expected} ${path}`
17624
+ });
17625
+ }
17626
+ }
17587
17627
  async getConfig() {
17588
17628
  if (this.configCache) {
17589
17629
  return this.configCache;
17590
17630
  }
17591
17631
  try {
17592
17632
  const fs = await import("fs/promises");
17633
+ await this.verifyPermissions(this.configDir, CONFIG_DIR_MODE, "directory");
17634
+ await this.verifyPermissions(this.configFile, CONFIG_FILE_MODE, "file");
17593
17635
  const data = await fs.readFile(this.configFile, "utf-8");
17594
17636
  this.configCache = JSON.parse(data);
17595
17637
  return this.configCache;
@@ -17598,6 +17640,9 @@ class ConfigService {
17598
17640
  this.configCache = {};
17599
17641
  return this.configCache;
17600
17642
  }
17643
+ if (error instanceof BBError) {
17644
+ throw error;
17645
+ }
17601
17646
  throw new BBError({
17602
17647
  code: 4001 /* CONFIG_READ_FAILED */,
17603
17648
  message: `Failed to read config file: ${this.configFile}`,
@@ -17607,13 +17652,20 @@ class ConfigService {
17607
17652
  }
17608
17653
  async setConfig(config) {
17609
17654
  await this.ensureConfigDir();
17655
+ const fs = await import("fs/promises");
17656
+ const body = JSON.stringify(config, null, 2);
17657
+ const tmpFile = `${this.configFile}.${randomUUID()}.tmp`;
17610
17658
  try {
17611
- const fs = await import("fs/promises");
17612
- await fs.writeFile(this.configFile, JSON.stringify(config, null, 2), {
17613
- mode: 384
17659
+ await fs.writeFile(tmpFile, body, {
17660
+ mode: CONFIG_FILE_MODE,
17661
+ flag: "wx"
17614
17662
  });
17663
+ await fs.rename(tmpFile, this.configFile);
17615
17664
  this.configCache = config;
17616
17665
  } catch (error) {
17666
+ try {
17667
+ await fs.unlink(tmpFile);
17668
+ } catch {}
17617
17669
  throw new BBError({
17618
17670
  code: 4002 /* CONFIG_WRITE_FAILED */,
17619
17671
  message: `Failed to write config file: ${this.configFile}`,
@@ -17627,7 +17679,7 @@ class ConfigService {
17627
17679
  if (!username || !apiToken) {
17628
17680
  throw new BBError({
17629
17681
  code: 1001 /* AUTH_REQUIRED */,
17630
- message: "Authentication required. Run 'bb auth login' to authenticate."
17682
+ message: "Authentication required. Run 'bb auth login' or set BB_USERNAME and BB_API_TOKEN."
17631
17683
  });
17632
17684
  }
17633
17685
  return { username, apiToken };
@@ -17674,7 +17726,7 @@ class ConfigService {
17674
17726
  if (!oauthAccessToken || !oauthRefreshToken || !oauthExpiresAt) {
17675
17727
  throw new BBError({
17676
17728
  code: 1001 /* AUTH_REQUIRED */,
17677
- message: "OAuth authentication required. Run 'bb auth login' to authenticate."
17729
+ message: "OAuth authentication required. Run 'bb auth login' or set BB_USERNAME and BB_API_TOKEN."
17678
17730
  });
17679
17731
  }
17680
17732
  return {
@@ -17773,6 +17825,9 @@ class GitService {
17773
17825
  async getCurrentBranch() {
17774
17826
  return this.execOrError(["rev-parse", "--abbrev-ref", "HEAD"]);
17775
17827
  }
17828
+ async getCurrentCommit() {
17829
+ return this.execOrError(["rev-parse", "HEAD"]);
17830
+ }
17776
17831
  async getRemoteUrl(remote = "origin") {
17777
17832
  const result = await this.exec(["remote", "get-url", remote]);
17778
17833
  if (result.exitCode !== 0) {
@@ -17797,14 +17852,14 @@ class ContextService {
17797
17852
  this.configService = configService;
17798
17853
  }
17799
17854
  parseRemoteUrl(url) {
17800
- const sshMatch = /git@bitbucket\.org:([^/]+)\/([^.]+)(?:\.git)?/.exec(url);
17855
+ const sshMatch = /^git@bitbucket\.org:([^/\s]+)\/([^/\s.]+)(?:\.git)?$/.exec(url);
17801
17856
  if (sshMatch) {
17802
17857
  return {
17803
17858
  workspace: sshMatch[1],
17804
17859
  repoSlug: sshMatch[2]
17805
17860
  };
17806
17861
  }
17807
- const httpsMatch = /https?:\/\/(?:[^@]+@)?bitbucket\.org\/([^/]+)\/([^/.]+)(?:\.git)?/.exec(url);
17862
+ const httpsMatch = /^https?:\/\/(?:[^@\s]+@)?bitbucket\.org\/([^/\s]+)\/([^/\s.]+)(?:\.git)?$/.exec(url);
17808
17863
  if (httpsMatch) {
17809
17864
  return {
17810
17865
  workspace: httpsMatch[1],
@@ -17814,29 +17869,48 @@ class ContextService {
17814
17869
  return null;
17815
17870
  }
17816
17871
  async getRepoContextFromGit() {
17872
+ const result = await this.inspectGitRepoContext();
17873
+ return result.context;
17874
+ }
17875
+ async inspectGitRepoContext() {
17817
17876
  const isRepo = await this.gitService.isRepository();
17818
17877
  if (!isRepo) {
17819
- return null;
17878
+ return { context: null, reason: "not_a_git_repo", remoteUrl: null };
17820
17879
  }
17880
+ let remoteUrl;
17821
17881
  try {
17822
- const remoteUrl = await this.gitService.getRemoteUrl();
17823
- return this.parseRemoteUrl(remoteUrl);
17882
+ remoteUrl = await this.gitService.getRemoteUrl();
17824
17883
  } catch {
17825
- return null;
17884
+ return { context: null, reason: "no_remote", remoteUrl: null };
17885
+ }
17886
+ const context = this.parseRemoteUrl(remoteUrl);
17887
+ if (!context) {
17888
+ return { context: null, reason: "remote_not_bitbucket", remoteUrl };
17826
17889
  }
17890
+ return { context, reason: null, remoteUrl };
17827
17891
  }
17828
17892
  async getRepoContext(options) {
17893
+ const result = await this.resolveRepoContext(options);
17894
+ return result.context;
17895
+ }
17896
+ async resolveRepoContext(options) {
17829
17897
  if (options.workspace && options.repo) {
17830
17898
  return {
17831
- workspace: options.workspace,
17832
- repoSlug: options.repo
17899
+ context: { workspace: options.workspace, repoSlug: options.repo },
17900
+ reason: null,
17901
+ remoteUrl: null
17833
17902
  };
17834
17903
  }
17835
- const gitContext = await this.getRepoContextFromGit();
17904
+ const gitResult = await this.inspectGitRepoContext();
17905
+ const gitContext = gitResult.context;
17836
17906
  if (options.workspace && gitContext) {
17837
17907
  return {
17838
- workspace: options.workspace,
17839
- repoSlug: gitContext.repoSlug
17908
+ context: {
17909
+ workspace: options.workspace,
17910
+ repoSlug: gitContext.repoSlug
17911
+ },
17912
+ reason: null,
17913
+ remoteUrl: gitResult.remoteUrl
17840
17914
  };
17841
17915
  }
17842
17916
  if (options.repo) {
@@ -17844,22 +17918,40 @@ class ContextService {
17844
17918
  const workspace = gitContext?.workspace || config.defaultWorkspace;
17845
17919
  if (workspace) {
17846
17920
  return {
17847
- workspace,
17848
- repoSlug: options.repo
17921
+ context: { workspace, repoSlug: options.repo },
17922
+ reason: null,
17923
+ remoteUrl: gitResult.remoteUrl
17849
17924
  };
17850
17925
  }
17851
17926
  }
17852
- return gitContext;
17927
+ return gitResult;
17853
17928
  }
17854
17929
  async requireRepoContext(options) {
17855
- const context = await this.getRepoContext(options);
17856
- if (!context) {
17930
+ const result = await this.resolveRepoContext(options);
17931
+ if (!result.context) {
17857
17932
  throw new BBError({
17858
17933
  code: 6001 /* CONTEXT_REPO_NOT_FOUND */,
17859
- message: "Could not determine repository. Use --workspace and --repo options, " + "or run this command from within a Bitbucket repository."
17934
+ message: this.buildRepoNotFoundMessage(result.reason, result.remoteUrl),
17935
+ context: {
17936
+ reason: result.reason ?? "unknown",
17937
+ ...result.remoteUrl ? { remoteUrl: result.remoteUrl } : {}
17938
+ }
17860
17939
  });
17861
17940
  }
17862
- return context;
17941
+ return result.context;
17942
+ }
17943
+ buildRepoNotFoundMessage(reason, remoteUrl) {
17944
+ const fallback = "Use --workspace and --repo options, or run this command from within a Bitbucket repository.";
17945
+ switch (reason) {
17946
+ case "not_a_git_repo":
17947
+ return `Not in a git repository. ${fallback}`;
17948
+ case "no_remote":
17949
+ return `Git repository has no remote configured. Add a Bitbucket remote with \`git remote add origin <url>\`, or ${fallback.charAt(0).toLowerCase()}${fallback.slice(1)}`;
17950
+ case "remote_not_bitbucket":
17951
+ return `Remote ${remoteUrl ? `'${remoteUrl}' ` : ""}is not a Bitbucket URL. ${fallback}`;
17952
+ default:
17953
+ return `Could not determine repository. ${fallback}`;
17954
+ }
17863
17955
  }
17864
17956
  async requireWorkspace(explicit) {
17865
17957
  if (explicit && explicit.length > 0) {
@@ -18397,6 +18489,10 @@ function deepGet(source, path) {
18397
18489
  }
18398
18490
 
18399
18491
  // src/services/output.service.ts
18492
+ var CONTROL_CHARS = /(\x1b\[[0-9;?]*m)|\x1b\[[0-9;?]*[A-Za-ln-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x9B\x9D]/g;
18493
+ function stripControl(value) {
18494
+ return value.replace(CONTROL_CHARS, (_match, sgr) => sgr ?? "");
18495
+ }
18400
18496
  var WRAPPER_ARRAY_KEYS = [
18401
18497
  "pullRequests",
18402
18498
  "repositories",
@@ -18442,36 +18538,38 @@ class OutputService {
18442
18538
  if (rows.length === 0) {
18443
18539
  return;
18444
18540
  }
18445
- const widths = headers.map((header, index) => {
18446
- const maxRowWidth = Math.max(...rows.map((row) => (row[index] || "").length));
18541
+ const sanitizedHeaders = headers.map(stripControl);
18542
+ const sanitizedRows = rows.map((row) => row.map((cell) => stripControl(cell || "")));
18543
+ const widths = sanitizedHeaders.map((header, index) => {
18544
+ const maxRowWidth = Math.max(...sanitizedRows.map((row) => (row[index] || "").length));
18447
18545
  return Math.max(header.length, maxRowWidth);
18448
18546
  });
18449
- const headerRow = headers.map((header, index) => header.padEnd(widths[index])).join(" ");
18547
+ const headerRow = sanitizedHeaders.map((header, index) => header.padEnd(widths[index])).join(" ");
18450
18548
  console.log(this.format(headerRow, source_default.bold));
18451
18549
  console.log(widths.map((width) => "-".repeat(width)).join(" "));
18452
- for (const row of rows) {
18453
- const formattedRow = row.map((cell, index) => (cell || "").padEnd(widths[index])).join(" ");
18550
+ for (const row of sanitizedRows) {
18551
+ const formattedRow = row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
18454
18552
  console.log(formattedRow);
18455
18553
  }
18456
18554
  }
18457
18555
  success(message) {
18458
18556
  const symbol = this.format("\u2713", source_default.green);
18459
- console.log(`${symbol} ${message}`);
18557
+ console.log(`${symbol} ${stripControl(message)}`);
18460
18558
  }
18461
18559
  error(message) {
18462
18560
  const symbol = this.format("\u2717", source_default.red);
18463
- console.error(`${symbol} ${message}`);
18561
+ console.error(`${symbol} ${stripControl(message)}`);
18464
18562
  }
18465
18563
  warning(message) {
18466
18564
  const symbol = this.format("\u26A0", source_default.yellow);
18467
- console.warn(`${symbol} ${message}`);
18565
+ console.warn(`${symbol} ${stripControl(message)}`);
18468
18566
  }
18469
18567
  info(message) {
18470
18568
  const symbol = this.format("\u2139", source_default.blue);
18471
- console.log(`${symbol} ${message}`);
18569
+ console.log(`${symbol} ${stripControl(message)}`);
18472
18570
  }
18473
18571
  text(message) {
18474
- console.log(message);
18572
+ console.log(stripControl(message));
18475
18573
  }
18476
18574
  formatDate(date) {
18477
18575
  const d = typeof date === "string" ? new Date(date) : date;
@@ -22485,6 +22583,17 @@ function getRetryDelay(error, attempt) {
22485
22583
  function sleep(ms) {
22486
22584
  return new Promise((resolve) => setTimeout(resolve, ms));
22487
22585
  }
22586
+ function redactRequestUrl(requestUrl, baseUrl) {
22587
+ const raw = requestUrl ?? "";
22588
+ try {
22589
+ const parsed = new URL(raw, baseUrl);
22590
+ const query = parsed.search ? "?[redacted]" : "";
22591
+ return `${parsed.origin}${parsed.pathname}${query}`;
22592
+ } catch {
22593
+ const queryIdx = raw.indexOf("?");
22594
+ return queryIdx === -1 ? raw : `${raw.slice(0, queryIdx)}?[redacted]`;
22595
+ }
22596
+ }
22488
22597
  function createApiClient(credentialStore, oauthService) {
22489
22598
  const instance = axios_default.create({
22490
22599
  baseURL: BASE_URL,
@@ -22495,7 +22604,7 @@ function createApiClient(credentialStore, oauthService) {
22495
22604
  });
22496
22605
  instance.interceptors.request.use(async (config) => {
22497
22606
  if (process.env.DEBUG === "true") {
22498
- console.debug(`[HTTP] ${config.method?.toUpperCase()} ${config.url}`);
22607
+ console.debug(`[HTTP] ${config.method?.toUpperCase()} ${redactRequestUrl(config.url, config.baseURL)}`);
22499
22608
  }
22500
22609
  const authMethod = await credentialStore.getAuthMethod();
22501
22610
  if (authMethod === "oauth" && oauthService) {
@@ -22558,11 +22667,17 @@ function createApiClient(credentialStore, oauthService) {
22558
22667
  if (error.response) {
22559
22668
  const { status, data } = error.response;
22560
22669
  const message = extractErrorMessage(data) || error.message;
22561
- throw new APIError(message, status, data);
22670
+ const method = error.config?.method?.toUpperCase();
22671
+ const url2 = error.config?.url;
22672
+ throw new APIError(message, status, data, {
22673
+ status,
22674
+ ...method ? { method } : {},
22675
+ ...url2 ? { url: url2 } : {}
22676
+ });
22562
22677
  } else if (error.request) {
22563
22678
  throw new BBError({
22564
22679
  code: 7001 /* NETWORK_ERROR */,
22565
- message: "Network error: Unable to reach Bitbucket API",
22680
+ message: "Network error: Unable to reach Bitbucket API. Run with DEBUG=true for details. If you're behind a proxy or using a custom CA, check your environment.",
22566
22681
  cause: error
22567
22682
  });
22568
22683
  } else {
@@ -22592,13 +22707,15 @@ function extractErrorMessage(data) {
22592
22707
  }
22593
22708
  // src/services/oauth.service.ts
22594
22709
  import { createServer } from "http";
22595
- import { randomBytes } from "crypto";
22710
+ import { createHash, randomBytes } from "crypto";
22596
22711
  var BITBUCKET_AUTHORIZE_URL = "https://bitbucket.org/site/oauth2/authorize";
22597
22712
  var BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token";
22713
+ var CALLBACK_HOST = "127.0.0.1";
22598
22714
  var CALLBACK_PORT = 19872;
22599
22715
  var CALLBACK_PATH = "/callback";
22600
22716
  var CALLBACK_URL = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
22601
22717
  var AUTH_TIMEOUT_MS = 5 * 60 * 1000;
22718
+ var FETCH_TIMEOUT_MS = 1e4;
22602
22719
  var DEFAULT_CLIENT_ID = "ErUBvNmdYtfVHgW6J4";
22603
22720
  var DEFAULT_CLIENT_SECRET = "QnrWypuKXv7YvU7WJwQRza2n2QfHCEw5";
22604
22721
  var OAUTH_SCOPES = [
@@ -22609,7 +22726,36 @@ var OAUTH_SCOPES = [
22609
22726
  "pullrequest:write"
22610
22727
  ].join(" ");
22611
22728
  function generateState() {
22612
- return randomBytes(16).toString("hex");
22729
+ return randomBytes(32).toString("hex");
22730
+ }
22731
+ function base64UrlEncode(buf) {
22732
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
22733
+ }
22734
+ function generatePkcePair() {
22735
+ const verifier = base64UrlEncode(randomBytes(32));
22736
+ const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
22737
+ return { verifier, challenge };
22738
+ }
22739
+ var OAUTH_ERROR_DESCRIPTION_MAX_LENGTH = 200;
22740
+ function extractOAuthErrorDescription(body) {
22741
+ let parsed;
22742
+ try {
22743
+ parsed = JSON.parse(body);
22744
+ } catch {
22745
+ return;
22746
+ }
22747
+ if (typeof parsed !== "object" || parsed === null || !("error_description" in parsed)) {
22748
+ return;
22749
+ }
22750
+ const description = parsed.error_description;
22751
+ if (typeof description !== "string") {
22752
+ return;
22753
+ }
22754
+ const sanitized = description.replace(/\s+/g, " ").trim();
22755
+ if (sanitized.length === 0) {
22756
+ return;
22757
+ }
22758
+ return sanitized.length > OAUTH_ERROR_DESCRIPTION_MAX_LENGTH ? `${sanitized.slice(0, OAUTH_ERROR_DESCRIPTION_MAX_LENGTH)}\u2026` : sanitized;
22613
22759
  }
22614
22760
 
22615
22761
  class OAuthService {
@@ -22622,9 +22768,10 @@ class OAuthService {
22622
22768
  async authorize(clientId, clientSecret) {
22623
22769
  const resolvedClientId = clientId ?? await this.getClientId();
22624
22770
  const state = generateState();
22625
- const authUrl = this.buildAuthUrl(resolvedClientId, state);
22771
+ const { verifier, challenge } = generatePkcePair();
22772
+ const authUrl = this.buildAuthUrl(resolvedClientId, state, challenge);
22626
22773
  const { code } = await this.waitForCallback(authUrl, state);
22627
- const tokenResponse = await this.exchangeCode(code, resolvedClientId, clientSecret);
22774
+ const tokenResponse = await this.exchangeCode(code, resolvedClientId, verifier, clientSecret);
22628
22775
  const expiresAt = Math.floor(Date.now() / 1000) + tokenResponse.expires_in;
22629
22776
  await this.credentialStore.setOAuthCredentials({
22630
22777
  accessToken: tokenResponse.access_token,
@@ -22654,14 +22801,17 @@ class OAuthService {
22654
22801
  "Content-Type": "application/x-www-form-urlencoded",
22655
22802
  Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`
22656
22803
  },
22657
- body: params.toString()
22804
+ body: params.toString(),
22805
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
22658
22806
  });
22659
22807
  if (!response.ok) {
22660
22808
  const errorBody = await response.text();
22809
+ const description = extractOAuthErrorDescription(errorBody);
22810
+ const baseMessage = `Failed to refresh OAuth token. Run 'bb auth login' to re-authenticate.`;
22661
22811
  throw new BBError({
22662
22812
  code: 1003 /* AUTH_EXPIRED */,
22663
- message: `Failed to refresh OAuth token. Run 'bb auth login' to re-authenticate.`,
22664
- context: { status: response.status, body: errorBody }
22813
+ message: description ? `${baseMessage} (${description})` : baseMessage,
22814
+ context: { status: response.status }
22665
22815
  });
22666
22816
  }
22667
22817
  const tokenResponse = await response.json();
@@ -22674,22 +22824,29 @@ class OAuthService {
22674
22824
  return tokenResponse.access_token;
22675
22825
  }
22676
22826
  async revokeToken() {
22677
- try {
22678
- const credentials = await this.credentialStore.getOAuthCredentials();
22679
- const clientId = await this.getClientId();
22680
- const clientSecret = await this.getClientSecret();
22681
- const params = new URLSearchParams({
22682
- token: credentials.accessToken
22683
- });
22684
- await fetch("https://bitbucket.org/site/oauth2/revoke", {
22685
- method: "POST",
22686
- headers: {
22687
- "Content-Type": "application/x-www-form-urlencoded",
22688
- Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`
22689
- },
22690
- body: params.toString()
22827
+ const credentials = await this.credentialStore.getOAuthCredentials();
22828
+ const clientId = await this.getClientId();
22829
+ const clientSecret = await this.getClientSecret();
22830
+ const params = new URLSearchParams({
22831
+ token: credentials.accessToken
22832
+ });
22833
+ const response = await fetch("https://bitbucket.org/site/oauth2/revoke", {
22834
+ method: "POST",
22835
+ headers: {
22836
+ "Content-Type": "application/x-www-form-urlencoded",
22837
+ Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`
22838
+ },
22839
+ body: params.toString(),
22840
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
22841
+ });
22842
+ if (!response.ok) {
22843
+ const errorBody = await response.text().catch(() => "");
22844
+ throw new BBError({
22845
+ code: 7001 /* NETWORK_ERROR */,
22846
+ message: `Failed to revoke OAuth token (HTTP ${response.status}).`,
22847
+ context: { status: response.status, body: errorBody }
22691
22848
  });
22692
- } catch {}
22849
+ }
22693
22850
  }
22694
22851
  async getValidAccessToken() {
22695
22852
  const isExpired = await this.credentialStore.isOAuthTokenExpired();
@@ -22707,13 +22864,15 @@ class OAuthService {
22707
22864
  const customSecret = await this.configService.getValue("oauthClientSecret");
22708
22865
  return customSecret ?? DEFAULT_CLIENT_SECRET;
22709
22866
  }
22710
- buildAuthUrl(clientId, state) {
22867
+ buildAuthUrl(clientId, state, codeChallenge) {
22711
22868
  const params = new URLSearchParams({
22712
22869
  client_id: clientId,
22713
22870
  response_type: "code",
22714
22871
  redirect_uri: CALLBACK_URL,
22715
22872
  scope: OAUTH_SCOPES,
22716
- state
22873
+ state,
22874
+ code_challenge: codeChallenge,
22875
+ code_challenge_method: "S256"
22717
22876
  });
22718
22877
  return `${BITBUCKET_AUTHORIZE_URL}?${params.toString()}`;
22719
22878
  }
@@ -22783,7 +22942,7 @@ class OAuthService {
22783
22942
  }));
22784
22943
  }
22785
22944
  });
22786
- server.listen(CALLBACK_PORT, async () => {
22945
+ server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
22787
22946
  try {
22788
22947
  const open2 = (await Promise.resolve().then(() => (init_open(), exports_open))).default;
22789
22948
  await open2(authUrl);
@@ -22794,12 +22953,13 @@ ${authUrl}
22794
22953
  });
22795
22954
  });
22796
22955
  }
22797
- async exchangeCode(code, clientId, clientSecretOverride) {
22956
+ async exchangeCode(code, clientId, codeVerifier, clientSecretOverride) {
22798
22957
  const clientSecret = clientSecretOverride ?? await this.getClientSecret();
22799
22958
  const params = new URLSearchParams({
22800
22959
  grant_type: "authorization_code",
22801
22960
  code,
22802
- redirect_uri: CALLBACK_URL
22961
+ redirect_uri: CALLBACK_URL,
22962
+ code_verifier: codeVerifier
22803
22963
  });
22804
22964
  const response = await fetch(BITBUCKET_TOKEN_URL, {
22805
22965
  method: "POST",
@@ -22807,21 +22967,25 @@ ${authUrl}
22807
22967
  "Content-Type": "application/x-www-form-urlencoded",
22808
22968
  Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`
22809
22969
  },
22810
- body: params.toString()
22970
+ body: params.toString(),
22971
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
22811
22972
  });
22812
22973
  if (!response.ok) {
22813
22974
  const errorBody = await response.text();
22975
+ const description = extractOAuthErrorDescription(errorBody);
22976
+ const baseMessage = `Failed to exchange authorization code. Please try again.`;
22814
22977
  throw new BBError({
22815
22978
  code: 1002 /* AUTH_INVALID */,
22816
- message: `Failed to exchange authorization code. Please try again.`,
22817
- context: { status: response.status, body: errorBody }
22979
+ message: description ? `${baseMessage} (${description})` : baseMessage,
22980
+ context: { status: response.status }
22818
22981
  });
22819
22982
  }
22820
22983
  return await response.json();
22821
22984
  }
22822
22985
  async fetchUserInfo(accessToken) {
22823
22986
  const response = await fetch("https://api.bitbucket.org/2.0/user", {
22824
- headers: { Authorization: `Bearer ${accessToken}` }
22987
+ headers: { Authorization: `Bearer ${accessToken}` },
22988
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
22825
22989
  });
22826
22990
  if (!response.ok) {
22827
22991
  throw new BBError({
@@ -23217,6 +23381,67 @@ function accountToEntry(account) {
23217
23381
  nickname: asUser.nickname
23218
23382
  };
23219
23383
  }
23384
+ // src/services/url-builder.service.ts
23385
+ var BITBUCKET_WEB_BASE = "https://bitbucket.org";
23386
+ function encodePathSegments(path3) {
23387
+ return path3.split("/").map((segment) => encodeURIComponent(segment)).join("/");
23388
+ }
23389
+
23390
+ class UrlBuilderService {
23391
+ base;
23392
+ constructor(base = BITBUCKET_WEB_BASE) {
23393
+ this.base = base.replace(/\/+$/, "");
23394
+ }
23395
+ repo(ctx) {
23396
+ return this.repoBase(ctx);
23397
+ }
23398
+ src(ctx, branch, path3, line) {
23399
+ const encodedBranch = encodeURIComponent(branch);
23400
+ const trimmedPath = path3?.replace(/^\/+/, "").replace(/\/+$/, "") ?? "";
23401
+ const pathPart = trimmedPath ? `/${encodePathSegments(trimmedPath)}` : "/";
23402
+ const lineFragment = typeof line === "number" && Number.isFinite(line) && line > 0 ? `#lines-${line}` : "";
23403
+ return `${this.repoBase(ctx)}/src/${encodedBranch}${pathPart}${lineFragment}`;
23404
+ }
23405
+ branchList(ctx) {
23406
+ return `${this.repoBase(ctx)}/branches/`;
23407
+ }
23408
+ commit(ctx, sha) {
23409
+ return `${this.repoBase(ctx)}/commits/${encodeURIComponent(sha)}`;
23410
+ }
23411
+ commitList(ctx) {
23412
+ return `${this.repoBase(ctx)}/commits/`;
23413
+ }
23414
+ pullRequest(ctx, id) {
23415
+ return `${this.repoBase(ctx)}/pull-requests/${id}`;
23416
+ }
23417
+ pullRequestList(ctx) {
23418
+ return `${this.repoBase(ctx)}/pull-requests/`;
23419
+ }
23420
+ pipelinesHome(ctx) {
23421
+ return `${this.repoBase(ctx)}/pipelines`;
23422
+ }
23423
+ pipelineRun(ctx, idOrUuid) {
23424
+ return `${this.repoBase(ctx)}/pipelines/results/${encodeURIComponent(idOrUuid)}`;
23425
+ }
23426
+ downloads(ctx) {
23427
+ return `${this.repoBase(ctx)}/downloads/`;
23428
+ }
23429
+ issue(ctx, id) {
23430
+ return `${this.repoBase(ctx)}/issues/${id}`;
23431
+ }
23432
+ issueList(ctx) {
23433
+ return `${this.repoBase(ctx)}/issues`;
23434
+ }
23435
+ wiki(ctx) {
23436
+ return `${this.repoBase(ctx)}/wiki`;
23437
+ }
23438
+ settings(ctx) {
23439
+ return `${this.repoBase(ctx)}/admin`;
23440
+ }
23441
+ repoBase(ctx) {
23442
+ return `${this.base}/${encodeURIComponent(ctx.workspace)}/${encodeURIComponent(ctx.repoSlug)}`;
23443
+ }
23444
+ }
23220
23445
  // src/bootstrap.ts
23221
23446
  import { createRequire } from "module";
23222
23447
 
@@ -27005,13 +27230,32 @@ class BaseCommand {
27005
27230
  }
27006
27231
  requireOption(value, name, message) {
27007
27232
  if (value === undefined || value === null || value === "") {
27233
+ const baseMessage = message || `Option --${name} is required`;
27008
27234
  throw new BBError({
27009
27235
  code: 5001 /* VALIDATION_REQUIRED */,
27010
- message: message || `Option --${name} is required`
27236
+ message: this.appendHelpHint(baseMessage)
27011
27237
  });
27012
27238
  }
27013
27239
  return value;
27014
27240
  }
27241
+ appendHelpHint(message) {
27242
+ const commandPath = this.getCommandPath();
27243
+ const target = commandPath ? `bb ${commandPath} --help` : "bb --help";
27244
+ return `${message} Run \`${target}\` for usage.`;
27245
+ }
27246
+ getCommandPath() {
27247
+ const argv = process.argv.slice(2);
27248
+ const tokens = [];
27249
+ for (const arg of argv) {
27250
+ if (arg.startsWith("-"))
27251
+ break;
27252
+ tokens.push(arg);
27253
+ }
27254
+ if (tokens.length >= 2) {
27255
+ return `${tokens[0]} ${tokens[1]}`;
27256
+ }
27257
+ return tokens.join(" ");
27258
+ }
27015
27259
  parseIntOption(value, name) {
27016
27260
  const parsed = Number.parseInt(value, 10);
27017
27261
  if (Number.isNaN(parsed)) {
@@ -27112,12 +27356,33 @@ class LoginCommand extends BaseCommand {
27112
27356
  this.output.success(`Logged in as ${user.display_name} (${user.username})`);
27113
27357
  } catch (error) {
27114
27358
  await this.credentialStore.clearCredentials();
27115
- throw new BBError({
27116
- code: 1002 /* AUTH_INVALID */,
27117
- message: `Authentication failed: ${error instanceof Error ? error.message : String(error)}`
27118
- });
27359
+ throw this.wrapLoginError(error);
27119
27360
  }
27120
27361
  }
27362
+ wrapLoginError(error) {
27363
+ const detail = error instanceof Error ? error.message : String(error);
27364
+ if (error instanceof APIError) {
27365
+ if (error.statusCode === 401 || error.statusCode === 403) {
27366
+ return new BBError({
27367
+ code: 1002 /* AUTH_INVALID */,
27368
+ message: `Invalid username or token: ${detail}. Verify your Bitbucket username and that the API token is current and has the required scopes.`,
27369
+ cause: error
27370
+ });
27371
+ }
27372
+ if (error.statusCode === 429) {
27373
+ return new BBError({
27374
+ code: 2004 /* API_RATE_LIMITED */,
27375
+ message: `Bitbucket API rate-limited: ${detail}. Wait a moment and try again.`,
27376
+ cause: error
27377
+ });
27378
+ }
27379
+ }
27380
+ return new BBError({
27381
+ code: 1002 /* AUTH_INVALID */,
27382
+ message: `Authentication failed: ${detail}`,
27383
+ cause: error instanceof Error ? error : undefined
27384
+ });
27385
+ }
27121
27386
  }
27122
27387
 
27123
27388
  // src/commands/auth/logout.command.ts
@@ -27133,16 +27398,28 @@ class LogoutCommand extends BaseCommand {
27133
27398
  }
27134
27399
  async execute(_options, context) {
27135
27400
  const authMethod = await this.credentialStore.getAuthMethod();
27401
+ let revokeFailed = false;
27136
27402
  if (authMethod === "oauth") {
27137
- await this.oauthService.revokeToken();
27403
+ try {
27404
+ await this.oauthService.revokeToken();
27405
+ } catch {
27406
+ revokeFailed = true;
27407
+ }
27138
27408
  await this.credentialStore.clearOAuthCredentials();
27139
27409
  } else {
27140
27410
  await this.credentialStore.clearCredentials();
27141
27411
  }
27142
27412
  if (context.globalOptions.json) {
27143
- await this.output.json({ authenticated: false, success: true });
27413
+ await this.output.json({
27414
+ authenticated: false,
27415
+ success: true,
27416
+ revokeFailed: revokeFailed || undefined
27417
+ });
27144
27418
  return;
27145
27419
  }
27420
+ if (revokeFailed) {
27421
+ this.output.warning("Token revocation failed; the access token may still be valid at Bitbucket. Consider revoking it manually.");
27422
+ }
27146
27423
  this.output.success("Logged out of Bitbucket");
27147
27424
  }
27148
27425
  }
@@ -27252,7 +27529,7 @@ class TokenCommand extends BaseCommand {
27252
27529
  if (!credentials.username || !credentials.apiToken) {
27253
27530
  throw new BBError({
27254
27531
  code: 1001 /* AUTH_REQUIRED */,
27255
- message: "Not authenticated. Run 'bb auth login' first."
27532
+ message: "Not authenticated. Run 'bb auth login' or set BB_USERNAME and BB_API_TOKEN."
27256
27533
  });
27257
27534
  }
27258
27535
  const token = Buffer.from(`${credentials.username}:${credentials.apiToken}`).toString("base64");
@@ -27729,7 +28006,7 @@ class ViewRepoCommand extends BaseCommand {
27729
28006
  const response = await this.repositoriesApi.repositoriesWorkspaceRepoSlugGet({
27730
28007
  workspace: repoContext.workspace,
27731
28008
  repoSlug: repoContext.repoSlug
27732
- });
28009
+ }).catch((error) => rethrowWithNotFoundContext(error, `Repository ${repoContext.workspace}/${repoContext.repoSlug} not found.`));
27733
28010
  const repo = response.data;
27734
28011
  if (context.globalOptions.json) {
27735
28012
  await this.output.json(repo);
@@ -27952,7 +28229,7 @@ class CreatePRCommand extends BaseCommand {
27952
28229
  if (!options.title) {
27953
28230
  throw new BBError({
27954
28231
  code: 5001 /* VALIDATION_REQUIRED */,
27955
- message: "Pull request title is required. Use --title option."
28232
+ message: this.appendHelpHint("Pull request title is required. Use --title option.")
27956
28233
  });
27957
28234
  }
27958
28235
  const repoContext = await this.contextService.requireRepoContext({
@@ -28175,7 +28452,7 @@ class ViewPRCommand extends BaseCommand {
28175
28452
  workspace: repoContext.workspace,
28176
28453
  repoSlug: repoContext.repoSlug,
28177
28454
  pullRequestId: prId
28178
- });
28455
+ }).catch((error) => rethrowWithNotFoundContext(error, `Pull request #${prId} not found in ${repoContext.workspace}/${repoContext.repoSlug}.`));
28179
28456
  const pr = response.data;
28180
28457
  if (context.globalOptions.json) {
28181
28458
  await this.output.json(pr);
@@ -28337,8 +28614,9 @@ class EditPRCommand extends BaseCommand {
28337
28614
  try {
28338
28615
  body = fs7.readFileSync(options.bodyFile, "utf-8");
28339
28616
  } catch (err) {
28617
+ const isNotFound = err instanceof Error && err.code === "ENOENT";
28340
28618
  throw new BBError({
28341
- code: 9999 /* UNKNOWN */,
28619
+ code: isNotFound ? 5003 /* FILE_NOT_FOUND */ : 9999 /* UNKNOWN */,
28342
28620
  message: `Failed to read file '${options.bodyFile}': ${err instanceof Error ? err.message : "Unknown error"}`,
28343
28621
  cause: err instanceof Error ? err : undefined,
28344
28622
  context: { bodyFile: options.bodyFile }
@@ -28608,10 +28886,6 @@ class CheckoutPRCommand extends BaseCommand {
28608
28886
  }
28609
28887
 
28610
28888
  // src/commands/pr/diff.command.ts
28611
- import { exec } from "child_process";
28612
- import { promisify as promisify7 } from "util";
28613
- var execAsync = promisify7(exec);
28614
-
28615
28889
  class DiffPRCommand extends BaseCommand {
28616
28890
  pullrequestsApi;
28617
28891
  contextService;
@@ -28722,16 +28996,8 @@ class DiffPRCommand extends BaseCommand {
28722
28996
  }
28723
28997
  async openInBrowser(url2) {
28724
28998
  this.output.info(`Opening ${url2} in your browser...`);
28725
- const platform2 = process.platform;
28726
- let command;
28727
- if (platform2 === "darwin") {
28728
- command = `open "${url2}"`;
28729
- } else if (platform2 === "win32") {
28730
- command = `start "" "${url2}"`;
28731
- } else {
28732
- command = `xdg-open "${url2}"`;
28733
- }
28734
- await execAsync(command);
28999
+ const open2 = (await Promise.resolve().then(() => (init_open(), exports_open))).default;
29000
+ await open2(url2);
28735
29001
  }
28736
29002
  async getWebDiffUrl(workspace, repoSlug, prId) {
28737
29003
  const prResponse = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsPullRequestIdGet({
@@ -28961,11 +29227,11 @@ class ActivityPRCommand extends BaseCommand {
28961
29227
  return activity.type ? activity.type.toLowerCase() : "activity";
28962
29228
  }
28963
29229
  getActorName(activity) {
28964
- const user = activity.comment?.user ?? activity.comment?.author ?? activity.approval?.user ?? activity.update?.author ?? activity.changes_requested?.user ?? activity.merge?.user ?? activity.decline?.user ?? activity.commit?.author?.user ?? activity.user;
29230
+ const user = activity.comment?.user ?? activity.comment?.author ?? activity.approval?.user ?? activity.changes_requested?.user ?? activity.merge?.user ?? activity.decline?.user ?? activity.commit?.author?.user ?? activity.update?.author ?? activity.user;
28965
29231
  return getUserDisplayName(user) ?? "Unknown";
28966
29232
  }
28967
29233
  formatActivityDate(activity) {
28968
- const date = activity.comment?.created_on ?? activity.approval?.date ?? activity.update?.date ?? activity.changes_requested?.date ?? activity.merge?.date ?? activity.decline?.date ?? activity.commit?.date;
29234
+ const date = activity.comment?.created_on ?? activity.approval?.date ?? activity.changes_requested?.date ?? activity.merge?.date ?? activity.decline?.date ?? activity.commit?.date ?? activity.update?.date;
28969
29235
  if (!date) {
28970
29236
  return "-";
28971
29237
  }
@@ -29034,16 +29300,23 @@ class CommentPRCommand extends BaseCommand {
29034
29300
  this.contextService = contextService;
29035
29301
  }
29036
29302
  async execute(options, context) {
29303
+ const validModesNote = "Valid modes: (1) general comment (no flags); (2) file-level comment (--file + --line-to or --line-from); (3) inline comment with line range (--file + --line-from + --line-to).";
29037
29304
  if ((options.lineTo || options.lineFrom) && !options.file) {
29038
29305
  throw new BBError({
29039
29306
  code: 5001 /* VALIDATION_REQUIRED */,
29040
- message: "--file is required when using --line-to or --line-from"
29307
+ message: this.appendHelpHint(`--file is required when using --line-to or --line-from. ${validModesNote}`),
29308
+ context: {
29309
+ validModes: ["general", "file", "inline"]
29310
+ }
29041
29311
  });
29042
29312
  }
29043
29313
  if (options.file && !options.lineTo && !options.lineFrom) {
29044
29314
  throw new BBError({
29045
29315
  code: 5001 /* VALIDATION_REQUIRED */,
29046
- message: "At least one of --line-to or --line-from is required when using --file"
29316
+ message: this.appendHelpHint(`At least one of --line-to or --line-from is required when using --file. ${validModesNote}`),
29317
+ context: {
29318
+ validModes: ["general", "file", "inline"]
29319
+ }
29047
29320
  });
29048
29321
  }
29049
29322
  if (options.lineTo) {
@@ -29575,13 +29848,13 @@ class ViewSnippetCommand extends BaseCommand {
29575
29848
  const response = await this.snippetsApi.snippetsWorkspaceEncodedIdGet({
29576
29849
  workspace,
29577
29850
  encodedId: options.id
29578
- });
29851
+ }).catch((error) => rethrowWithNotFoundContext(error, `Snippet ${options.id} not found in workspace ${workspace}.`));
29579
29852
  const snippet = response.data;
29580
29853
  const fileNames = this.extractFileNames(snippet);
29581
29854
  if (options.file !== undefined) {
29582
29855
  if (!fileNames.includes(options.file)) {
29583
29856
  throw new BBError({
29584
- code: 5002 /* VALIDATION_INVALID */,
29857
+ code: 5003 /* FILE_NOT_FOUND */,
29585
29858
  message: `File not found in snippet: ${options.file}`,
29586
29859
  context: { file: options.file, available: fileNames }
29587
29860
  });
@@ -29689,7 +29962,7 @@ class CreateSnippetCommand extends BaseCommand {
29689
29962
  for (const filePath of options.file) {
29690
29963
  if (!fs8.existsSync(filePath)) {
29691
29964
  throw new BBError({
29692
- code: 5002 /* VALIDATION_INVALID */,
29965
+ code: 5003 /* FILE_NOT_FOUND */,
29693
29966
  message: `File not found: ${filePath}`,
29694
29967
  context: { file: filePath }
29695
29968
  });
@@ -29748,7 +30021,7 @@ class EditSnippetCommand extends BaseCommand {
29748
30021
  for (const filePath of options.file) {
29749
30022
  if (!fs9.existsSync(filePath)) {
29750
30023
  throw new BBError({
29751
- code: 5002 /* VALIDATION_INVALID */,
30024
+ code: 5003 /* FILE_NOT_FOUND */,
29752
30025
  message: `File not found: ${filePath}`,
29753
30026
  context: { file: filePath }
29754
30027
  });
@@ -30052,7 +30325,7 @@ class GetConfigCommand extends BaseCommand {
30052
30325
  if (GetConfigCommand.HIDDEN_KEYS.includes(key)) {
30053
30326
  throw new BBError({
30054
30327
  code: 4003 /* CONFIG_INVALID_KEY */,
30055
- message: `Cannot display '${key}' - use 'bb auth token' to get authentication credentials`,
30328
+ message: `Cannot display '${key}' - it is part of your authentication credentials, not config. ` + `Use 'bb auth token' to retrieve credentials, or run 'bb config list' to see readable keys.`,
30056
30329
  context: { key }
30057
30330
  });
30058
30331
  }
@@ -30164,9 +30437,11 @@ class ListConfigCommand extends BaseCommand {
30164
30437
  ]);
30165
30438
  if (rows.length === 0) {
30166
30439
  this.output.text("No configuration set");
30167
- return;
30440
+ } else {
30441
+ this.output.table(["KEY", "VALUE"], rows);
30168
30442
  }
30169
- this.output.table(["KEY", "VALUE"], rows);
30443
+ this.output.text("");
30444
+ this.output.text(this.output.dim(`Settable keys: ${SETTABLE_CONFIG_KEYS.join(", ")}. Run 'bb config set --help' for details.`));
30170
30445
  }
30171
30446
  }
30172
30447
 
@@ -30199,7 +30474,7 @@ class InstallCompletionCommand extends BaseCommand {
30199
30474
  this.output.text("Restart your shell or source your profile to enable completions.");
30200
30475
  } catch (error) {
30201
30476
  throw new BBError({
30202
- code: 9999 /* UNKNOWN */,
30477
+ code: 9001 /* COMPLETION_INSTALL_FAILED */,
30203
30478
  message: `Failed to install completions: ${error}`,
30204
30479
  cause: error instanceof Error ? error : undefined
30205
30480
  });
@@ -30234,7 +30509,7 @@ class UninstallCompletionCommand extends BaseCommand {
30234
30509
  this.output.success("Shell completions uninstalled successfully!");
30235
30510
  } catch (error) {
30236
30511
  throw new BBError({
30237
- code: 9999 /* UNKNOWN */,
30512
+ code: 9002 /* COMPLETION_UNINSTALL_FAILED */,
30238
30513
  message: `Failed to uninstall completions: ${error}`,
30239
30514
  cause: error instanceof Error ? error : undefined
30240
30515
  });
@@ -30242,6 +30517,193 @@ class UninstallCompletionCommand extends BaseCommand {
30242
30517
  }
30243
30518
  }
30244
30519
 
30520
+ // src/commands/browse.command.ts
30521
+ var SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
30522
+ var PR_NUMBER_PATTERN = /^\d+$/;
30523
+ var PATH_LINE_PATTERN = /^(.+):(\d+)$/;
30524
+
30525
+ class BrowseCommand extends BaseCommand {
30526
+ contextService;
30527
+ gitService;
30528
+ urlBuilder;
30529
+ name = "browse";
30530
+ description = "Open a Bitbucket page (repo, file, PR, commit, etc.) in your browser";
30531
+ constructor(contextService, gitService, urlBuilder, output) {
30532
+ super(output);
30533
+ this.contextService = contextService;
30534
+ this.gitService = gitService;
30535
+ this.urlBuilder = urlBuilder;
30536
+ }
30537
+ async execute(options, context) {
30538
+ const repoContext = await this.contextService.requireRepoContext({
30539
+ ...context.globalOptions,
30540
+ ...options
30541
+ });
30542
+ this.validateFlagCombination(options);
30543
+ const url2 = await this.resolveUrl(options, repoContext);
30544
+ const useJson = Boolean(context.globalOptions.json);
30545
+ const printOnly = options.browser === false;
30546
+ if (useJson) {
30547
+ await this.output.json({ url: url2 });
30548
+ return { url: url2, opened: false };
30549
+ }
30550
+ if (printOnly) {
30551
+ this.output.text(url2);
30552
+ return { url: url2, opened: false };
30553
+ }
30554
+ await this.openInBrowser(url2);
30555
+ return { url: url2, opened: true };
30556
+ }
30557
+ async resolveUrl(options, ctx) {
30558
+ if (options.pr !== undefined) {
30559
+ return this.urlBuilder.pullRequest(ctx, this.parsePositiveInt(options.pr, "pr"));
30560
+ }
30561
+ if (options.prs || options.pullRequests) {
30562
+ return this.urlBuilder.pullRequestList(ctx);
30563
+ }
30564
+ if (options.branches) {
30565
+ return this.urlBuilder.branchList(ctx);
30566
+ }
30567
+ if (options.commits) {
30568
+ return this.urlBuilder.commitList(ctx);
30569
+ }
30570
+ if (options.commit !== undefined) {
30571
+ const sha = typeof options.commit === "string" && options.commit.length > 0 ? options.commit : await this.gitService.getCurrentCommit();
30572
+ return this.urlBuilder.commit(ctx, sha);
30573
+ }
30574
+ if (options.pipelines) {
30575
+ return this.urlBuilder.pipelinesHome(ctx);
30576
+ }
30577
+ if (options.pipeline !== undefined) {
30578
+ const value = options.pipeline.trim();
30579
+ if (value.length === 0) {
30580
+ throw new BBError({
30581
+ code: 5002 /* VALIDATION_INVALID */,
30582
+ message: this.appendHelpHint("--pipeline requires a run id or uuid.")
30583
+ });
30584
+ }
30585
+ return this.urlBuilder.pipelineRun(ctx, value);
30586
+ }
30587
+ if (options.downloads) {
30588
+ return this.urlBuilder.downloads(ctx);
30589
+ }
30590
+ if (options.issue !== undefined) {
30591
+ return this.urlBuilder.issue(ctx, this.parsePositiveInt(options.issue, "issue"));
30592
+ }
30593
+ if (options.issues) {
30594
+ return this.urlBuilder.issueList(ctx);
30595
+ }
30596
+ if (options.wiki) {
30597
+ return this.urlBuilder.wiki(ctx);
30598
+ }
30599
+ if (options.settings) {
30600
+ return this.urlBuilder.settings(ctx);
30601
+ }
30602
+ const target = options.target?.trim();
30603
+ if (target && target.length > 0) {
30604
+ if (PR_NUMBER_PATTERN.test(target)) {
30605
+ return this.urlBuilder.pullRequest(ctx, Number.parseInt(target, 10));
30606
+ }
30607
+ if (SHA_PATTERN.test(target)) {
30608
+ return this.urlBuilder.commit(ctx, target);
30609
+ }
30610
+ const { path: path3, line } = this.parsePathWithLine(target);
30611
+ const branch = await this.resolveBranch(options.branch);
30612
+ return this.urlBuilder.src(ctx, branch, path3, line);
30613
+ }
30614
+ if (options.branch) {
30615
+ return this.urlBuilder.src(ctx, options.branch);
30616
+ }
30617
+ return this.urlBuilder.repo(ctx);
30618
+ }
30619
+ validateFlagCombination(options) {
30620
+ const setFlags = [];
30621
+ if (options.pr !== undefined)
30622
+ setFlags.push("--pr");
30623
+ if (options.prs) {
30624
+ setFlags.push("--prs");
30625
+ } else if (options.pullRequests) {
30626
+ setFlags.push("--pull-requests");
30627
+ }
30628
+ if (options.branches)
30629
+ setFlags.push("--branches");
30630
+ if (options.commit !== undefined)
30631
+ setFlags.push("--commit");
30632
+ if (options.commits)
30633
+ setFlags.push("--commits");
30634
+ if (options.pipelines)
30635
+ setFlags.push("--pipelines");
30636
+ if (options.pipeline !== undefined)
30637
+ setFlags.push("--pipeline");
30638
+ if (options.downloads)
30639
+ setFlags.push("--downloads");
30640
+ if (options.issue !== undefined)
30641
+ setFlags.push("--issue");
30642
+ if (options.issues)
30643
+ setFlags.push("--issues");
30644
+ if (options.wiki)
30645
+ setFlags.push("--wiki");
30646
+ if (options.settings)
30647
+ setFlags.push("--settings");
30648
+ if (setFlags.length > 1) {
30649
+ throw new BBError({
30650
+ code: 5002 /* VALIDATION_INVALID */,
30651
+ message: this.appendHelpHint(`Cannot combine ${setFlags.join(" and ")}; pick one resource.`)
30652
+ });
30653
+ }
30654
+ const hasResourceFlag = setFlags.length === 1;
30655
+ const hasTarget = !!options.target?.trim();
30656
+ if (hasResourceFlag && hasTarget) {
30657
+ throw new BBError({
30658
+ code: 5002 /* VALIDATION_INVALID */,
30659
+ message: this.appendHelpHint(`Cannot use a positional target with ${setFlags[0]}.`)
30660
+ });
30661
+ }
30662
+ if (hasResourceFlag && options.branch) {
30663
+ throw new BBError({
30664
+ code: 5002 /* VALIDATION_INVALID */,
30665
+ message: this.appendHelpHint(`Cannot combine --branch with ${setFlags[0]}.`)
30666
+ });
30667
+ }
30668
+ }
30669
+ parsePathWithLine(target) {
30670
+ const match = PATH_LINE_PATTERN.exec(target);
30671
+ if (match) {
30672
+ const line = Number.parseInt(match[2], 10);
30673
+ if (Number.isFinite(line) && line > 0) {
30674
+ return { path: match[1], line };
30675
+ }
30676
+ }
30677
+ return { path: target };
30678
+ }
30679
+ async resolveBranch(explicit) {
30680
+ if (explicit && explicit.length > 0) {
30681
+ return explicit;
30682
+ }
30683
+ try {
30684
+ return await this.gitService.getCurrentBranch();
30685
+ } catch {
30686
+ return "HEAD";
30687
+ }
30688
+ }
30689
+ parsePositiveInt(value, name) {
30690
+ const parsed = Number.parseInt(value, 10);
30691
+ if (!Number.isFinite(parsed) || parsed <= 0 || String(parsed) !== value.trim()) {
30692
+ throw new BBError({
30693
+ code: 5002 /* VALIDATION_INVALID */,
30694
+ message: this.appendHelpHint(`--${name} must be a positive integer.`),
30695
+ context: { [name]: value }
30696
+ });
30697
+ }
30698
+ return parsed;
30699
+ }
30700
+ async openInBrowser(url2) {
30701
+ this.output.info(`Opening ${url2} in your browser...`);
30702
+ const open2 = (await Promise.resolve().then(() => (init_open(), exports_open))).default;
30703
+ await open2(url2);
30704
+ }
30705
+ }
30706
+
30245
30707
  // src/bootstrap.ts
30246
30708
  var require2 = createRequire(import.meta.url);
30247
30709
  var pkg = require2("../package.json");
@@ -30288,6 +30750,7 @@ function bootstrap(options = {}) {
30288
30750
  });
30289
30751
  registerCommand(container, ServiceTokens.SnippetFilesService, SnippetFilesService, [ServiceTokens.SnippetsAxios]);
30290
30752
  registerCommand(container, ServiceTokens.DefaultReviewerService, DefaultReviewerService, [ServiceTokens.PullrequestsApi]);
30753
+ container.register(ServiceTokens.UrlBuilderService, () => new UrlBuilderService);
30291
30754
  registerCommand(container, ServiceTokens.LoginCommand, LoginCommand, [
30292
30755
  ServiceTokens.CredentialStore,
30293
30756
  ServiceTokens.UsersApi,
@@ -30522,6 +30985,12 @@ function bootstrap(options = {}) {
30522
30985
  ServiceTokens.OutputService
30523
30986
  ]);
30524
30987
  registerCommand(container, ServiceTokens.ListConfigCommand, ListConfigCommand, [ServiceTokens.ConfigService, ServiceTokens.OutputService]);
30988
+ registerCommand(container, ServiceTokens.BrowseCommand, BrowseCommand, [
30989
+ ServiceTokens.ContextService,
30990
+ ServiceTokens.GitService,
30991
+ ServiceTokens.UrlBuilderService,
30992
+ ServiceTokens.OutputService
30993
+ ]);
30525
30994
  registerCommand(container, ServiceTokens.InstallCompletionCommand, InstallCompletionCommand, [ServiceTokens.OutputService]);
30526
30995
  registerCommand(container, ServiceTokens.UninstallCompletionCommand, UninstallCompletionCommand, [ServiceTokens.OutputService]);
30527
30996
  container.register(ServiceTokens.VersionService, () => {
@@ -30569,6 +31038,15 @@ function createHelpTextBuilder(noColor) {
30569
31038
  sections.push(` ${c.bold(name.padEnd(maxLen + 2))}${c.dim(desc)}`);
30570
31039
  }
30571
31040
  }
31041
+ if (config.seeAlso?.length) {
31042
+ if (sections.length)
31043
+ sections.push("");
31044
+ sections.push(c.bold("See also:"));
31045
+ const maxLen = Math.max(...config.seeAlso.map((e) => e.label.length));
31046
+ for (const { label, url: url2 } of config.seeAlso) {
31047
+ sections.push(` ${c.bold(label.padEnd(maxLen + 2))}${c.cyan(url2)}`);
31048
+ }
31049
+ }
30572
31050
  return `
30573
31051
  ` + sections.join(`
30574
31052
  `) + `
@@ -30602,6 +31080,7 @@ var ROOT_COMPLETIONS = [
30602
31080
  "repo",
30603
31081
  "pr",
30604
31082
  "snippet",
31083
+ "browse",
30605
31084
  "config",
30606
31085
  "completion",
30607
31086
  "--help",
@@ -30754,14 +31233,28 @@ function withGlobalOptions(options, context) {
30754
31233
  };
30755
31234
  }
30756
31235
  var cli = new Command;
30757
- cli.name("bb").description("A command-line interface for Bitbucket Cloud").version(pkg2.version).option("--json [fields]", "Output as JSON; optionally project to a comma-separated field list (e.g. number,title,author.display_name)").option("--jq <expression>", "Filter the JSON output through a jq expression (requires --json)").option("--no-color", "Disable color output").option("-w, --workspace <workspace>", "Specify workspace").option("-r, --repo <repo>", "Specify repository").addHelpText("after", buildHelpText({
31236
+ cli.name("bb").description("A command-line interface for Bitbucket Cloud").version(pkg2.version).option("--json [fields]", "Output as JSON; optionally project to a comma-separated field list (e.g. number,title,author.display_name)").option("--jq <expression>", `Filter the JSON output through a jq expression \u2014 runs in-process via embedded jq, requires --json (e.g. '.pullRequests[] | select(.state == "OPEN") | .title')`).option("--no-color", "Disable color output").option("-w, --workspace <workspace>", "Specify workspace").option("-r, --repo <repo>", "Specify repository").addHelpText("after", buildHelpText({
30758
31237
  envVars: {
30759
31238
  BB_USERNAME: "Bitbucket username (fallback for auth login)",
30760
31239
  BB_API_TOKEN: "Bitbucket API token (fallback for auth login)",
30761
31240
  NO_COLOR: "Disable color output when set",
30762
31241
  FORCE_COLOR: "Force color output when set (and not '0')",
30763
31242
  DEBUG: "Enable HTTP debug logging when 'true'"
30764
- }
31243
+ },
31244
+ seeAlso: [
31245
+ {
31246
+ label: "Quick Start",
31247
+ url: "https://bitbucket-cli.paulvanderlei.com/getting-started/quickstart/"
31248
+ },
31249
+ {
31250
+ label: "Scripting",
31251
+ url: "https://bitbucket-cli.paulvanderlei.com/guides/scripting/"
31252
+ },
31253
+ {
31254
+ label: "Changelog",
31255
+ url: "https://bitbucket-cli.paulvanderlei.com/help/changelog/"
31256
+ }
31257
+ ]
30765
31258
  })).action(async () => {
30766
31259
  cli.outputHelp();
30767
31260
  const versionService = container.resolve(ServiceTokens.VersionService);
@@ -30777,9 +31270,23 @@ cli.name("bb").description("A command-line interface for Bitbucket Cloud").versi
30777
31270
  output.text("\u2500".repeat(50));
30778
31271
  }
30779
31272
  } catch {}
31273
+ try {
31274
+ const configService = container.resolve(ServiceTokens.ConfigService);
31275
+ const config = await configService.getConfig();
31276
+ const hasBasicAuth = Boolean(config.username && config.apiToken);
31277
+ const hasOAuth = Boolean(config.oauthAccessToken && config.oauthRefreshToken);
31278
+ if (!hasBasicAuth && !hasOAuth) {
31279
+ output.text("");
31280
+ output.text(`Tip: Run '${output.highlight("bb auth login")}' to get started.`);
31281
+ }
31282
+ } catch {}
30780
31283
  });
30781
31284
  var authCmd = new Command("auth").description("Authenticate with Bitbucket");
30782
- authCmd.command("login").description("Authenticate with Bitbucket (OAuth or API token)").option("-u, --username <username>", "Bitbucket username (implies API token auth)").option("-p, --password <password>", "Bitbucket API token (implies API token auth)").option("--app-password", "Use API token authentication instead of OAuth").option("--client-id <clientId>", "Custom OAuth consumer client ID").option("--client-secret <clientSecret>", "Custom OAuth consumer client secret").addHelpText("after", buildHelpText({
31285
+ authCmd.command("login").description("Authenticate with Bitbucket (OAuth or API token)").option("-u, --username <username>", "Bitbucket username (implies API token auth)").option("-p, --password <password>", "Bitbucket API token (implies API token auth)").option("--app-password", "Use API token authentication (instead of OAuth). App passwords are deprecated; use API tokens.").option("--client-id <clientId>", "Custom OAuth consumer client ID").option("--client-secret <clientSecret>", "Custom OAuth consumer client secret").addHelpText("before", `
31286
+ Default: OAuth (browser-based, recommended).
31287
+ ` + `For CI/CD: API token via --app-password or BB_API_TOKEN env var.
31288
+ ` + `Note: Bitbucket app passwords are deprecated; use OAuth or an API token.
31289
+ `).addHelpText("after", buildHelpText({
30783
31290
  examples: [
30784
31291
  "bb auth login",
30785
31292
  "bb auth login --app-password -u myuser -p mytoken",
@@ -30793,7 +31300,9 @@ authCmd.command("login").description("Authenticate with Bitbucket (OAuth or API
30793
31300
  })).action(async (options) => {
30794
31301
  await runCommand(ServiceTokens.LoginCommand, options, cli);
30795
31302
  });
30796
- authCmd.command("logout").description("Log out of Bitbucket").addHelpText("after", buildHelpText({ examples: ["bb auth logout"] })).action(async () => {
31303
+ authCmd.command("logout").description("Log out of Bitbucket").addHelpText("after", buildHelpText({
31304
+ examples: ["bb auth logout", "bb auth logout --json"]
31305
+ })).action(async () => {
30797
31306
  await runCommand(ServiceTokens.LogoutCommand, undefined, cli);
30798
31307
  });
30799
31308
  authCmd.command("status").description("Show authentication status").addHelpText("after", buildHelpText({
@@ -30901,12 +31410,22 @@ prCmd.command("create").description("Create a pull request").option("-t, --title
30901
31410
  source: "current git branch",
30902
31411
  destination: "main",
30903
31412
  "default-reviewers": "false (override with --default-reviewers or config key prCreateIncludeDefaultReviewers)"
30904
- }
31413
+ },
31414
+ seeAlso: [
31415
+ {
31416
+ label: "Repository Context",
31417
+ url: "https://bitbucket-cli.paulvanderlei.com/guides/repository-context/"
31418
+ },
31419
+ {
31420
+ label: "Default reviewers",
31421
+ url: "https://bitbucket-cli.paulvanderlei.com/commands/repo/#bb-repo-default-reviewers"
31422
+ }
31423
+ ]
30905
31424
  })).action(async (options) => {
30906
31425
  const context = createContext(cli);
30907
31426
  await runCommand(ServiceTokens.CreatePRCommand, withGlobalOptions(options, context), cli, context);
30908
31427
  });
30909
- prCmd.command("list").description("List pull requests").option("-s, --state <state>", `Filter by state (${PR_STATES.join(", ")})`, "OPEN").option("--limit <number>", "Maximum number of PRs to list", "25").option("--mine", "Show only PRs where you are a reviewer").addHelpText("after", buildHelpText({
31428
+ prCmd.command("list").description("List pull requests").option("-s, --state <state>", `Filter by state (${PR_STATES.join(", ")})`, "OPEN").option("--limit <number>", "Maximum number of PRs to list", "25").option("--mine", "Show only PRs where you are a reviewer (not authored by you)").addHelpText("after", buildHelpText({
30910
31429
  examples: [
30911
31430
  "bb pr list",
30912
31431
  "bb pr list -s MERGED --limit 10",
@@ -30916,7 +31435,17 @@ prCmd.command("list").description("List pull requests").option("-s, --state <sta
30916
31435
  validValues: {
30917
31436
  "Valid states": [...PR_STATES]
30918
31437
  },
30919
- defaults: { state: "OPEN", limit: "25" }
31438
+ defaults: { state: "OPEN", limit: "25" },
31439
+ seeAlso: [
31440
+ {
31441
+ label: "Scripting & Automation",
31442
+ url: "https://bitbucket-cli.paulvanderlei.com/guides/scripting/"
31443
+ },
31444
+ {
31445
+ label: "JSON Output",
31446
+ url: "https://bitbucket-cli.paulvanderlei.com/reference/json-output/"
31447
+ }
31448
+ ]
30920
31449
  })).action(async (options) => {
30921
31450
  const context = createContext(cli);
30922
31451
  await runCommand(ServiceTokens.ListPRsCommand, withGlobalOptions(options, context), cli, context);
@@ -30981,24 +31510,50 @@ prCmd.command("merge <id>").description("Merge a pull request").option("-m, --me
30981
31510
  "rebase_fast_forward",
30982
31511
  "rebase_merge"
30983
31512
  ]
31513
+ },
31514
+ defaults: {
31515
+ strategy: "the repository's configured merge strategy (typically merge_commit)"
30984
31516
  }
30985
31517
  })).action(async (id, options) => {
30986
31518
  const context = createContext(cli);
30987
31519
  await runCommand(ServiceTokens.MergePRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
30988
31520
  });
30989
- prCmd.command("approve <id>").description("Approve a pull request").addHelpText("after", buildHelpText({ examples: ["bb pr approve 42"] })).action(async (id, options) => {
31521
+ prCmd.command("approve <id>").description("Approve a pull request").addHelpText("after", buildHelpText({
31522
+ examples: [
31523
+ "bb pr approve 42",
31524
+ "bb pr approve 42 --json",
31525
+ "bb pr approve 42 -w my-workspace -r my-repo"
31526
+ ]
31527
+ })).action(async (id, options) => {
30990
31528
  const context = createContext(cli);
30991
31529
  await runCommand(ServiceTokens.ApprovePRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
30992
31530
  });
30993
- prCmd.command("decline <id>").description("Decline a pull request").addHelpText("after", buildHelpText({ examples: ["bb pr decline 42"] })).action(async (id, options) => {
31531
+ prCmd.command("decline <id>").description("Decline a pull request").addHelpText("after", buildHelpText({
31532
+ examples: [
31533
+ "bb pr decline 42",
31534
+ "bb pr decline 42 --json",
31535
+ "bb pr decline 42 -w my-workspace -r my-repo"
31536
+ ]
31537
+ })).action(async (id, options) => {
30994
31538
  const context = createContext(cli);
30995
31539
  await runCommand(ServiceTokens.DeclinePRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
30996
31540
  });
30997
- prCmd.command("ready <id>").description("Mark a draft pull request as ready for review").addHelpText("after", buildHelpText({ examples: ["bb pr ready 42"] })).action(async (id, options) => {
31541
+ prCmd.command("ready <id>").description("Mark a draft pull request as ready for review").addHelpText("after", buildHelpText({
31542
+ examples: [
31543
+ "bb pr ready 42",
31544
+ "bb pr ready 42 --json",
31545
+ "bb pr ready 42 -w my-workspace -r my-repo"
31546
+ ]
31547
+ })).action(async (id, options) => {
30998
31548
  const context = createContext(cli);
30999
31549
  await runCommand(ServiceTokens.ReadyPRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31000
31550
  });
31001
- prCmd.command("checkout <id>").description("Checkout a pull request locally").addHelpText("after", buildHelpText({ examples: ["bb pr checkout 42"] })).action(async (id, options) => {
31551
+ prCmd.command("checkout <id>").description("Checkout a pull request locally").addHelpText("after", buildHelpText({
31552
+ examples: [
31553
+ "bb pr checkout 42",
31554
+ "bb pr checkout 42 -w my-workspace -r my-repo"
31555
+ ]
31556
+ })).action(async (id, options) => {
31002
31557
  const context = createContext(cli);
31003
31558
  await runCommand(ServiceTokens.CheckoutPRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31004
31559
  });
@@ -31040,13 +31595,19 @@ prCommentsCmd.command("add <id> <message>").description("Add a comment to a pull
31040
31595
  await runCommand(ServiceTokens.CommentPRCommand, withGlobalOptions({ id, message, ...options }, context), cli, context);
31041
31596
  });
31042
31597
  prCommentsCmd.command("edit <pr-id> <comment-id> <message>").description("Edit a comment on a pull request").addHelpText("after", buildHelpText({
31043
- examples: ['bb pr comments edit 42 12345 "Updated comment"']
31598
+ examples: [
31599
+ 'bb pr comments edit 42 12345 "Updated comment"',
31600
+ 'bb pr comments edit 42 12345 "Updated comment" --json'
31601
+ ]
31044
31602
  })).action(async (prId, commentId, message, options) => {
31045
31603
  const context = createContext(cli);
31046
31604
  await runCommand(ServiceTokens.EditCommentPRCommand, withGlobalOptions({ prId, commentId, message }, context), cli, context);
31047
31605
  });
31048
31606
  prCommentsCmd.command("delete <pr-id> <comment-id>").description("Delete a comment on a pull request").option("-y, --yes", "Skip confirmation prompt").addHelpText("after", buildHelpText({
31049
- examples: ["bb pr comments delete 42 12345 --yes"]
31607
+ examples: [
31608
+ "bb pr comments delete 42 12345",
31609
+ "bb pr comments delete 42 12345 --yes"
31610
+ ]
31050
31611
  })).action(async (prId, commentId, options) => {
31051
31612
  const context = createContext(cli);
31052
31613
  await runCommand(ServiceTokens.DeleteCommentPRCommand, withGlobalOptions({ prId, commentId, ...options }, context), cli, context);
@@ -31058,13 +31619,23 @@ prReviewersCmd.command("list <id>").description("List reviewers on a pull reques
31058
31619
  const context = createContext(cli);
31059
31620
  await runCommand(ServiceTokens.ListReviewersPRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31060
31621
  });
31061
- prReviewersCmd.command("add <id> <username>").description("Add a reviewer to a pull request").addHelpText("after", buildHelpText({ examples: ["bb pr reviewers add 42 jdoe"] })).action(async (id, username, options) => {
31622
+ prReviewersCmd.command("add <id> <user>").description("Add a reviewer to a pull request (user is an account ID or {uuid})").addHelpText("after", buildHelpText({
31623
+ examples: [
31624
+ 'bb pr reviewers add 42 "712020:3cfed7e0-0ed6-49fc-bb35-410a00ccee6f"',
31625
+ 'bb pr reviewers add 42 "{c1cb1bb5-2e32-456e-a373-43978dc12aa1}"'
31626
+ ]
31627
+ })).action(async (id, user, options) => {
31062
31628
  const context = createContext(cli);
31063
- await runCommand(ServiceTokens.AddReviewerPRCommand, withGlobalOptions({ id, username, ...options }, context), cli, context);
31629
+ await runCommand(ServiceTokens.AddReviewerPRCommand, withGlobalOptions({ id, username: user, ...options }, context), cli, context);
31064
31630
  });
31065
- prReviewersCmd.command("remove <id> <username>").description("Remove a reviewer from a pull request").addHelpText("after", buildHelpText({ examples: ["bb pr reviewers remove 42 jdoe"] })).action(async (id, username, options) => {
31631
+ prReviewersCmd.command("remove <id> <user>").description("Remove a reviewer from a pull request (user is an account ID or {uuid})").addHelpText("after", buildHelpText({
31632
+ examples: [
31633
+ 'bb pr reviewers remove 42 "712020:3cfed7e0-0ed6-49fc-bb35-410a00ccee6f"',
31634
+ 'bb pr reviewers remove 42 "{c1cb1bb5-2e32-456e-a373-43978dc12aa1}"'
31635
+ ]
31636
+ })).action(async (id, user, options) => {
31066
31637
  const context = createContext(cli);
31067
- await runCommand(ServiceTokens.RemoveReviewerPRCommand, withGlobalOptions({ id, username, ...options }, context), cli, context);
31638
+ await runCommand(ServiceTokens.RemoveReviewerPRCommand, withGlobalOptions({ id, username: user, ...options }, context), cli, context);
31068
31639
  });
31069
31640
  cli.addCommand(prCmd);
31070
31641
  prCmd.addCommand(prCommentsCmd);
@@ -31095,7 +31666,7 @@ snippetCmd.command("view <id>").description("View snippet details").option("-f,
31095
31666
  const context = createContext(cli);
31096
31667
  await runCommand(ServiceTokens.ViewSnippetCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31097
31668
  });
31098
- snippetCmd.command("create").description("Create a new snippet").option("-t, --title <title>", "Snippet title").option("-f, --file <path...>", "File(s) to include").option("--private", "Create a private snippet (default)").option("--public", "Create a public snippet").addHelpText("after", buildHelpText({
31669
+ snippetCmd.command("create").description("Create a new snippet").option("-t, --title <title>", "Snippet title").option("-f, --file <path...>", "File(s) to include (variadic; pass multiple paths or repeat the flag)").option("--private", "Create a private snippet (default)").option("--public", "Create a public snippet").addHelpText("after", buildHelpText({
31099
31670
  examples: [
31100
31671
  'bb snippet create -t "My snippet" -f file.txt',
31101
31672
  'bb snippet create -t "Config files" -f config.yml -f setup.sh --public'
@@ -31105,7 +31676,7 @@ snippetCmd.command("create").description("Create a new snippet").option("-t, --t
31105
31676
  const context = createContext(cli);
31106
31677
  await runCommand(ServiceTokens.CreateSnippetCommand, withGlobalOptions(options, context), cli, context);
31107
31678
  });
31108
- snippetCmd.command("edit <id>").description("Edit a snippet").option("-t, --title <title>", "New snippet title").option("--private", "Make snippet private").option("--public", "Make snippet public").option("-f, --file <path...>", "Replace/add file(s) (sends multipart update)").addHelpText("after", buildHelpText({
31679
+ snippetCmd.command("edit <id>").description("Edit a snippet").option("-t, --title <title>", "New snippet title").option("--private", "Make snippet private").option("--public", "Make snippet public").option("-f, --file <path...>", "Replace/add file(s) (variadic; pass multiple paths or repeat the flag; sends multipart update)").addHelpText("after", buildHelpText({
31109
31680
  examples: [
31110
31681
  'bb snippet edit kypj -t "New title"',
31111
31682
  "bb snippet edit kypj --public",
@@ -31121,11 +31692,18 @@ snippetCmd.command("delete <id>").description("Delete a snippet").option("-y, --
31121
31692
  const context = createContext(cli);
31122
31693
  await runCommand(ServiceTokens.DeleteSnippetCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31123
31694
  });
31124
- snippetCmd.command("watch <id>").description("Watch a snippet").addHelpText("after", buildHelpText({ examples: ["bb snippet watch kypj"] })).action(async (id, options) => {
31695
+ snippetCmd.command("watch <id>").description("Watch a snippet").addHelpText("after", buildHelpText({
31696
+ examples: ["bb snippet watch kypj", "bb snippet watch kypj -w my-team"]
31697
+ })).action(async (id, options) => {
31125
31698
  const context = createContext(cli);
31126
31699
  await runCommand(ServiceTokens.WatchSnippetCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31127
31700
  });
31128
- snippetCmd.command("unwatch <id>").description("Stop watching a snippet").addHelpText("after", buildHelpText({ examples: ["bb snippet unwatch kypj"] })).action(async (id, options) => {
31701
+ snippetCmd.command("unwatch <id>").description("Stop watching a snippet").addHelpText("after", buildHelpText({
31702
+ examples: [
31703
+ "bb snippet unwatch kypj",
31704
+ "bb snippet unwatch kypj -w my-team"
31705
+ ]
31706
+ })).action(async (id, options) => {
31129
31707
  const context = createContext(cli);
31130
31708
  await runCommand(ServiceTokens.UnwatchSnippetCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31131
31709
  });
@@ -31140,26 +31718,57 @@ snippetCommentsCmd.command("list <id>").description("List comments on a snippet"
31140
31718
  const context = createContext(cli);
31141
31719
  await runCommand(ServiceTokens.ListSnippetCommentsCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31142
31720
  });
31143
- snippetCommentsCmd.command("add <id>").description("Add a comment to a snippet").option("-m, --message <text>", "Comment message").addHelpText("after", buildHelpText({
31144
- examples: ['bb snippet comments add kypj -m "Great snippet!"']
31145
- })).action(async (id, options) => {
31721
+ snippetCommentsCmd.command("add <id> [message]").description("Add a comment to a snippet (message is required)").option("-m, --message <text>", "Comment message (alternative to the positional [message] argument; one of the two is required)").addHelpText("after", buildHelpText({
31722
+ examples: [
31723
+ 'bb snippet comments add kypj "Great snippet!"',
31724
+ 'bb snippet comments add kypj -m "Great snippet!"',
31725
+ 'bb snippet comments add kypj "Great snippet!" --json'
31726
+ ]
31727
+ })).action(async (id, message, options) => {
31146
31728
  const context = createContext(cli);
31147
- await runCommand(ServiceTokens.AddSnippetCommentCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31729
+ const resolvedMessage = message ?? options.message;
31730
+ await runCommand(ServiceTokens.AddSnippetCommentCommand, withGlobalOptions({ id, ...options, message: resolvedMessage }, context), cli, context);
31148
31731
  });
31149
31732
  snippetCommentsCmd.command("edit <snippet-id> <comment-id> <message>").description("Edit a comment on a snippet").addHelpText("after", buildHelpText({
31150
- examples: ['bb snippet comments edit kypj 123 "Updated comment"']
31733
+ examples: [
31734
+ 'bb snippet comments edit kypj 123 "Updated comment"',
31735
+ 'bb snippet comments edit kypj 123 "Updated comment" --json'
31736
+ ]
31151
31737
  })).action(async (snippetId, commentId, message, options) => {
31152
31738
  const context = createContext(cli);
31153
31739
  await runCommand(ServiceTokens.EditSnippetCommentCommand, withGlobalOptions({ snippetId, commentId, message }, context), cli, context);
31154
31740
  });
31155
31741
  snippetCommentsCmd.command("delete <snippet-id> <comment-id>").description("Delete a comment on a snippet").option("-y, --yes", "Skip confirmation prompt").addHelpText("after", buildHelpText({
31156
- examples: ["bb snippet comments delete kypj 123 --yes"]
31742
+ examples: [
31743
+ "bb snippet comments delete kypj 123",
31744
+ "bb snippet comments delete kypj 123 --yes"
31745
+ ]
31157
31746
  })).action(async (snippetId, commentId, options) => {
31158
31747
  const context = createContext(cli);
31159
31748
  await runCommand(ServiceTokens.DeleteSnippetCommentCommand, withGlobalOptions({ snippetId, commentId, ...options }, context), cli, context);
31160
31749
  });
31161
31750
  snippetCmd.addCommand(snippetCommentsCmd);
31162
31751
  cli.addCommand(snippetCmd);
31752
+ cli.command("browse [target]").description("Open a Bitbucket page (repo home, file, PR, commit, etc.) in your browser").option("--pr <id>", "Open a specific pull request").option("--prs", "Open the pull-requests list").option("--pull-requests", "Alias for --prs").option("--branch <name>", "Open the branch source tree (or, with <target>, that path on the branch)").option("--branches", "Open the branches list").option("--commit [sha]", "Open a specific commit (defaults to HEAD when no SHA is given)").option("--commits", "Open the commits list").option("--pipelines", "Open the pipelines page").option("--pipeline <id>", "Open a specific pipeline run").option("--downloads", "Open the downloads page").option("--issue <id>", "Open a specific issue").option("--issues", "Open the issue tracker").option("--wiki", "Open the wiki").option("--settings", "Open repository settings").option("-n, --no-browser", "Print the URL to stdout instead of opening it").addHelpText("after", buildHelpText({
31753
+ examples: [
31754
+ "bb browse",
31755
+ "bb browse src/cli.ts",
31756
+ "bb browse src/cli.ts:42",
31757
+ "bb browse --branch release/2.0 src/cli.ts",
31758
+ "bb browse 217",
31759
+ "bb browse --pr 217",
31760
+ "bb browse --prs",
31761
+ "bb browse abc1234",
31762
+ "bb browse --commit",
31763
+ "bb browse --pipelines",
31764
+ "bb browse --settings",
31765
+ "bb browse --pr 217 --no-browser",
31766
+ "bb browse --pr 217 --json url"
31767
+ ]
31768
+ })).action(async (target, options) => {
31769
+ const context = createContext(cli);
31770
+ await runCommand(ServiceTokens.BrowseCommand, withGlobalOptions({ target, ...options }, context), cli, context);
31771
+ });
31163
31772
  var configCmd = new Command("config").description("Manage configuration");
31164
31773
  configCmd.command("get <key>").description("Get a configuration value").addHelpText("after", buildHelpText({
31165
31774
  examples: ["bb config get defaultWorkspace"],
@@ -31168,7 +31777,8 @@ configCmd.command("get <key>").description("Get a configuration value").addHelpT
31168
31777
  "username",
31169
31778
  "defaultWorkspace",
31170
31779
  "skipVersionCheck",
31171
- "versionCheckInterval"
31780
+ "versionCheckInterval",
31781
+ "prCreateIncludeDefaultReviewers"
31172
31782
  ]
31173
31783
  }
31174
31784
  })).action(async (key) => {
@@ -31184,9 +31794,16 @@ configCmd.command("set <key> <value>").description("Set a configuration value").
31184
31794
  "Settable config keys": [
31185
31795
  "defaultWorkspace (string)",
31186
31796
  "skipVersionCheck (true/false)",
31187
- "versionCheckInterval (positive integer, seconds)"
31797
+ "versionCheckInterval (positive integer, seconds)",
31798
+ "prCreateIncludeDefaultReviewers (true/false)"
31188
31799
  ]
31189
- }
31800
+ },
31801
+ seeAlso: [
31802
+ {
31803
+ label: "Configuration File",
31804
+ url: "https://bitbucket-cli.paulvanderlei.com/reference/configuration/"
31805
+ }
31806
+ ]
31190
31807
  })).action(async (key, value) => {
31191
31808
  await runCommand(ServiceTokens.SetConfigCommand, { key, value }, cli);
31192
31809
  });
@@ -31197,10 +31814,20 @@ configCmd.command("list").description("List all configuration values").addHelpTe
31197
31814
  });
31198
31815
  cli.addCommand(configCmd);
31199
31816
  var completionCmd = new Command("completion").description("Shell completion utilities");
31200
- completionCmd.command("install").description("Install shell completions for bash, zsh, or fish").addHelpText("after", buildHelpText({ examples: ["bb completion install"] })).action(async () => {
31817
+ completionCmd.command("install").description("Install shell completions for bash, zsh, or fish").addHelpText("after", buildHelpText({
31818
+ examples: ["bb completion install", "bb completion install --json"],
31819
+ validValues: {
31820
+ "Supported shells": ["bash", "zsh", "fish"]
31821
+ }
31822
+ })).action(async () => {
31201
31823
  await runCommand(ServiceTokens.InstallCompletionCommand, undefined, cli);
31202
31824
  });
31203
- completionCmd.command("uninstall").description("Uninstall shell completions").addHelpText("after", buildHelpText({ examples: ["bb completion uninstall"] })).action(async () => {
31825
+ completionCmd.command("uninstall").description("Uninstall shell completions").addHelpText("after", buildHelpText({
31826
+ examples: ["bb completion uninstall", "bb completion uninstall --json"],
31827
+ validValues: {
31828
+ "Supported shells": ["bash", "zsh", "fish"]
31829
+ }
31830
+ })).action(async () => {
31204
31831
  await runCommand(ServiceTokens.UninstallCompletionCommand, undefined, cli);
31205
31832
  });
31206
31833
  cli.addCommand(completionCmd);