@pilatos/bitbucket-cli 1.14.0 → 1.15.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 +51 -8
  2. package/dist/index.js +480 -134
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -47,15 +47,28 @@
47
47
 
48
48
  ## Install
49
49
 
50
- ```bash
51
- npm install -g @pilatos/bitbucket-cli
52
- ```
50
+ > **Requires:** [Bun](https://bun.sh) runtime 1.0 or higher. The CLI is installed via npm but runs on the Bun runtime — Node.js is not supported.
53
51
 
54
- ```bash
55
- bb --version
56
- ```
52
+ 1. **Install Bun** (if `bun --version` fails):
53
+
54
+ ```bash
55
+ curl -fsSL https://bun.sh/install | bash
56
+ ```
57
+
58
+ 2. **Install the CLI:**
57
59
 
58
- > **Requires:** [Bun](https://bun.sh) runtime 1.0 or higher. The CLI is installed via npm but runs on the Bun runtime — Node.js is not supported. Install Bun first: `curl -fsSL https://bun.sh/install | bash`
60
+ ```bash
61
+ npm install -g @pilatos/bitbucket-cli
62
+ bb --version
63
+ ```
64
+
65
+ 3. **Tab completion** (optional, recommended):
66
+
67
+ ```bash
68
+ bb completion install
69
+ ```
70
+
71
+ Then restart your shell.
59
72
 
60
73
  ---
61
74
 
@@ -78,7 +91,21 @@ bb pr approve 42
78
91
  bb config set defaultWorkspace myworkspace
79
92
  ```
80
93
 
81
- **Global options:** `--json`, `--no-color`, `-w, --workspace`, `-r, --repo`
94
+ **Global options:** `--json [fields]`, `--jq <expression>`, `--no-color`, `-w, --workspace`, `-r, --repo`
95
+
96
+ ### Scripting with `--json` and `--jq`
97
+
98
+ `--json` accepts an optional comma-separated field list to project the output, and `--jq` filters the JSON in-process (no external `jq` binary required):
99
+
100
+ ```bash
101
+ # Project to specific fields
102
+ bb pr list --json id,title,state
103
+
104
+ # Filter through built-in jq
105
+ bb pr list --json --jq '.pullRequests[] | select(.state == "OPEN") | .title'
106
+ ```
107
+
108
+ See [JSON Output](https://bitbucket-cli.paulvanderlei.com/reference/json-output/) and the [Scripting guide](https://bitbucket-cli.paulvanderlei.com/guides/scripting/) for more.
82
109
 
83
110
  ---
84
111
 
@@ -89,6 +116,8 @@ Full documentation: **[bitbucket-cli.paulvanderlei.com](https://bitbucket-cli.pa
89
116
  - [Quick Start Guide](https://bitbucket-cli.paulvanderlei.com/getting-started/quickstart/)
90
117
  - [Command Reference](https://bitbucket-cli.paulvanderlei.com/commands/auth/)
91
118
  - [Guides](https://bitbucket-cli.paulvanderlei.com/guides/scripting/) (Scripting, CI/CD)
119
+ - AI assistant integration (Claude Code, Cursor, Windsurf): see [Guides &gt; AI Agents](https://bitbucket-cli.paulvanderlei.com/guides/ai-agents/)
120
+ - [Changelog](https://bitbucket-cli.paulvanderlei.com/help/changelog/) — what's new in recent releases
92
121
  - [Help](https://bitbucket-cli.paulvanderlei.com/help/troubleshooting/) (Troubleshooting, FAQ)
93
122
 
94
123
  ---
@@ -102,6 +131,20 @@ Full documentation: **[bitbucket-cli.paulvanderlei.com](https://bitbucket-cli.pa
102
131
 
103
132
  ---
104
133
 
134
+ ## Environment Variables
135
+
136
+ | Variable | Description |
137
+ | -------------- | ---------------------------------------------------------- |
138
+ | `BB_USERNAME` | Bitbucket username (fallback for `bb auth login`) |
139
+ | `BB_API_TOKEN` | Bitbucket API token (fallback for `bb auth login`; for CI) |
140
+ | `DEBUG` | Enable HTTP debug logging when set to `true` |
141
+ | `NO_COLOR` | Disable color output when set |
142
+ | `FORCE_COLOR` | Force color output when set (and not `0`) |
143
+
144
+ Full reference: [Environment variables](https://bitbucket-cli.paulvanderlei.com/reference/environment-variables/).
145
+
146
+ ---
147
+
105
148
  ## Contributing
106
149
 
107
150
  Read the [Contributing Guide](CONTRIBUTING.md) to get started.
package/dist/index.js CHANGED
@@ -17483,6 +17483,7 @@ var ServiceTokens = {
17483
17483
  // src/services/config.service.ts
17484
17484
  import { posix, win32 } from "path";
17485
17485
  import { homedir } from "os";
17486
+ import { randomUUID } from "crypto";
17486
17487
 
17487
17488
  // src/types/errors.ts
17488
17489
  class BBError extends Error {
@@ -17531,6 +17532,12 @@ class APIError extends BBError {
17531
17532
  }
17532
17533
  }
17533
17534
  }
17535
+ function rethrowWithNotFoundContext(error, notFoundMessage) {
17536
+ if (error instanceof APIError && error.statusCode === 404) {
17537
+ throw new APIError(notFoundMessage, 404, error.response, error.context);
17538
+ }
17539
+ throw error;
17540
+ }
17534
17541
 
17535
17542
  class GitError extends BBError {
17536
17543
  command;
@@ -17548,13 +17555,19 @@ class GitError extends BBError {
17548
17555
  }
17549
17556
 
17550
17557
  // src/services/config.service.ts
17558
+ var CONFIG_FILE_MODE = 384;
17559
+ var CONFIG_DIR_MODE = 448;
17560
+ var INSECURE_MODE_MASK = 63;
17561
+
17551
17562
  class ConfigService {
17552
17563
  configDir;
17553
17564
  configFile;
17565
+ platform;
17554
17566
  configCache = null;
17555
17567
  constructor(configDir, options = {}) {
17556
17568
  const platform = options.platform ?? process.platform;
17557
17569
  const joinPath = platform === "win32" ? win32.join : posix.join;
17570
+ this.platform = platform;
17558
17571
  this.configDir = configDir ?? this.resolveDefaultConfigDir({ ...options, platform });
17559
17572
  this.configFile = joinPath(this.configDir, "config.json");
17560
17573
  }
@@ -17575,7 +17588,10 @@ class ConfigService {
17575
17588
  async ensureConfigDir() {
17576
17589
  try {
17577
17590
  const fs = await import("fs/promises");
17578
- await fs.mkdir(this.configDir, { recursive: true });
17591
+ await fs.mkdir(this.configDir, {
17592
+ recursive: true,
17593
+ mode: CONFIG_DIR_MODE
17594
+ });
17579
17595
  } catch (error) {
17580
17596
  throw new BBError({
17581
17597
  code: 4002 /* CONFIG_WRITE_FAILED */,
@@ -17584,12 +17600,36 @@ class ConfigService {
17584
17600
  });
17585
17601
  }
17586
17602
  }
17603
+ async verifyPermissions(path, expectedMode, kind) {
17604
+ if (this.platform === "win32")
17605
+ return;
17606
+ const fs = await import("fs/promises");
17607
+ let stats;
17608
+ try {
17609
+ stats = await fs.stat(path);
17610
+ } catch (error) {
17611
+ if (error.code === "ENOENT")
17612
+ return;
17613
+ throw error;
17614
+ }
17615
+ const mode = stats.mode & 511;
17616
+ if (mode & INSECURE_MODE_MASK) {
17617
+ const actual = mode.toString(8).padStart(3, "0");
17618
+ const expected = expectedMode.toString(8).padStart(3, "0");
17619
+ throw new BBError({
17620
+ code: 4001 /* CONFIG_READ_FAILED */,
17621
+ message: `Config ${kind} has insecure permissions (${actual}); expected ${expected}. Run: chmod ${expected} ${path}`
17622
+ });
17623
+ }
17624
+ }
17587
17625
  async getConfig() {
17588
17626
  if (this.configCache) {
17589
17627
  return this.configCache;
17590
17628
  }
17591
17629
  try {
17592
17630
  const fs = await import("fs/promises");
17631
+ await this.verifyPermissions(this.configDir, CONFIG_DIR_MODE, "directory");
17632
+ await this.verifyPermissions(this.configFile, CONFIG_FILE_MODE, "file");
17593
17633
  const data = await fs.readFile(this.configFile, "utf-8");
17594
17634
  this.configCache = JSON.parse(data);
17595
17635
  return this.configCache;
@@ -17598,6 +17638,9 @@ class ConfigService {
17598
17638
  this.configCache = {};
17599
17639
  return this.configCache;
17600
17640
  }
17641
+ if (error instanceof BBError) {
17642
+ throw error;
17643
+ }
17601
17644
  throw new BBError({
17602
17645
  code: 4001 /* CONFIG_READ_FAILED */,
17603
17646
  message: `Failed to read config file: ${this.configFile}`,
@@ -17607,13 +17650,20 @@ class ConfigService {
17607
17650
  }
17608
17651
  async setConfig(config) {
17609
17652
  await this.ensureConfigDir();
17653
+ const fs = await import("fs/promises");
17654
+ const body = JSON.stringify(config, null, 2);
17655
+ const tmpFile = `${this.configFile}.${randomUUID()}.tmp`;
17610
17656
  try {
17611
- const fs = await import("fs/promises");
17612
- await fs.writeFile(this.configFile, JSON.stringify(config, null, 2), {
17613
- mode: 384
17657
+ await fs.writeFile(tmpFile, body, {
17658
+ mode: CONFIG_FILE_MODE,
17659
+ flag: "wx"
17614
17660
  });
17661
+ await fs.rename(tmpFile, this.configFile);
17615
17662
  this.configCache = config;
17616
17663
  } catch (error) {
17664
+ try {
17665
+ await fs.unlink(tmpFile);
17666
+ } catch {}
17617
17667
  throw new BBError({
17618
17668
  code: 4002 /* CONFIG_WRITE_FAILED */,
17619
17669
  message: `Failed to write config file: ${this.configFile}`,
@@ -17627,7 +17677,7 @@ class ConfigService {
17627
17677
  if (!username || !apiToken) {
17628
17678
  throw new BBError({
17629
17679
  code: 1001 /* AUTH_REQUIRED */,
17630
- message: "Authentication required. Run 'bb auth login' to authenticate."
17680
+ message: "Authentication required. Run 'bb auth login' or set BB_USERNAME and BB_API_TOKEN."
17631
17681
  });
17632
17682
  }
17633
17683
  return { username, apiToken };
@@ -17674,7 +17724,7 @@ class ConfigService {
17674
17724
  if (!oauthAccessToken || !oauthRefreshToken || !oauthExpiresAt) {
17675
17725
  throw new BBError({
17676
17726
  code: 1001 /* AUTH_REQUIRED */,
17677
- message: "OAuth authentication required. Run 'bb auth login' to authenticate."
17727
+ message: "OAuth authentication required. Run 'bb auth login' or set BB_USERNAME and BB_API_TOKEN."
17678
17728
  });
17679
17729
  }
17680
17730
  return {
@@ -17797,14 +17847,14 @@ class ContextService {
17797
17847
  this.configService = configService;
17798
17848
  }
17799
17849
  parseRemoteUrl(url) {
17800
- const sshMatch = /git@bitbucket\.org:([^/]+)\/([^.]+)(?:\.git)?/.exec(url);
17850
+ const sshMatch = /^git@bitbucket\.org:([^/\s]+)\/([^/\s.]+)(?:\.git)?$/.exec(url);
17801
17851
  if (sshMatch) {
17802
17852
  return {
17803
17853
  workspace: sshMatch[1],
17804
17854
  repoSlug: sshMatch[2]
17805
17855
  };
17806
17856
  }
17807
- const httpsMatch = /https?:\/\/(?:[^@]+@)?bitbucket\.org\/([^/]+)\/([^/.]+)(?:\.git)?/.exec(url);
17857
+ const httpsMatch = /^https?:\/\/(?:[^@\s]+@)?bitbucket\.org\/([^/\s]+)\/([^/\s.]+)(?:\.git)?$/.exec(url);
17808
17858
  if (httpsMatch) {
17809
17859
  return {
17810
17860
  workspace: httpsMatch[1],
@@ -17814,29 +17864,48 @@ class ContextService {
17814
17864
  return null;
17815
17865
  }
17816
17866
  async getRepoContextFromGit() {
17867
+ const result = await this.inspectGitRepoContext();
17868
+ return result.context;
17869
+ }
17870
+ async inspectGitRepoContext() {
17817
17871
  const isRepo = await this.gitService.isRepository();
17818
17872
  if (!isRepo) {
17819
- return null;
17873
+ return { context: null, reason: "not_a_git_repo", remoteUrl: null };
17820
17874
  }
17875
+ let remoteUrl;
17821
17876
  try {
17822
- const remoteUrl = await this.gitService.getRemoteUrl();
17823
- return this.parseRemoteUrl(remoteUrl);
17877
+ remoteUrl = await this.gitService.getRemoteUrl();
17824
17878
  } catch {
17825
- return null;
17879
+ return { context: null, reason: "no_remote", remoteUrl: null };
17826
17880
  }
17881
+ const context = this.parseRemoteUrl(remoteUrl);
17882
+ if (!context) {
17883
+ return { context: null, reason: "remote_not_bitbucket", remoteUrl };
17884
+ }
17885
+ return { context, reason: null, remoteUrl };
17827
17886
  }
17828
17887
  async getRepoContext(options) {
17888
+ const result = await this.resolveRepoContext(options);
17889
+ return result.context;
17890
+ }
17891
+ async resolveRepoContext(options) {
17829
17892
  if (options.workspace && options.repo) {
17830
17893
  return {
17831
- workspace: options.workspace,
17832
- repoSlug: options.repo
17894
+ context: { workspace: options.workspace, repoSlug: options.repo },
17895
+ reason: null,
17896
+ remoteUrl: null
17833
17897
  };
17834
17898
  }
17835
- const gitContext = await this.getRepoContextFromGit();
17899
+ const gitResult = await this.inspectGitRepoContext();
17900
+ const gitContext = gitResult.context;
17836
17901
  if (options.workspace && gitContext) {
17837
17902
  return {
17838
- workspace: options.workspace,
17839
- repoSlug: gitContext.repoSlug
17903
+ context: {
17904
+ workspace: options.workspace,
17905
+ repoSlug: gitContext.repoSlug
17906
+ },
17907
+ reason: null,
17908
+ remoteUrl: gitResult.remoteUrl
17840
17909
  };
17841
17910
  }
17842
17911
  if (options.repo) {
@@ -17844,22 +17913,40 @@ class ContextService {
17844
17913
  const workspace = gitContext?.workspace || config.defaultWorkspace;
17845
17914
  if (workspace) {
17846
17915
  return {
17847
- workspace,
17848
- repoSlug: options.repo
17916
+ context: { workspace, repoSlug: options.repo },
17917
+ reason: null,
17918
+ remoteUrl: gitResult.remoteUrl
17849
17919
  };
17850
17920
  }
17851
17921
  }
17852
- return gitContext;
17922
+ return gitResult;
17853
17923
  }
17854
17924
  async requireRepoContext(options) {
17855
- const context = await this.getRepoContext(options);
17856
- if (!context) {
17925
+ const result = await this.resolveRepoContext(options);
17926
+ if (!result.context) {
17857
17927
  throw new BBError({
17858
17928
  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."
17929
+ message: this.buildRepoNotFoundMessage(result.reason, result.remoteUrl),
17930
+ context: {
17931
+ reason: result.reason ?? "unknown",
17932
+ ...result.remoteUrl ? { remoteUrl: result.remoteUrl } : {}
17933
+ }
17860
17934
  });
17861
17935
  }
17862
- return context;
17936
+ return result.context;
17937
+ }
17938
+ buildRepoNotFoundMessage(reason, remoteUrl) {
17939
+ const fallback = "Use --workspace and --repo options, or run this command from within a Bitbucket repository.";
17940
+ switch (reason) {
17941
+ case "not_a_git_repo":
17942
+ return `Not in a git repository. ${fallback}`;
17943
+ case "no_remote":
17944
+ 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)}`;
17945
+ case "remote_not_bitbucket":
17946
+ return `Remote ${remoteUrl ? `'${remoteUrl}' ` : ""}is not a Bitbucket URL. ${fallback}`;
17947
+ default:
17948
+ return `Could not determine repository. ${fallback}`;
17949
+ }
17863
17950
  }
