@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.
- package/README.md +51 -8
- package/dist/index.js +480 -134
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -47,15 +47,28 @@
|
|
|
47
47
|
|
|
48
48
|
## Install
|
|
49
49
|
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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 > 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, {
|
|
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
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
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'
|
|
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'
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
17894
|
+
context: { workspace: options.workspace, repoSlug: options.repo },
|
|
17895
|
+
reason: null,
|
|
17896
|
+
remoteUrl: null
|
|
17833
17897
|
};
|
|
17834
17898
|
}
|
|
17835
|
-
const
|
|
17899
|
+
const gitResult = await this.inspectGitRepoContext();
|
|
17900
|
+
const gitContext = gitResult.context;
|
|
17836
17901
|
if (options.workspace && gitContext) {
|
|
17837
17902
|
return {
|
|
17838
|
-
|
|
17839
|
-
|
|
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
|
-
|
|
17916
|
+
context: { workspace, repoSlug: options.repo },
|
|
17917
|
+
reason: null,
|
|
17918
|
+
remoteUrl: gitResult.remoteUrl
|
|
17849
17919
|
};
|
|
17850
17920
|
}
|
|
17851
17921
|
}
|
|
17852
|
-
return
|
|
17922
|
+
return gitResult;
|
|
17853
17923
|
}
|
|
17854
17924
|
async requireRepoContext(options) {
|
|
17855
|
-
const
|
|
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:
|
|
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
|
|
18446
|
-
|
|
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 =
|
|
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
|
|
18453
|
-
const formattedRow = row.map((cell, index) =>
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
22664
|
-
context: { status: response.status
|
|
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
|
-
|
|
22678
|
-
|
|
22679
|
-
|
|
22680
|
-
|
|
22681
|
-
|
|
22682
|
-
|
|
22683
|
-
|
|
22684
|
-
|
|
22685
|
-
|
|
22686
|
-
|
|
22687
|
-
|
|
22688
|
-
|
|
22689
|
-
|
|
22690
|
-
|
|
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
|
-
}
|
|
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:
|
|
22817
|
-
context: { status: response.status
|
|
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:
|
|
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
|
|
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
|
-
|
|
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({
|
|
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'
|
|
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
|
|
28726
|
-
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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}' -
|
|
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
|
-
|
|
30374
|
+
} else {
|
|
30375
|
+
this.output.table(["KEY", "VALUE"], rows);
|
|
30168
30376
|
}
|
|
30169
|
-
this.output.
|
|
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:
|
|
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:
|
|
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>",
|
|
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("
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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: [
|
|
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: [
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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: [
|
|
31145
|
-
|
|
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
|
-
|
|
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: [
|
|
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: [
|
|
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({
|
|
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({
|
|
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.
|
|
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",
|