17864
17951
  async requireWorkspace(explicit) {
17865
17952
  if (explicit && explicit.length > 0) {
@@ -18397,6 +18484,10 @@ function deepGet(source, path) {
18397
18484
  }
18398
18485
 
18399
18486
  // src/services/output.service.ts
18487
+ 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;
18488
+ function stripControl(value) {
18489
+ return value.replace(CONTROL_CHARS, (_match, sgr) => sgr ?? "");
18490
+ }
18400
18491
  var WRAPPER_ARRAY_KEYS = [
18401
18492
  "pullRequests",
18402
18493
  "repositories",
@@ -18442,36 +18533,38 @@ class OutputService {
18442
18533
  if (rows.length === 0) {
18443
18534
  return;
18444
18535
  }
18445
- const widths = headers.map((header, index) => {
18446
- const maxRowWidth = Math.max(...rows.map((row) => (row[index] || "").length));
18536
+ const sanitizedHeaders = headers.map(stripControl);
18537
+ const sanitizedRows = rows.map((row) => row.map((cell) => stripControl(cell || "")));
18538
+ const widths = sanitizedHeaders.map((header, index) => {
18539
+ const maxRowWidth = Math.max(...sanitizedRows.map((row) => (row[index] || "").length));
18447
18540
  return Math.max(header.length, maxRowWidth);
18448
18541
  });
18449
- const headerRow = headers.map((header, index) => header.padEnd(widths[index])).join(" ");
18542
+ const headerRow = sanitizedHeaders.map((header, index) => header.padEnd(widths[index])).join(" ");
18450
18543
  console.log(this.format(headerRow, source_default.bold));
18451
18544
  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(" ");
18545
+ for (const row of sanitizedRows) {
18546
+ const formattedRow = row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
18454
18547
  console.log(formattedRow);
18455
18548
  }
18456
18549
  }
18457
18550
  success(message) {
18458
18551
  const symbol = this.format("\u2713", source_default.green);
18459
- console.log(`${symbol} ${message}`);
18552
+ console.log(`${symbol} ${stripControl(message)}`);
18460
18553
  }
18461
18554
  error(message) {
18462
18555
  const symbol = this.format("\u2717", source_default.red);
18463
- console.error(`${symbol} ${message}`);
18556
+ console.error(`${symbol} ${stripControl(message)}`);
18464
18557
  }
18465
18558
  warning(message) {
18466
18559
  const symbol = this.format("\u26A0", source_default.yellow);
18467
- console.warn(`${symbol} ${message}`);
18560
+ console.warn(`${symbol} ${stripControl(message)}`);
18468
18561
  }
18469
18562
  info(message) {
18470
18563
  const symbol = this.format("\u2139", source_default.blue);
18471
- console.log(`${symbol} ${message}`);
18564
+ console.log(`${symbol} ${stripControl(message)}`);
18472
18565
  }
18473
18566
  text(message) {
18474
- console.log(message);
18567
+ console.log(stripControl(message));
18475
18568
  }
18476
18569
  formatDate(date) {
18477
18570
  const d = typeof date === "string" ? new Date(date) : date;
@@ -22485,6 +22578,17 @@ function getRetryDelay(error, attempt) {
22485
22578
  function sleep(ms) {
22486
22579
  return new Promise((resolve) => setTimeout(resolve, ms));
22487
22580
  }
22581
+ function redactRequestUrl(requestUrl, baseUrl) {
22582
+ const raw = requestUrl ?? "";
22583
+ try {
22584
+ const parsed = new URL(raw, baseUrl);
22585
+ const query = parsed.search ? "?[redacted]" : "";
22586
+ return `${parsed.origin}${parsed.pathname}${query}`;
22587
+ } catch {
22588
+ const queryIdx = raw.indexOf("?");
22589
+ return queryIdx === -1 ? raw : `${raw.slice(0, queryIdx)}?[redacted]`;
22590
+ }
22591
+ }
22488
22592
  function createApiClient(credentialStore, oauthService) {
22489
22593
  const instance = axios_default.create({
22490
22594
  baseURL: BASE_URL,
@@ -22495,7 +22599,7 @@ function createApiClient(credentialStore, oauthService) {
22495
22599
  });
22496
22600
  instance.interceptors.request.use(async (config) => {
22497
22601
  if (process.env.DEBUG === "true") {
22498
- console.debug(`[HTTP] ${config.method?.toUpperCase()} ${config.url}`);
22602
+ console.debug(`[HTTP] ${config.method?.toUpperCase()} ${redactRequestUrl(config.url, config.baseURL)}`);
22499
22603
  }
22500
22604
  const authMethod = await credentialStore.getAuthMethod();
22501
22605
  if (authMethod === "oauth" && oauthService) {
@@ -22558,11 +22662,17 @@ function createApiClient(credentialStore, oauthService) {
22558
22662
  if (error.response) {
22559
22663
  const { status, data } = error.response;
22560
22664
  const message = extractErrorMessage(data) || error.message;
22561
- throw new APIError(message, status, data);
22665
+ const method = error.config?.method?.toUpperCase();
22666
+ const url2 = error.config?.url;
22667
+ throw new APIError(message, status, data, {
22668
+ status,
22669
+ ...method ? { method } : {},
22670
+ ...url2 ? { url: url2 } : {}
22671
+ });
22562
22672
  } else if (error.request) {
22563
22673
  throw new BBError({
22564
22674
  code: 7001 /* NETWORK_ERROR */,
22565
- message: "Network error: Unable to reach Bitbucket API",
22675
+ 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
22676
  cause: error
22567
22677
  });
22568
22678
  } else {
@@ -22592,13 +22702,15 @@ function extractErrorMessage(data) {
22592
22702
  }
22593
22703
  // src/services/oauth.service.ts
22594
22704
  import { createServer } from "http";
22595
- import { randomBytes } from "crypto";
22705
+ import { createHash, randomBytes } from "crypto";
22596
22706
  var BITBUCKET_AUTHORIZE_URL = "https://bitbucket.org/site/oauth2/authorize";
22597
22707
  var BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token";
22708
+ var CALLBACK_HOST = "127.0.0.1";
22598
22709
  var CALLBACK_PORT = 19872;
22599
22710
  var CALLBACK_PATH = "/callback";
22600
22711
  var CALLBACK_URL = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
22601
22712
  var AUTH_TIMEOUT_MS = 5 * 60 * 1000;
22713
+ var FETCH_TIMEOUT_MS = 1e4;
22602
22714
  var DEFAULT_CLIENT_ID = "ErUBvNmdYtfVHgW6J4";
22603
22715
  var DEFAULT_CLIENT_SECRET = "QnrWypuKXv7YvU7WJwQRza2n2QfHCEw5";
22604
22716
  var OAUTH_SCOPES = [
@@ -22609,7 +22721,36 @@ var OAUTH_SCOPES = [
22609
22721
  "pullrequest:write"
22610
22722
  ].join(" ");
22611
22723
  function generateState() {
22612
- return randomBytes(16).toString("hex");
22724
+ return randomBytes(32).toString("hex");
22725
+ }
22726
+ function base64UrlEncode(buf) {
22727
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
22728
+ }
22729
+ function generatePkcePair() {
22730
+ const verifier = base64UrlEncode(randomBytes(32));
22731
+ const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
22732
+ return { verifier, challenge };
22733
+ }
22734
+ var OAUTH_ERROR_DESCRIPTION_MAX_LENGTH = 200;
22735
+ function extractOAuthErrorDescription(body) {
22736
+ let parsed;
22737
+ try {
22738
+ parsed = JSON.parse(body);
22739
+ } catch {
22740
+ return;
22741
+ }
22742
+ if (typeof parsed !== "object" || parsed === null || !("error_description" in parsed)) {
22743
+ return;
22744
+ }
22745
+ const description = parsed.error_description;
22746
+ if (typeof description !== "string") {
22747
+ return;
22748
+ }
22749
+ const sanitized = description.replace(/\s+/g, " ").trim();
22750
+ if (sanitized.length === 0) {
22751
+ return;
22752
+ }
22753
+ return sanitized.length > OAUTH_ERROR_DESCRIPTION_MAX_LENGTH ? `${sanitized.slice(0, OAUTH_ERROR_DESCRIPTION_MAX_LENGTH)}\u2026` : sanitized;
22613
22754
  }
22614
22755
 
22615
22756
  class OAuthService {
@@ -22622,9 +22763,10 @@ class OAuthService {
22622
22763
  async authorize(clientId, clientSecret) {
22623
22764
  const resolvedClientId = clientId ?? await this.getClientId();
22624
22765
  const state = generateState();
22625
- const authUrl = this.buildAuthUrl(resolvedClientId, state);
22766
+ const { verifier, challenge } = generatePkcePair();
22767
+ const authUrl = this.buildAuthUrl(resolvedClientId, state, challenge);
22626
22768
  const { code } = await this.waitForCallback(authUrl, state);
22627
- const tokenResponse = await this.exchangeCode(code, resolvedClientId, clientSecret);
22769
+ const tokenResponse = await this.exchangeCode(code, resolvedClientId, verifier, clientSecret);
22628
22770
  const expiresAt = Math.floor(Date.now() / 1000) + tokenResponse.expires_in;
22629
22771
  await this.credentialStore.setOAuthCredentials({
22630
22772
  accessToken: tokenResponse.access_token,
@@ -22654,14 +22796,17 @@ class OAuthService {
22654
22796
  "Content-Type": "application/x-www-form-urlencoded",
22655
22797
  Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`
22656
22798
  },
22657
- body: params.toString()
22799
+ body: params.toString(),
22800
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
22658
22801
  });
22659
22802
  if (!response.ok) {
22660
22803
  const errorBody = await response.text();
22804
+ const description = extractOAuthErrorDescription(errorBody);
22805
+ const baseMessage = `Failed to refresh OAuth token. Run 'bb auth login' to re-authenticate.`;
22661
22806
  throw new BBError({
22662
22807
  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 }
22808
+ message: description ? `${baseMessage} (${description})` : baseMessage,
22809
+ context: { status: response.status }
22665
22810
  });
22666
22811
  }
22667
22812
  const tokenResponse = await response.json();
@@ -22674,22 +22819,29 @@ class OAuthService {
22674
22819
  return tokenResponse.access_token;
22675
22820
  }
22676
22821
  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()
22822
+ const credentials = await this.credentialStore.getOAuthCredentials();
22823
+ const clientId = await this.getClientId();
22824
+ const clientSecret = await this.getClientSecret();
22825
+ const params = new URLSearchParams({
22826
+ token: credentials.accessToken
22827
+ });
22828
+ const response = await fetch("https://bitbucket.org/site/oauth2/revoke", {
22829
+ method: "POST",
22830
+ headers: {
22831
+ "Content-Type": "application/x-www-form-urlencoded",
22832
+ Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`
22833
+ },
22834
+ body: params.toString(),
22835
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
22836
+ });
22837
+ if (!response.ok) {
22838
+ const errorBody = await response.text().catch(() => "");
22839
+ throw new BBError({
22840
+ code: 7001 /* NETWORK_ERROR */,
22841
+ message: `Failed to revoke OAuth token (HTTP ${response.status}).`,
22842
+ context: { status: response.status, body: errorBody }
22691
22843
  });
22692
- } catch {}
22844
+ }
22693
22845
  }
22694
22846
  async getValidAccessToken() {
22695
22847
  const isExpired = await this.credentialStore.isOAuthTokenExpired();
@@ -22707,13 +22859,15 @@ class OAuthService {
22707
22859
  const customSecret = await this.configService.getValue("oauthClientSecret");
22708
22860
  return customSecret ?? DEFAULT_CLIENT_SECRET;
22709
22861
  }
22710
- buildAuthUrl(clientId, state) {
22862
+ buildAuthUrl(clientId, state, codeChallenge) {
22711
22863
  const params = new URLSearchParams({
22712
22864
  client_id: clientId,
22713
22865
  response_type: "code",
22714
22866
  redirect_uri: CALLBACK_URL,
22715
22867
  scope: OAUTH_SCOPES,
22716
- state
22868
+ state,
22869
+ code_challenge: codeChallenge,
22870
+ code_challenge_method: "S256"
22717
22871
  });
22718
22872
  return `${BITBUCKET_AUTHORIZE_URL}?${params.toString()}`;
22719
22873
  }
@@ -22783,7 +22937,7 @@ class OAuthService {
22783
22937
  }));
22784
22938
  }
22785
22939
  });
22786
- server.listen(CALLBACK_PORT, async () => {
22940
+ server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
22787
22941
  try {
22788
22942
  const open2 = (await Promise.resolve().then(() => (init_open(), exports_open))).default;
22789
22943
  await open2(authUrl);
@@ -22794,12 +22948,13 @@ ${authUrl}
22794
22948
  });
22795
22949
  });
22796
22950
  }
22797
- async exchangeCode(code, clientId, clientSecretOverride) {
22951
+ async exchangeCode(code, clientId, codeVerifier, clientSecretOverride) {
22798
22952
  const clientSecret = clientSecretOverride ?? await this.getClientSecret();
22799
22953
  const params = new URLSearchParams({
22800
22954
  grant_type: "authorization_code",
22801
22955
  code,
22802
- redirect_uri: CALLBACK_URL
22956
+ redirect_uri: CALLBACK_URL,
22957
+ code_verifier: codeVerifier
22803
22958
  });
22804
22959
  const response = await fetch(BITBUCKET_TOKEN_URL, {
22805
22960
  method: "POST",
@@ -22807,21 +22962,25 @@ ${authUrl}
22807
22962
  "Content-Type": "application/x-www-form-urlencoded",
22808
22963
  Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`
22809
22964
  },
22810
- body: params.toString()
22965
+ body: params.toString(),
22966
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
22811
22967
  });
22812
22968
  if (!response.ok) {
22813
22969
  const errorBody = await response.text();
22970
+ const description = extractOAuthErrorDescription(errorBody);
22971
+ const baseMessage = `Failed to exchange authorization code. Please try again.`;
22814
22972
  throw new BBError({
22815
22973
  code: 1002 /* AUTH_INVALID */,
22816
- message: `Failed to exchange authorization code. Please try again.`,
22817
- context: { status: response.status, body: errorBody }
22974
+ message: description ? `${baseMessage} (${description})` : baseMessage,
22975
+ context: { status: response.status }
22818
22976
  });
22819
22977
  }
22820
22978
  return await response.json();
22821
22979
  }
22822
22980
  async fetchUserInfo(accessToken) {
22823
22981
  const response = await fetch("https://api.bitbucket.org/2.0/user", {
22824
- headers: { Authorization: `Bearer ${accessToken}` }
22982
+ headers: { Authorization: `Bearer ${accessToken}` },
22983
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
22825
22984
  });
22826
22985
  if (!response.ok) {
22827
22986
  throw new BBError({
@@ -27005,13 +27164,32 @@ class BaseCommand {
27005
27164
  }
27006
27165
  requireOption(value, name, message) {
27007
27166
  if (value === undefined || value === null || value === "") {
27167
+ const baseMessage = message || `Option --${name} is required`;
27008
27168
  throw new BBError({
27009
27169
  code: 5001 /* VALIDATION_REQUIRED */,
27010
- message: message || `Option --${name} is required`
27170
+ message: this.appendHelpHint(baseMessage)
27011
27171
  });
27012
27172
  }
27013
27173
  return value;
27014
27174
  }
27175
+ appendHelpHint(message) {
27176
+ const commandPath = this.getCommandPath();
27177
+ const target = commandPath ? `bb ${commandPath} --help` : "bb --help";
27178
+ return `${message} Run \`${target}\` for usage.`;
27179
+ }
27180
+ getCommandPath() {
27181
+ const argv = process.argv.slice(2);
27182
+ const tokens = [];
27183
+ for (const arg of argv) {
27184
+ if (arg.startsWith("-"))
27185
+ break;
27186
+ tokens.push(arg);
27187
+ }
27188
+ if (tokens.length >= 2) {
27189
+ return `${tokens[0]} ${tokens[1]}`;
27190
+ }
27191
+ return tokens.join(" ");
27192
+ }
27015
27193
  parseIntOption(value, name) {
27016
27194
  const parsed = Number.parseInt(value, 10);
27017
27195
  if (Number.isNaN(parsed)) {
@@ -27112,12 +27290,33 @@ class LoginCommand extends BaseCommand {
27112
27290
  this.output.success(`Logged in as ${user.display_name} (${user.username})`);
27113
27291
  } catch (error) {
27114
27292
  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
- });
27293
+ throw this.wrapLoginError(error);
27119
27294
  }
27120
27295
  }
27296
+ wrapLoginError(error) {
27297
+ const detail = error instanceof Error ? error.message : String(error);
27298
+ if (error instanceof APIError) {
27299
+ if (error.statusCode === 401 || error.statusCode === 403) {
27300
+ return new BBError({
27301
+ code: 1002 /* AUTH_INVALID */,
27302
+ message: `Invalid username or token: ${detail}. Verify your Bitbucket username and that the API token is current and has the required scopes.`,
27303
+ cause: error
27304
+ });
27305
+ }
27306
+ if (error.statusCode === 429) {
27307
+ return new BBError({
27308
+ code: 2004 /* API_RATE_LIMITED */,
27309
+ message: `Bitbucket API rate-limited: ${detail}. Wait a moment and try again.`,
27310
+ cause: error
27311
+ });
27312
+ }
27313
+ }
27314
+ return new BBError({
27315
+ code: 1002 /* AUTH_INVALID */,
27316
+ message: `Authentication failed: ${detail}`,
27317
+ cause: error instanceof Error ? error : undefined
27318
+ });
27319
+ }
27121
27320
  }
27122
27321
 
27123
27322
  // src/commands/auth/logout.command.ts
@@ -27133,16 +27332,28 @@ class LogoutCommand extends BaseCommand {
27133
27332
  }
27134
27333
  async execute(_options, context) {
27135
27334
  const authMethod = await this.credentialStore.getAuthMethod();
27335
+ let revokeFailed = false;
27136
27336
  if (authMethod === "oauth") {
27137
- await this.oauthService.revokeToken();
27337
+ try {
27338
+ await this.oauthService.revokeToken();
27339
+ } catch {
27340
+ revokeFailed = true;
27341
+ }
27138
27342
  await this.credentialStore.clearOAuthCredentials();
27139
27343
  } else {
27140
27344
  await this.credentialStore.clearCredentials();
27141
27345
  }
27142
27346
  if (context.globalOptions.json) {
27143
- await this.output.json({ authenticated: false, success: true });
27347
+ await this.output.json({
27348
+ authenticated: false,
27349
+ success: true,
27350
+ revokeFailed: revokeFailed || undefined
27351
+ });
27144
27352
  return;
27145
27353
  }
27354
+ if (revokeFailed) {
27355
+ this.output.warning("Token revocation failed; the access token may still be valid at Bitbucket. Consider revoking it manually.");
27356
+ }
27146
27357
  this.output.success("Logged out of Bitbucket");
27147
27358
  }
27148
27359
  }
@@ -27252,7 +27463,7 @@ class TokenCommand extends BaseCommand {
27252
27463
  if (!credentials.username || !credentials.apiToken) {
27253
27464
  throw new BBError({
27254
27465
  code: 1001 /* AUTH_REQUIRED */,
27255
- message: "Not authenticated. Run 'bb auth login' first."
27466
+ message: "Not authenticated. Run 'bb auth login' or set BB_USERNAME and BB_API_TOKEN."
27256
27467
  });
27257
27468
  }
27258
27469
  const token = Buffer.from(`${credentials.username}:${credentials.apiToken}`).toString("base64");
@@ -27729,7 +27940,7 @@ class ViewRepoCommand extends BaseCommand {
27729
27940
  const response = await this.repositoriesApi.repositoriesWorkspaceRepoSlugGet({
27730
27941
  workspace: repoContext.workspace,
27731
27942
  repoSlug: repoContext.repoSlug
27732
- });
27943
+ }).catch((error) => rethrowWithNotFoundContext(error, `Repository ${repoContext.workspace}/${repoContext.repoSlug} not found.`));
27733
27944
  const repo = response.data;
27734
27945
  if (context.globalOptions.json) {
27735
27946
  await this.output.json(repo);
@@ -27952,7 +28163,7 @@ class CreatePRCommand extends BaseCommand {
27952
28163
  if (!options.title) {
27953
28164
  throw new BBError({
27954
28165
  code: 5001 /* VALIDATION_REQUIRED */,
27955
- message: "Pull request title is required. Use --title option."
28166
+ message: this.appendHelpHint("Pull request title is required. Use --title option.")
27956
28167
  });
27957
28168
  }
27958
28169
  const repoContext = await this.contextService.requireRepoContext({
@@ -28175,7 +28386,7 @@ class ViewPRCommand extends BaseCommand {
28175
28386
  workspace: repoContext.workspace,
28176
28387
  repoSlug: repoContext.repoSlug,
28177
28388
  pullRequestId: prId
28178
- });
28389
+ }).catch((error) => rethrowWithNotFoundContext(error, `Pull request #${prId} not found in ${repoContext.workspace}/${repoContext.repoSlug}.`));
28179
28390
  const pr = response.data;
28180
28391
  if (context.globalOptions.json) {
28181
28392
  await this.output.json(pr);
@@ -28337,8 +28548,9 @@ class EditPRCommand extends BaseCommand {
28337
28548
  try {
28338
28549
  body = fs7.readFileSync(options.bodyFile, "utf-8");
28339
28550
  } catch (err) {
28551
+ const isNotFound = err instanceof Error && err.code === "ENOENT";
28340
28552
  throw new BBError({
28341
- code: 9999 /* UNKNOWN */,
28553
+ code: isNotFound ? 5003 /* FILE_NOT_FOUND */ : 9999 /* UNKNOWN */,
28342
28554
  message: `Failed to read file '${options.bodyFile}': ${err instanceof Error ? err.message : "Unknown error"}`,
28343
28555
  cause: err instanceof Error ? err : undefined,
28344
28556
  context: { bodyFile: options.bodyFile }
@@ -28608,10 +28820,6 @@ class CheckoutPRCommand extends BaseCommand {
28608
28820
  }
28609
28821
 
28610
28822
  // 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
28823
  class DiffPRCommand extends BaseCommand {
28616
28824
  pullrequestsApi;
28617
28825
  contextService;
@@ -28722,16 +28930,8 @@ class DiffPRCommand extends BaseCommand {
28722
28930
  }
28723
28931
  async openInBrowser(url2) {
28724
28932
  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);
28933
+ const open2 = (await Promise.resolve().then(() => (init_open(), exports_open))).default;
28934
+ await open2(url2);
28735
28935
  }
28736
28936
  async getWebDiffUrl(workspace, repoSlug, prId) {
28737
28937
  const prResponse = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsPullRequestIdGet({
@@ -28961,11 +29161,11 @@ class ActivityPRCommand extends BaseCommand {
28961
29161
  return activity.type ? activity.type.toLowerCase() : "activity";
28962
29162
  }
28963
29163
  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;
29164
+ 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
29165
  return getUserDisplayName(user) ?? "Unknown";
28966
29166
  }
28967
29167
  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;
29168
+ 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
29169
  if (!date) {
28970
29170
  return "-";
28971
29171
  }
@@ -29034,16 +29234,23 @@ class CommentPRCommand extends BaseCommand {
29034
29234
  this.contextService = contextService;
29035
29235
  }
29036
29236
  async execute(options, context) {
29237
+ 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
29238
  if ((options.lineTo || options.lineFrom) && !options.file) {
29038
29239
  throw new BBError({
29039
29240
  code: 5001 /* VALIDATION_REQUIRED */,
29040
- message: "--file is required when using --line-to or --line-from"
29241
+ message: this.appendHelpHint(`--file is required when using --line-to or --line-from. ${validModesNote}`),
29242
+ context: {
29243
+ validModes: ["general", "file", "inline"]
29244
+ }
29041
29245
  });
29042
29246
  }
29043
29247
  if (options.file && !options.lineTo && !options.lineFrom) {
29044
29248
  throw new BBError({
29045
29249
  code: 5001 /* VALIDATION_REQUIRED */,
29046
- message: "At least one of --line-to or --line-from is required when using --file"
29250
+ message: this.appendHelpHint(`At least one of --line-to or --line-from is required when using --file. ${validModesNote}`),
29251
+ context: {
29252
+ validModes: ["general", "file", "inline"]
29253
+ }
29047
29254
  });
29048
29255
  }
29049
29256
  if (options.lineTo) {
@@ -29575,13 +29782,13 @@ class ViewSnippetCommand extends BaseCommand {
29575
29782
  const response = await this.snippetsApi.snippetsWorkspaceEncodedIdGet({
29576
29783
  workspace,
29577
29784
  encodedId: options.id
29578
- });
29785
+ }).catch((error) => rethrowWithNotFoundContext(error, `Snippet ${options.id} not found in workspace ${workspace}.`));
29579
29786
  const snippet = response.data;
29580
29787
  const fileNames = this.extractFileNames(snippet);
29581
29788
  if (options.file !== undefined) {
29582
29789
  if (!fileNames.includes(options.file)) {
29583
29790
  throw new BBError({
29584
- code: 5002 /* VALIDATION_INVALID */,
29791
+ code: 5003 /* FILE_NOT_FOUND */,
29585
29792
  message: `File not found in snippet: ${options.file}`,
29586
29793
  context: { file: options.file, available: fileNames }
29587
29794
  });
@@ -29689,7 +29896,7 @@ class CreateSnippetCommand extends BaseCommand {
29689
29896
  for (const filePath of options.file) {
29690
29897
  if (!fs8.existsSync(filePath)) {
29691
29898
  throw new BBError({
29692
- code: 5002 /* VALIDATION_INVALID */,
29899
+ code: 5003 /* FILE_NOT_FOUND */,
29693
29900
  message: `File not found: ${filePath}`,
29694
29901
  context: { file: filePath }
29695
29902
  });
@@ -29748,7 +29955,7 @@ class EditSnippetCommand extends BaseCommand {
29748
29955
  for (const filePath of options.file) {
29749
29956
  if (!fs9.existsSync(filePath)) {
29750
29957
  throw new BBError({
29751
- code: 5002 /* VALIDATION_INVALID */,
29958
+ code: 5003 /* FILE_NOT_FOUND */,
29752
29959
  message: `File not found: ${filePath}`,
29753
29960
  context: { file: filePath }
29754
29961
  });
@@ -30052,7 +30259,7 @@ class GetConfigCommand extends BaseCommand {
30052
30259
  if (GetConfigCommand.HIDDEN_KEYS.includes(key)) {
30053
30260
  throw new BBError({
30054
30261
  code: 4003 /* CONFIG_INVALID_KEY */,
30055
- message: `Cannot display '${key}' - use 'bb auth token' to get authentication credentials`,
30262
+ 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
30263
  context: { key }
30057
30264
  });
30058
30265
  }
@@ -30164,9 +30371,11 @@ class ListConfigCommand extends BaseCommand {
30164
30371
  ]);
30165
30372
  if (rows.length === 0) {
30166
30373
  this.output.text("No configuration set");
30167
- return;
30374
+ } else {
30375
+ this.output.table(["KEY", "VALUE"], rows);
30168
30376
  }
30169
- this.output.table(["KEY", "VALUE"], rows);
30377
+ this.output.text("");
30378
+ this.output.text(this.output.dim(`Settable keys: ${SETTABLE_CONFIG_KEYS.join(", ")}. Run 'bb config set --help' for details.`));
30170
30379
  }
30171
30380
  }
30172
30381
 
@@ -30199,7 +30408,7 @@ class InstallCompletionCommand extends BaseCommand {
30199
30408
  this.output.text("Restart your shell or source your profile to enable completions.");
30200
30409
  } catch (error) {
30201
30410
  throw new BBError({
30202
- code: 9999 /* UNKNOWN */,
30411
+ code: 9001 /* COMPLETION_INSTALL_FAILED */,
30203
30412
  message: `Failed to install completions: ${error}`,
30204
30413
  cause: error instanceof Error ? error : undefined
30205
30414
  });
@@ -30234,7 +30443,7 @@ class UninstallCompletionCommand extends BaseCommand {
30234
30443
  this.output.success("Shell completions uninstalled successfully!");
30235
30444
  } catch (error) {
30236
30445
  throw new BBError({
30237
- code: 9999 /* UNKNOWN */,
30446
+ code: 9002 /* COMPLETION_UNINSTALL_FAILED */,
30238
30447
  message: `Failed to uninstall completions: ${error}`,
30239
30448
  cause: error instanceof Error ? error : undefined
30240
30449
  });
@@ -30569,6 +30778,15 @@ function createHelpTextBuilder(noColor) {
30569
30778
  sections.push(` ${c.bold(name.padEnd(maxLen + 2))}${c.dim(desc)}`);
30570
30779
  }
30571
30780
  }
30781
+ if (config.seeAlso?.length) {
30782
+ if (sections.length)
30783
+ sections.push("");
30784
+ sections.push(c.bold("See also:"));
30785
+ const maxLen = Math.max(...config.seeAlso.map((e) => e.label.length));
30786
+ for (const { label, url: url2 } of config.seeAlso) {
30787
+ sections.push(` ${c.bold(label.padEnd(maxLen + 2))}${c.cyan(url2)}`);
30788
+ }
30789
+ }
30572
30790
  return `
30573
30791
  ` + sections.join(`
30574
30792
  `) + `
@@ -30754,14 +30972,28 @@ function withGlobalOptions(options, context) {
30754
30972
  };
30755
30973
  }
30756
30974
  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({
30975
+ 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
30976
  envVars: {
30759
30977
  BB_USERNAME: "Bitbucket username (fallback for auth login)",
30760
30978
  BB_API_TOKEN: "Bitbucket API token (fallback for auth login)",
30761
30979
  NO_COLOR: "Disable color output when set",
30762
30980
  FORCE_COLOR: "Force color output when set (and not '0')",
30763
30981
  DEBUG: "Enable HTTP debug logging when 'true'"
30764
- }
30982
+ },
30983
+ seeAlso: [
30984
+ {
30985
+ label: "Quick Start",
30986
+ url: "https://bitbucket-cli.paulvanderlei.com/getting-started/quickstart/"
30987
+ },
30988
+ {
30989
+ label: "Scripting",
30990
+ url: "https://bitbucket-cli.paulvanderlei.com/guides/scripting/"
30991
+ },
30992
+ {
30993
+ label: "Changelog",
30994
+ url: "https://bitbucket-cli.paulvanderlei.com/help/changelog/"
30995
+ }
30996
+ ]
30765
30997
  })).action(async () => {
30766
30998
  cli.outputHelp();
30767
30999
  const versionService = container.resolve(ServiceTokens.VersionService);
@@ -30777,9 +31009,23 @@ cli.name("bb").description("A command-line interface for Bitbucket Cloud").versi
30777
31009
  output.text("\u2500".repeat(50));
30778
31010
  }
30779
31011
  } catch {}
31012
+ try {
31013
+ const configService = container.resolve(ServiceTokens.ConfigService);
31014
+ const config = await configService.getConfig();
31015
+ const hasBasicAuth = Boolean(config.username && config.apiToken);
31016
+ const hasOAuth = Boolean(config.oauthAccessToken && config.oauthRefreshToken);
31017
+ if (!hasBasicAuth && !hasOAuth) {
31018
+ output.text("");
31019
+ output.text(`Tip: Run '${output.highlight("bb auth login")}' to get started.`);
31020
+ }
31021
+ } catch {}
30780
31022
  });
30781
31023
  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({
31024
+ 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("before", `
31025
+ Default: OAuth (browser-based, recommended).
31026
+ ` + `For CI/CD: API token via --app-password or BB_API_TOKEN env var.
31027
+ ` + `Note: Bitbucket app passwords are deprecated; use OAuth or an API token.
31028
+ `).addHelpText("after", buildHelpText({
30783
31029
  examples: [
30784
31030
  "bb auth login",
30785
31031
  "bb auth login --app-password -u myuser -p mytoken",
@@ -30793,7 +31039,9 @@ authCmd.command("login").description("Authenticate with Bitbucket (OAuth or API
30793
31039
  })).action(async (options) => {
30794
31040
  await runCommand(ServiceTokens.LoginCommand, options, cli);
30795
31041
  });
30796
- authCmd.command("logout").description("Log out of Bitbucket").addHelpText("after", buildHelpText({ examples: ["bb auth logout"] })).action(async () => {
31042
+ authCmd.command("logout").description("Log out of Bitbucket").addHelpText("after", buildHelpText({
31043
+ examples: ["bb auth logout", "bb auth logout --json"]
31044
+ })).action(async () => {
30797
31045
  await runCommand(ServiceTokens.LogoutCommand, undefined, cli);
30798
31046
  });
30799
31047
  authCmd.command("status").description("Show authentication status").addHelpText("after", buildHelpText({
@@ -30901,7 +31149,17 @@ prCmd.command("create").description("Create a pull request").option("-t, --title
30901
31149
  source: "current git branch",
30902
31150
  destination: "main",
30903
31151
  "default-reviewers": "false (override with --default-reviewers or config key prCreateIncludeDefaultReviewers)"
30904
- }
31152
+ },
31153
+ seeAlso: [
31154
+ {
31155
+ label: "Repository Context",
31156
+ url: "https://bitbucket-cli.paulvanderlei.com/guides/repository-context/"
31157
+ },
31158
+ {
31159
+ label: "Default reviewers",
31160
+ url: "https://bitbucket-cli.paulvanderlei.com/commands/repo/#bb-repo-default-reviewers"
31161
+ }
31162
+ ]
30905
31163
  })).action(async (options) => {
30906
31164
  const context = createContext(cli);
30907
31165
  await runCommand(ServiceTokens.CreatePRCommand, withGlobalOptions(options, context), cli, context);
@@ -30916,7 +31174,17 @@ prCmd.command("list").description("List pull requests").option("-s, --state <sta
30916
31174
  validValues: {
30917
31175
  "Valid states": [...PR_STATES]
30918
31176
  },
30919
- defaults: { state: "OPEN", limit: "25" }
31177
+ defaults: { state: "OPEN", limit: "25" },
31178
+ seeAlso: [
31179
+ {
31180
+ label: "Scripting & Automation",
31181
+ url: "https://bitbucket-cli.paulvanderlei.com/guides/scripting/"
31182
+ },
31183
+ {
31184
+ label: "JSON Output",
31185
+ url: "https://bitbucket-cli.paulvanderlei.com/reference/json-output/"
31186
+ }
31187
+ ]
30920
31188
  })).action(async (options) => {
30921
31189
  const context = createContext(cli);
30922
31190
  await runCommand(ServiceTokens.ListPRsCommand, withGlobalOptions(options, context), cli, context);
@@ -30981,24 +31249,50 @@ prCmd.command("merge <id>").description("Merge a pull request").option("-m, --me
30981
31249
  "rebase_fast_forward",
30982
31250
  "rebase_merge"
30983
31251
  ]
31252
+ },
31253
+ defaults: {
31254
+ strategy: "the repository's configured merge strategy (typically merge_commit)"
30984
31255
  }
30985
31256
  })).action(async (id, options) => {
30986
31257
  const context = createContext(cli);
30987
31258
  await runCommand(ServiceTokens.MergePRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
30988
31259
  });
30989
- prCmd.command("approve <id>").description("Approve a pull request").addHelpText("after", buildHelpText({ examples: ["bb pr approve 42"] })).action(async (id, options) => {
31260
+ prCmd.command("approve <id>").description("Approve a pull request").addHelpText("after", buildHelpText({
31261
+ examples: [
31262
+ "bb pr approve 42",
31263
+ "bb pr approve 42 --json",
31264
+ "bb pr approve 42 -w my-workspace -r my-repo"
31265
+ ]
31266
+ })).action(async (id, options) => {
30990
31267
  const context = createContext(cli);
30991
31268
  await runCommand(ServiceTokens.ApprovePRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
30992
31269
  });
30993
- prCmd.command("decline <id>").description("Decline a pull request").addHelpText("after", buildHelpText({ examples: ["bb pr decline 42"] })).action(async (id, options) => {
31270
+ prCmd.command("decline <id>").description("Decline a pull request").addHelpText("after", buildHelpText({
31271
+ examples: [
31272
+ "bb pr decline 42",
31273
+ "bb pr decline 42 --json",
31274
+ "bb pr decline 42 -w my-workspace -r my-repo"
31275
+ ]
31276
+ })).action(async (id, options) => {
30994
31277
  const context = createContext(cli);
30995
31278
  await runCommand(ServiceTokens.DeclinePRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
30996
31279
  });
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) => {
31280
+ prCmd.command("ready <id>").description("Mark a draft pull request as ready for review").addHelpText("after", buildHelpText({
31281
+ examples: [
31282
+ "bb pr ready 42",
31283
+ "bb pr ready 42 --json",
31284
+ "bb pr ready 42 -w my-workspace -r my-repo"
31285
+ ]
31286
+ })).action(async (id, options) => {
30998
31287
  const context = createContext(cli);
30999
31288
  await runCommand(ServiceTokens.ReadyPRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31000
31289
  });
31001
- prCmd.command("checkout <id>").description("Checkout a pull request locally").addHelpText("after", buildHelpText({ examples: ["bb pr checkout 42"] })).action(async (id, options) => {
31290
+ prCmd.command("checkout <id>").description("Checkout a pull request locally").addHelpText("after", buildHelpText({
31291
+ examples: [
31292
+ "bb pr checkout 42",
31293
+ "bb pr checkout 42 -w my-workspace -r my-repo"
31294
+ ]
31295
+ })).action(async (id, options) => {
31002
31296
  const context = createContext(cli);
31003
31297
  await runCommand(ServiceTokens.CheckoutPRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31004
31298
  });
@@ -31040,13 +31334,19 @@ prCommentsCmd.command("add <id> <message>").description("Add a comment to a pull
31040
31334
  await runCommand(ServiceTokens.CommentPRCommand, withGlobalOptions({ id, message, ...options }, context), cli, context);
31041
31335
  });
31042
31336
  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"']
31337
+ examples: [
31338
+ 'bb pr comments edit 42 12345 "Updated comment"',
31339
+ 'bb pr comments edit 42 12345 "Updated comment" --json'
31340
+ ]
31044
31341
  })).action(async (prId, commentId, message, options) => {
31045
31342
  const context = createContext(cli);
31046
31343
  await runCommand(ServiceTokens.EditCommentPRCommand, withGlobalOptions({ prId, commentId, message }, context), cli, context);
31047
31344
  });
31048
31345
  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"]
31346
+ examples: [
31347
+ "bb pr comments delete 42 12345",
31348
+ "bb pr comments delete 42 12345 --yes"
31349
+ ]
31050
31350
  })).action(async (prId, commentId, options) => {
31051
31351
  const context = createContext(cli);
31052
31352
  await runCommand(ServiceTokens.DeleteCommentPRCommand, withGlobalOptions({ prId, commentId, ...options }, context), cli, context);
@@ -31058,11 +31358,21 @@ prReviewersCmd.command("list <id>").description("List reviewers on a pull reques
31058
31358
  const context = createContext(cli);
31059
31359
  await runCommand(ServiceTokens.ListReviewersPRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31060
31360
  });
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) => {
31361
+ prReviewersCmd.command("add <id> <username>").description("Add a reviewer to a pull request").addHelpText("after", buildHelpText({
31362
+ examples: [
31363
+ 'bb pr reviewers add 42 "712020:3cfed7e0-0ed6-49fc-bb35-410a00ccee6f"',
31364
+ 'bb pr reviewers add 42 "{c1cb1bb5-2e32-456e-a373-43978dc12aa1}"'
31365
+ ]
31366
+ })).action(async (id, username, options) => {
31062
31367
  const context = createContext(cli);
31063
31368
  await runCommand(ServiceTokens.AddReviewerPRCommand, withGlobalOptions({ id, username, ...options }, context), cli, context);
31064
31369
  });
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) => {
31370
+ prReviewersCmd.command("remove <id> <username>").description("Remove a reviewer from a pull request").addHelpText("after", buildHelpText({
31371
+ examples: [
31372
+ 'bb pr reviewers remove 42 "712020:3cfed7e0-0ed6-49fc-bb35-410a00ccee6f"',
31373
+ 'bb pr reviewers remove 42 "{c1cb1bb5-2e32-456e-a373-43978dc12aa1}"'
31374
+ ]
31375
+ })).action(async (id, username, options) => {
31066
31376
  const context = createContext(cli);
31067
31377
  await runCommand(ServiceTokens.RemoveReviewerPRCommand, withGlobalOptions({ id, username, ...options }, context), cli, context);
31068
31378
  });
@@ -31121,11 +31431,18 @@ snippetCmd.command("delete <id>").description("Delete a snippet").option("-y, --
31121
31431
  const context = createContext(cli);
31122
31432
  await runCommand(ServiceTokens.DeleteSnippetCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31123
31433
  });
31124
- snippetCmd.command("watch <id>").description("Watch a snippet").addHelpText("after", buildHelpText({ examples: ["bb snippet watch kypj"] })).action(async (id, options) => {
31434
+ snippetCmd.command("watch <id>").description("Watch a snippet").addHelpText("after", buildHelpText({
31435
+ examples: ["bb snippet watch kypj", "bb snippet watch kypj -w my-team"]
31436
+ })).action(async (id, options) => {
31125
31437
  const context = createContext(cli);
31126
31438
  await runCommand(ServiceTokens.WatchSnippetCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31127
31439
  });
31128
- snippetCmd.command("unwatch <id>").description("Stop watching a snippet").addHelpText("after", buildHelpText({ examples: ["bb snippet unwatch kypj"] })).action(async (id, options) => {
31440
+ snippetCmd.command("unwatch <id>").description("Stop watching a snippet").addHelpText("after", buildHelpText({
31441
+ examples: [
31442
+ "bb snippet unwatch kypj",
31443
+ "bb snippet unwatch kypj -w my-team"
31444
+ ]
31445
+ })).action(async (id, options) => {
31129
31446
  const context = createContext(cli);
31130
31447
  await runCommand(ServiceTokens.UnwatchSnippetCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31131
31448
  });
@@ -31140,20 +31457,31 @@ snippetCommentsCmd.command("list <id>").description("List comments on a snippet"
31140
31457
  const context = createContext(cli);
31141
31458
  await runCommand(ServiceTokens.ListSnippetCommentsCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31142
31459
  });
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) => {
31460
+ 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({
31461
+ examples: [
31462
+ 'bb snippet comments add kypj "Great snippet!"',
31463
+ 'bb snippet comments add kypj -m "Great snippet!"',
31464
+ 'bb snippet comments add kypj "Great snippet!" --json'
31465
+ ]
31466
+ })).action(async (id, message, options) => {
31146
31467
  const context = createContext(cli);
31147
- await runCommand(ServiceTokens.AddSnippetCommentCommand, withGlobalOptions({ id, ...options }, context), cli, context);
31468
+ const resolvedMessage = message ?? options.message;
31469
+ await runCommand(ServiceTokens.AddSnippetCommentCommand, withGlobalOptions({ id, ...options, message: resolvedMessage }, context), cli, context);
31148
31470
  });
31149
31471
  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"']
31472
+ examples: [
31473
+ 'bb snippet comments edit kypj 123 "Updated comment"',
31474
+ 'bb snippet comments edit kypj 123 "Updated comment" --json'
31475
+ ]
31151
31476
  })).action(async (snippetId, commentId, message, options) => {
31152
31477
  const context = createContext(cli);
31153
31478
  await runCommand(ServiceTokens.EditSnippetCommentCommand, withGlobalOptions({ snippetId, commentId, message }, context), cli, context);
31154
31479
  });
31155
31480
  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"]
31481
+ examples: [
31482
+ "bb snippet comments delete kypj 123",
31483
+ "bb snippet comments delete kypj 123 --yes"
31484
+ ]
31157
31485
  })).action(async (snippetId, commentId, options) => {
31158
31486
  const context = createContext(cli);
31159
31487
  await runCommand(ServiceTokens.DeleteSnippetCommentCommand, withGlobalOptions({ snippetId, commentId, ...options }, context), cli, context);
@@ -31168,7 +31496,8 @@ configCmd.command("get <key>").description("Get a configuration value").addHelpT
31168
31496
  "username",
31169
31497
  "defaultWorkspace",
31170
31498
  "skipVersionCheck",
31171
- "versionCheckInterval"
31499
+ "versionCheckInterval",
31500
+ "prCreateIncludeDefaultReviewers"
31172
31501
  ]
31173
31502
  }
31174
31503
  })).action(async (key) => {
@@ -31184,9 +31513,16 @@ configCmd.command("set <key> <value>").description("Set a configuration value").
31184
31513
  "Settable config keys": [
31185
31514
  "defaultWorkspace (string)",
31186
31515
  "skipVersionCheck (true/false)",
31187
- "versionCheckInterval (positive integer, seconds)"
31516
+ "versionCheckInterval (positive integer, seconds)",
31517
+ "prCreateIncludeDefaultReviewers (true/false)"
31188
31518
  ]
31189
- }
31519
+ },
31520
+ seeAlso: [
31521
+ {
31522
+ label: "Configuration File",
31523
+ url: "https://bitbucket-cli.paulvanderlei.com/reference/configuration/"
31524
+ }
31525
+ ]
31190
31526
  })).action(async (key, value) => {
31191
31527
  await runCommand(ServiceTokens.SetConfigCommand, { key, value }, cli);
31192
31528
  });
@@ -31197,10 +31533,20 @@ configCmd.command("list").description("List all configuration values").addHelpTe
31197
31533
  });
31198
31534
  cli.addCommand(configCmd);
31199
31535
  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 () => {
31536
+ completionCmd.command("install").description("Install shell completions for bash, zsh, or fish").addHelpText("after", buildHelpText({
31537
+ examples: ["bb completion install", "bb completion install --json"],
31538
+ validValues: {
31539
+ "Supported shells": ["bash", "zsh", "fish"]
31540
+ }
31541
+ })).action(async () => {
31201
31542
  await runCommand(ServiceTokens.InstallCompletionCommand, undefined, cli);
31202
31543
  });
31203
- completionCmd.command("uninstall").description("Uninstall shell completions").addHelpText("after", buildHelpText({ examples: ["bb completion uninstall"] })).action(async () => {
31544
+ completionCmd.command("uninstall").description("Uninstall shell completions").addHelpText("after", buildHelpText({
31545
+ examples: ["bb completion uninstall", "bb completion uninstall --json"],
31546
+ validValues: {
31547
+ "Supported shells": ["bash", "zsh", "fish"]
31548
+ }
31549
+ })).action(async () => {
31204
31550
  await runCommand(ServiceTokens.UninstallCompletionCommand, undefined, cli);
31205
31551
  });
31206
31552
  cli.addCommand(completionCmd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilatos/bitbucket-cli",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "A command-line interface for Bitbucket Cloud",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -30,6 +30,7 @@
30
30
  "build": "bun build src/index.ts --outdir dist --target bun --external tabtab",
31
31
  "test": "bun test",
32
32
  "lint": "tsc --noEmit",
33
+ "lint:docs": "bun scripts/check-error-codes-docs.ts && bun scripts/check-env-vars-docs.ts",
33
34
  "format": "prettier --write .",
34
35
  "format:check": "prettier --check .",
35
36
  "generate:api": "openapi-generator-cli generate && bun scripts/patch-generated.ts",