@pilatos/bitbucket-cli 1.7.1 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +507 -123
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -44211,7 +44211,7 @@ var ServiceTokens = {
44211
44211
  };
44212
44212
 
44213
44213
  // src/services/config.service.ts
44214
- import { join } from "path";
44214
+ import { join, win32 } from "path";
44215
44215
  import { homedir } from "os";
44216
44216
 
44217
44217
  // src/types/errors.ts
@@ -44282,9 +44282,24 @@ class ConfigService {
44282
44282
  configDir;
44283
44283
  configFile;
44284
44284
  configCache = null;
44285
- constructor(configDir) {
44286
- this.configDir = configDir ?? join(homedir(), ".config", "bb");
44287
- this.configFile = join(this.configDir, "config.json");
44285
+ constructor(configDir, options = {}) {
44286
+ const platform = options.platform ?? process.platform;
44287
+ const joinPath = platform === "win32" ? win32.join : join;
44288
+ this.configDir = configDir ?? this.resolveDefaultConfigDir({ ...options, platform });
44289
+ this.configFile = joinPath(this.configDir, "config.json");
44290
+ }
44291
+ resolveDefaultConfigDir(options) {
44292
+ const platform = options.platform ?? process.platform;
44293
+ if (platform === "win32") {
44294
+ const appDataDir = options.appData ?? process.env.APPDATA;
44295
+ if (appDataDir) {
44296
+ return win32.join(appDataDir, "bb");
44297
+ }
44298
+ const homeDir2 = options.homeDir ?? homedir();
44299
+ return win32.join(homeDir2, "AppData", "Roaming", "bb");
44300
+ }
44301
+ const homeDir = options.homeDir ?? homedir();
44302
+ return join(homeDir, ".config", "bb");
44288
44303
  }
44289
44304
  async ensureConfigDir() {
44290
44305
  try {
@@ -44354,6 +44369,11 @@ class ConfigService {
44354
44369
  apiToken: credentials.apiToken
44355
44370
  });
44356
44371
  }
44372
+ async clearCredentials() {
44373
+ const config = await this.getConfig();
44374
+ const { username: _username, apiToken: _apiToken, ...rest } = config;
44375
+ await this.setConfig(rest);
44376
+ }
44357
44377
  async clearConfig() {
44358
44378
  this.configCache = null;
44359
44379
  await this.setConfig({});
@@ -45018,6 +45038,9 @@ class OutputService {
45018
45038
  json(data) {
45019
45039
  console.log(JSON.stringify(data, null, 2));
45020
45040
  }
45041
+ jsonError(data) {
45042
+ console.error(JSON.stringify(data));
45043
+ }
45021
45044
  table(headers, rows) {
45022
45045
  if (rows.length === 0) {
45023
45046
  return;
@@ -45103,6 +45126,109 @@ class OutputService {
45103
45126
  return this.format(text, source_default.underline);
45104
45127
  }
45105
45128
  }
45129
+ // src/types/config.ts
45130
+ var SETTABLE_CONFIG_KEYS = [
45131
+ "defaultWorkspace",
45132
+ "skipVersionCheck",
45133
+ "versionCheckInterval"
45134
+ ];
45135
+ var READABLE_CONFIG_KEYS = [
45136
+ "username",
45137
+ "defaultWorkspace",
45138
+ "skipVersionCheck",
45139
+ "versionCheckInterval"
45140
+ ];
45141
+ function isSettableConfigKey(key) {
45142
+ return SETTABLE_CONFIG_KEYS.includes(key);
45143
+ }
45144
+ function isReadableConfigKey(key) {
45145
+ return READABLE_CONFIG_KEYS.includes(key);
45146
+ }
45147
+ function parseBooleanLiteral(value) {
45148
+ const normalized = value.trim().toLowerCase();
45149
+ if (normalized === "true") {
45150
+ return true;
45151
+ }
45152
+ if (normalized === "false") {
45153
+ return false;
45154
+ }
45155
+ return;
45156
+ }
45157
+ function parsePositiveIntegerLiteral(value) {
45158
+ const normalized = value.trim();
45159
+ if (!/^[1-9]\d*$/.test(normalized)) {
45160
+ return;
45161
+ }
45162
+ const parsed = Number.parseInt(normalized, 10);
45163
+ if (!Number.isSafeInteger(parsed)) {
45164
+ return;
45165
+ }
45166
+ return parsed;
45167
+ }
45168
+ function parseSettableConfigValue(key, value) {
45169
+ switch (key) {
45170
+ case "defaultWorkspace":
45171
+ return value;
45172
+ case "skipVersionCheck": {
45173
+ const parsed = parseBooleanLiteral(value);
45174
+ if (parsed === undefined) {
45175
+ throw new BBError({
45176
+ code: 5002 /* VALIDATION_INVALID */,
45177
+ message: "Invalid value for 'skipVersionCheck'. Expected 'true' or 'false'.",
45178
+ context: { key, value }
45179
+ });
45180
+ }
45181
+ return parsed;
45182
+ }
45183
+ case "versionCheckInterval": {
45184
+ const parsed = parsePositiveIntegerLiteral(value);
45185
+ if (parsed === undefined) {
45186
+ throw new BBError({
45187
+ code: 5002 /* VALIDATION_INVALID */,
45188
+ message: "Invalid value for 'versionCheckInterval'. Expected a positive integer (1 or greater).",
45189
+ context: { key, value }
45190
+ });
45191
+ }
45192
+ return parsed;
45193
+ }
45194
+ }
45195
+ }
45196
+ function coerceSkipVersionCheckValue(value) {
45197
+ if (typeof value === "boolean") {
45198
+ return value;
45199
+ }
45200
+ if (typeof value === "string") {
45201
+ return parseBooleanLiteral(value);
45202
+ }
45203
+ return;
45204
+ }
45205
+ function coerceVersionCheckIntervalValue(value) {
45206
+ if (typeof value === "number") {
45207
+ if (!Number.isSafeInteger(value) || value < 1) {
45208
+ return;
45209
+ }
45210
+ return value;
45211
+ }
45212
+ if (typeof value === "string") {
45213
+ return parsePositiveIntegerLiteral(value);
45214
+ }
45215
+ return;
45216
+ }
45217
+ function normalizeReadableConfigValue(key, value) {
45218
+ if (value === undefined || value === null) {
45219
+ return;
45220
+ }
45221
+ switch (key) {
45222
+ case "skipVersionCheck":
45223
+ return coerceSkipVersionCheckValue(value);
45224
+ case "versionCheckInterval":
45225
+ return coerceVersionCheckIntervalValue(value);
45226
+ case "username":
45227
+ case "defaultWorkspace":
45228
+ return typeof value === "string" ? value : undefined;
45229
+ }
45230
+ }
45231
+
45106
45232
  // src/services/version.service.ts
45107
45233
  var NPM_REGISTRY_URL = "https://registry.npmjs.org/@pilatos/bitbucket-cli";
45108
45234
  var PACKAGE_NAME = "@pilatos/bitbucket-cli";
@@ -45115,7 +45241,8 @@ class VersionService {
45115
45241
  this.currentVersion = currentVersion;
45116
45242
  }
45117
45243
  async checkForUpdate() {
45118
- const skipCheck = await this.configService.getValue("skipVersionCheck");
45244
+ const skipCheckRaw = await this.configService.getValue("skipVersionCheck");
45245
+ const skipCheck = coerceSkipVersionCheckValue(skipCheckRaw);
45119
45246
  if (skipCheck === true) {
45120
45247
  return null;
45121
45248
  }
@@ -45148,7 +45275,7 @@ class VersionService {
45148
45275
  const now = new Date;
45149
45276
  const timeSinceLastCheck = now.getTime() - lastCheckDate.getTime();
45150
45277
  const intervalDays = await this.configService.getValue("versionCheckInterval");
45151
- const days = typeof intervalDays === "number" ? intervalDays : 1;
45278
+ const days = coerceVersionCheckIntervalValue(intervalDays) ?? 1;
45152
45279
  const intervalMs = days * 24 * 60 * 60 * 1000;
45153
45280
  return timeSinceLastCheck >= intervalMs;
45154
45281
  }
@@ -51518,7 +51645,9 @@ class BaseCommand {
51518
51645
  }
51519
51646
  }
51520
51647
  handleError(error, context) {
51521
- if (error instanceof Error) {
51648
+ if (context.globalOptions.json) {
51649
+ this.output.jsonError(this.normalizeErrorForJson(error));
51650
+ } else if (error instanceof Error) {
51522
51651
  this.output.error(error.message);
51523
51652
  } else {
51524
51653
  this.output.error(String(error));
@@ -51527,6 +51656,23 @@ class BaseCommand {
51527
51656
  process.exitCode = 1;
51528
51657
  }
51529
51658
  }
51659
+ normalizeErrorForJson(error) {
51660
+ if (error instanceof BBError) {
51661
+ return error.toJSON();
51662
+ }
51663
+ if (error instanceof Error) {
51664
+ return {
51665
+ name: error.name,
51666
+ code: 9999 /* UNKNOWN */,
51667
+ message: error.message
51668
+ };
51669
+ }
51670
+ return {
51671
+ name: "Error",
51672
+ code: 9999 /* UNKNOWN */,
51673
+ message: String(error)
51674
+ };
51675
+ }
51530
51676
  requireOption(value, name, message) {
51531
51677
  if (value === undefined || value === null || value === "") {
51532
51678
  throw new BBError({
@@ -51553,10 +51699,16 @@ class LoginCommand extends BaseCommand {
51553
51699
  const username = options.username || process.env.BB_USERNAME;
51554
51700
  const apiToken = options.password || process.env.BB_API_TOKEN;
51555
51701
  if (!username) {
51556
- throw new Error("Username is required. Use --username option or set BB_USERNAME environment variable.");
51702
+ throw new BBError({
51703
+ code: 5001 /* VALIDATION_REQUIRED */,
51704
+ message: "Username is required. Use --username option or set BB_USERNAME environment variable."
51705
+ });
51557
51706
  }
51558
51707
  if (!apiToken) {
51559
- throw new Error("API token is required. Use --password option or set BB_API_TOKEN environment variable.");
51708
+ throw new BBError({
51709
+ code: 5001 /* VALIDATION_REQUIRED */,
51710
+ message: "API token is required. Use --password option or set BB_API_TOKEN environment variable."
51711
+ });
51560
51712
  }
51561
51713
  await this.configService.setCredentials({ username, apiToken });
51562
51714
  try {
@@ -51575,8 +51727,11 @@ class LoginCommand extends BaseCommand {
51575
51727
  }
51576
51728
  this.output.success(`Logged in as ${user.display_name} (${user.username})`);
51577
51729
  } catch (error) {
51578
- await this.configService.clearConfig();
51579
- throw new Error(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`);
51730
+ await this.configService.clearCredentials();
51731
+ throw new BBError({
51732
+ code: 1002 /* AUTH_INVALID */,
51733
+ message: `Authentication failed: ${error instanceof Error ? error.message : String(error)}`
51734
+ });
51580
51735
  }
51581
51736
  }
51582
51737
  }
@@ -51591,7 +51746,7 @@ class LogoutCommand extends BaseCommand {
51591
51746
  this.configService = configService;
51592
51747
  }
51593
51748
  async execute(_options, context) {
51594
- await this.configService.clearConfig();
51749
+ await this.configService.clearCredentials();
51595
51750
  if (context.globalOptions.json) {
51596
51751
  this.output.json({ authenticated: false, success: true });
51597
51752
  return;
@@ -51645,7 +51800,11 @@ class StatusCommand extends BaseCommand {
51645
51800
  this.output.text(` Default workspace: ${this.output.highlight(config.defaultWorkspace)}`);
51646
51801
  }
51647
51802
  } catch (error) {
51648
- throw new Error(`Authentication is invalid or expired. Run ${this.output.highlight("bb auth login")} to re-authenticate.`);
51803
+ throw new BBError({
51804
+ code: 1002 /* AUTH_INVALID */,
51805
+ message: `Authentication is invalid or expired. Run ${this.output.highlight("bb auth login")} to re-authenticate.`,
51806
+ cause: error instanceof Error ? error : undefined
51807
+ });
51649
51808
  }
51650
51809
  }
51651
51810
  }
@@ -51662,7 +51821,10 @@ class TokenCommand extends BaseCommand {
51662
51821
  async execute(_options, context) {
51663
51822
  const credentials = await this.configService.getCredentials();
51664
51823
  if (!credentials.username || !credentials.apiToken) {
51665
- throw new Error("Not authenticated. Run 'bb auth login' first.");
51824
+ throw new BBError({
51825
+ code: 1001 /* AUTH_REQUIRED */,
51826
+ message: "Not authenticated. Run 'bb auth login' first."
51827
+ });
51666
51828
  }
51667
51829
  const token = Buffer.from(`${credentials.username}:${credentials.apiToken}`).toString("base64");
51668
51830
  if (context.globalOptions.json) {
@@ -51710,7 +51872,10 @@ class CloneCommand extends BaseCommand {
51710
51872
  if (parts.length === 1) {
51711
51873
  const config = await this.configService.getConfig();
51712
51874
  if (!config.defaultWorkspace) {
51713
- throw new Error("No workspace specified. Use workspace/repo format or set a default workspace.");
51875
+ throw new BBError({
51876
+ code: 6002 /* CONTEXT_WORKSPACE_NOT_FOUND */,
51877
+ message: "No workspace specified. Use workspace/repo format or set a default workspace."
51878
+ });
51714
51879
  }
51715
51880
  workspace = config.defaultWorkspace;
51716
51881
  repoSlug = parts[0];
@@ -51718,7 +51883,11 @@ class CloneCommand extends BaseCommand {
51718
51883
  workspace = parts[0];
51719
51884
  repoSlug = parts[1];
51720
51885
  } else {
51721
- throw new Error("Invalid repository format. Use workspace/repo or a full URL.");
51886
+ throw new BBError({
51887
+ code: 5002 /* VALIDATION_INVALID */,
51888
+ message: "Invalid repository format. Use workspace/repo or a full URL.",
51889
+ context: { repository }
51890
+ });
51722
51891
  }
51723
51892
  return `git@bitbucket.org:${workspace}/${repoSlug}.git`;
51724
51893
  }
@@ -51726,7 +51895,11 @@ class CloneCommand extends BaseCommand {
51726
51895
  const parts = repository.split("/");
51727
51896
  const lastPart = parts.at(-1);
51728
51897
  if (!lastPart) {
51729
- throw new Error("Invalid repository format.");
51898
+ throw new BBError({
51899
+ code: 5002 /* VALIDATION_INVALID */,
51900
+ message: "Invalid repository format.",
51901
+ context: { repository }
51902
+ });
51730
51903
  }
51731
51904
  return lastPart.replace(".git", "");
51732
51905
  }
@@ -51782,12 +51955,61 @@ class CreateRepoCommand extends BaseCommand {
51782
51955
  }
51783
51956
  const config = await this.configService.getConfig();
51784
51957
  if (!config.defaultWorkspace) {
51785
- throw new Error("No workspace specified. Use --workspace option or set a default workspace.");
51958
+ throw new BBError({
51959
+ code: 6002 /* CONTEXT_WORKSPACE_NOT_FOUND */,
51960
+ message: "No workspace specified. Use --workspace option or set a default workspace."
51961
+ });
51786
51962
  }
51787
51963
  return config.defaultWorkspace;
51788
51964
  }
51789
51965
  }
51790
51966
 
51967
+ // src/services/pagination.ts
51968
+ var DEFAULT_LIMIT = 25;
51969
+ var MAX_PAGE_LENGTH = 50;
51970
+ function parseLimit(limit, fallback = DEFAULT_LIMIT) {
51971
+ if (!limit) {
51972
+ return fallback;
51973
+ }
51974
+ const parsed = Number.parseInt(limit, 10);
51975
+ if (Number.isNaN(parsed) || parsed <= 0) {
51976
+ return fallback;
51977
+ }
51978
+ return parsed;
51979
+ }
51980
+ async function collectPages(options) {
51981
+ const { fetchPage, shouldInclude } = options;
51982
+ const limit = Math.max(0, options.limit);
51983
+ if (limit === 0) {
51984
+ return [];
51985
+ }
51986
+ const requestedPageSize = options.pageSize ?? limit;
51987
+ const pagelen = Math.max(1, Math.min(requestedPageSize, MAX_PAGE_LENGTH));
51988
+ const items = [];
51989
+ let page = 1;
51990
+ while (items.length < limit) {
51991
+ const data = await fetchPage(page, pagelen);
51992
+ const pageValues = data.values ? Array.from(data.values) : [];
51993
+ if (pageValues.length === 0) {
51994
+ break;
51995
+ }
51996
+ for (const value of pageValues) {
51997
+ if (shouldInclude && !shouldInclude(value)) {
51998
+ continue;
51999
+ }
52000
+ items.push(value);
52001
+ if (items.length >= limit) {
52002
+ return items;
52003
+ }
52004
+ }
52005
+ if (!data.next) {
52006
+ break;
52007
+ }
52008
+ page += 1;
52009
+ }
52010
+ return items;
52011
+ }
52012
+
51791
52013
  // src/commands/repo/list.command.ts
51792
52014
  class ListReposCommand extends BaseCommand {
51793
52015
  repositoriesApi;
@@ -51801,11 +52023,18 @@ class ListReposCommand extends BaseCommand {
51801
52023
  }
51802
52024
  async execute(options, context) {
51803
52025
  const workspace = await this.resolveWorkspace(options.workspace ?? context.globalOptions.workspace);
51804
- const limit = Number.parseInt(options.limit || "25", 10);
51805
- const response = await this.repositoriesApi.repositoriesWorkspaceGet({
51806
- workspace
52026
+ const limit = parseLimit(options.limit);
52027
+ const repos = await collectPages({
52028
+ limit,
52029
+ fetchPage: async (page, pagelen) => {
52030
+ const response = await this.repositoriesApi.repositoriesWorkspaceGet({
52031
+ workspace
52032
+ }, {
52033
+ params: { page, pagelen }
52034
+ });
52035
+ return response.data;
52036
+ }
51807
52037
  });
51808
- const repos = Array.from(response.data.values ?? []).slice(0, limit);
51809
52038
  if (context.globalOptions.json) {
51810
52039
  this.output.json({
51811
52040
  workspace,
@@ -51831,7 +52060,10 @@ class ListReposCommand extends BaseCommand {
51831
52060
  }
51832
52061
  const config = await this.configService.getConfig();
51833
52062
  if (!config.defaultWorkspace) {
51834
- throw new Error("No workspace specified. Use --workspace option or set a default workspace.");
52063
+ throw new BBError({
52064
+ code: 6002 /* CONTEXT_WORKSPACE_NOT_FOUND */,
52065
+ message: "No workspace specified. Use --workspace option or set a default workspace."
52066
+ });
51835
52067
  }
51836
52068
  return config.defaultWorkspace;
51837
52069
  }
@@ -51916,8 +52148,11 @@ class DeleteRepoCommand extends BaseCommand {
51916
52148
  }
51917
52149
  const repoContext = await this.contextService.requireRepoContext(contextOptions);
51918
52150
  if (!yes) {
51919
- throw new Error(`This will permanently delete ${repoContext.workspace}/${repoContext.repoSlug}.
51920
- ` + "Use --yes to confirm deletion.");
52151
+ throw new BBError({
52152
+ code: 5001 /* VALIDATION_REQUIRED */,
52153
+ message: `This will permanently delete ${repoContext.workspace}/${repoContext.repoSlug}.
52154
+ ` + "Use --yes to confirm deletion."
52155
+ });
51921
52156
  }
51922
52157
  await this.repositoriesApi.repositoriesWorkspaceRepoSlugDelete({
51923
52158
  workspace: repoContext.workspace,
@@ -51950,7 +52185,10 @@ class CreatePRCommand extends BaseCommand {
51950
52185
  }
51951
52186
  async execute(options, context) {
51952
52187
  if (!options.title) {
51953
- throw new Error("Pull request title is required. Use --title option.");
52188
+ throw new BBError({
52189
+ code: 5001 /* VALIDATION_REQUIRED */,
52190
+ message: "Pull request title is required. Use --title option."
52191
+ });
51954
52192
  }
51955
52193
  const repoContext = await this.contextService.requireRepoContext({
51956
52194
  ...context.globalOptions,
@@ -52014,13 +52252,20 @@ class ListPRsCommand extends BaseCommand {
52014
52252
  ...options
52015
52253
  });
52016
52254
  const state = options.state || "OPEN";
52017
- const response = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsGet({
52018
- workspace: repoContext.workspace,
52019
- repoSlug: repoContext.repoSlug,
52020
- state
52255
+ const limit = parseLimit(options.limit);
52256
+ const values = await collectPages({
52257
+ limit,
52258
+ fetchPage: async (page, pagelen) => {
52259
+ const response = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsGet({
52260
+ workspace: repoContext.workspace,
52261
+ repoSlug: repoContext.repoSlug,
52262
+ state
52263
+ }, {
52264
+ params: { page, pagelen }
52265
+ });
52266
+ return response.data;
52267
+ }
52021
52268
  });
52022
- const data = response.data;
52023
- const values = data.values ? Array.from(data.values) : [];
52024
52269
  if (context.globalOptions.json) {
52025
52270
  this.output.json({
52026
52271
  workspace: repoContext.workspace,
@@ -52203,18 +52448,31 @@ class EditPRCommand extends BaseCommand {
52203
52448
  prId = Number.parseInt(options.id, 10);
52204
52449
  } else {
52205
52450
  const currentBranch = await this.gitService.getCurrentBranch();
52206
- const prsResponse = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsGet({
52207
- workspace: repoContext.workspace,
52208
- repoSlug: repoContext.repoSlug,
52209
- state: "OPEN"
52210
- });
52211
- const values = prsResponse.data.values ? Array.from(prsResponse.data.values) : [];
52212
- const matchingPR = values.find((pr2) => {
52213
- const source = pr2.source;
52214
- return source?.branch?.name === currentBranch;
52451
+ const matches = await collectPages({
52452
+ limit: 1,
52453
+ pageSize: MAX_PAGE_LENGTH,
52454
+ fetchPage: async (page, pagelen) => {
52455
+ const response2 = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsGet({
52456
+ workspace: repoContext.workspace,
52457
+ repoSlug: repoContext.repoSlug,
52458
+ state: "OPEN"
52459
+ }, {
52460
+ params: { page, pagelen }
52461
+ });
52462
+ return response2.data;
52463
+ },
52464
+ shouldInclude: (pullRequest) => {
52465
+ const source = pullRequest.source;
52466
+ return source?.branch?.name === currentBranch;
52467
+ }
52215
52468
  });
52469
+ const matchingPR = matches[0];
52216
52470
  if (!matchingPR) {
52217
- throw new Error(`No open pull request found for current branch '${currentBranch}'. Specify a PR ID explicitly.`);
52471
+ throw new BBError({
52472
+ code: 2002 /* API_NOT_FOUND */,
52473
+ message: `No open pull request found for current branch '${currentBranch}'. Specify a PR ID explicitly.`,
52474
+ context: { branch: currentBranch }
52475
+ });
52218
52476
  }
52219
52477
  prId = matchingPR.id;
52220
52478
  }
@@ -52223,11 +52481,19 @@ class EditPRCommand extends BaseCommand {
52223
52481
  try {
52224
52482
  body = fs.readFileSync(options.bodyFile, "utf-8");
52225
52483
  } catch (err) {
52226
- throw new Error(`Failed to read file '${options.bodyFile}': ${err instanceof Error ? err.message : "Unknown error"}`);
52484
+ throw new BBError({
52485
+ code: 9999 /* UNKNOWN */,
52486
+ message: `Failed to read file '${options.bodyFile}': ${err instanceof Error ? err.message : "Unknown error"}`,
52487
+ cause: err instanceof Error ? err : undefined,
52488
+ context: { bodyFile: options.bodyFile }
52489
+ });
52227
52490
  }
52228
52491
  }
52229
52492
  if (!options.title && !body) {
52230
- throw new Error("At least one of --title or --body (or --body-file) is required.");
52493
+ throw new BBError({
52494
+ code: 5001 /* VALIDATION_REQUIRED */,
52495
+ message: "At least one of --title or --body (or --body-file) is required."
52496
+ });
52231
52497
  }
52232
52498
  const request = {
52233
52499
  type: "pullrequest"
@@ -52444,7 +52710,11 @@ class CheckoutPRCommand extends BaseCommand {
52444
52710
  const branchName = pr.source?.branch?.name;
52445
52711
  const localBranchName = `pr-${prId}`;
52446
52712
  if (!branchName) {
52447
- throw new Error("Pull request source branch not found");
52713
+ throw new BBError({
52714
+ code: 2002 /* API_NOT_FOUND */,
52715
+ message: "Pull request source branch not found",
52716
+ context: { pullRequestId: prId }
52717
+ });
52448
52718
  }
52449
52719
  await this.gitService.fetch();
52450
52720
  let checkedOutBranch;
@@ -52505,18 +52775,39 @@ class DiffPRCommand extends BaseCommand {
52505
52775
  if (options.id) {
52506
52776
  prId = Number.parseInt(options.id, 10);
52507
52777
  if (Number.isNaN(prId)) {
52508
- throw new TypeError("Invalid PR ID");
52778
+ throw new BBError({
52779
+ code: 5002 /* VALIDATION_INVALID */,
52780
+ message: "Invalid PR ID",
52781
+ context: { id: options.id }
52782
+ });
52509
52783
  }
52510
52784
  } else {
52511
52785
  const currentBranch = await this.gitService.getCurrentBranch();
52512
- const prsResponse = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsGet({
52513
- workspace: repoContext.workspace,
52514
- repoSlug: repoContext.repoSlug,
52515
- state: "OPEN"
52786
+ const matches = await collectPages({
52787
+ limit: 1,
52788
+ pageSize: MAX_PAGE_LENGTH,
52789
+ fetchPage: async (page, pagelen) => {
52790
+ const response = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsGet({
52791
+ workspace: repoContext.workspace,
52792
+ repoSlug: repoContext.repoSlug,
52793
+ state: "OPEN"
52794
+ }, {
52795
+ params: { page, pagelen }
52796
+ });
52797
+ return response.data;
52798
+ },
52799
+ shouldInclude: (pullRequest) => {
52800
+ const source = pullRequest.source;
52801
+ return source?.branch?.name === currentBranch;
52802
+ }
52516
52803
  });
52517
- const pr = Array.from(prsResponse.data.values ?? []).find((p) => p.source?.branch?.name === currentBranch);
52804
+ const pr = matches[0];
52518
52805
  if (!pr) {
52519
- throw new Error(`No open pull request found for branch "${currentBranch}"`);
52806
+ throw new BBError({
52807
+ code: 2002 /* API_NOT_FOUND */,
52808
+ message: `No open pull request found for branch "${currentBranch}"`,
52809
+ context: { branch: currentBranch }
52810
+ });
52520
52811
  }
52521
52812
  prId = pr.id;
52522
52813
  }
@@ -52591,11 +52882,13 @@ class DiffPRCommand extends BaseCommand {
52591
52882
  repoSlug,
52592
52883
  pullRequestId: prId
52593
52884
  });
52594
- const diffUrl = prResponse.data.links?.diff?.href;
52595
- if (!diffUrl) {
52596
- throw new Error("Could not get diff URL");
52885
+ const links = prResponse.data.links;
52886
+ const htmlUrl = links?.html?.href;
52887
+ if (htmlUrl) {
52888
+ const normalizedHtmlUrl = htmlUrl.replace(/\/$/, "");
52889
+ return `${normalizedHtmlUrl}/diff`;
52597
52890
  }
52598
- return diffUrl.replace(/api\.bitbucket\.org\/2\.0\/repositories\/(.*?)\/pullrequests\/(\d+)\/diff/, "bitbucket.org/$1/pull-requests/$2/diff");
52891
+ return `https://bitbucket.org/${workspace}/${repoSlug}/pull-requests/${prId}/diff`;
52599
52892
  }
52600
52893
  async showStat(workspace, repoSlug, prId, useJson) {
52601
52894
  const diffstatResponse = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsPullRequestIdDiffstatGet({
@@ -52718,15 +53011,28 @@ class ActivityPRCommand extends BaseCommand {
52718
53011
  ...options
52719
53012
  });
52720
53013
  const prId = Number.parseInt(options.id, 10);
52721
- const response = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsPullRequestIdActivityGet({
52722
- workspace: repoContext.workspace,
52723
- repoSlug: repoContext.repoSlug,
52724
- pullRequestId: prId
52725
- });
52726
- const data = response.data;
52727
- const values = data?.values ? Array.from(data.values) : [];
52728
53014
  const filterTypes = this.parseTypeFilter(options.type);
52729
- const activities = filterTypes.length > 0 ? values.filter((activity) => filterTypes.includes(this.getActivityType(activity))) : values;
53015
+ const limit = parseLimit(options.limit);
53016
+ const activities = await collectPages({
53017
+ limit,
53018
+ fetchPage: async (page, pagelen) => {
53019
+ const response = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsPullRequestIdActivityGet({
53020
+ workspace: repoContext.workspace,
53021
+ repoSlug: repoContext.repoSlug,
53022
+ pullRequestId: prId
53023
+ }, {
53024
+ params: { page, pagelen }
53025
+ });
53026
+ return response.data;
53027
+ },
53028
+ shouldInclude: (activity) => {
53029
+ if (filterTypes.length === 0) {
53030
+ return true;
53031
+ }
53032
+ return filterTypes.includes(this.getActivityType(activity));
53033
+ }
53034
+ });
53035
+ const typedActivities = activities;
52730
53036
  if (context.globalOptions.json) {
52731
53037
  this.output.json({
52732
53038
  workspace: repoContext.workspace,
@@ -52735,12 +53041,12 @@ class ActivityPRCommand extends BaseCommand {
52735
53041
  filters: {
52736
53042
  types: filterTypes
52737
53043
  },
52738
- count: activities.length,
52739
- activities
53044
+ count: typedActivities.length,
53045
+ activities: typedActivities
52740
53046
  });
52741
53047
  return;
52742
53048
  }
52743
- if (activities.length === 0) {
53049
+ if (typedActivities.length === 0) {
52744
53050
  if (filterTypes.length > 0) {
52745
53051
  this.output.info("No activity entries matched the requested filter");
52746
53052
  } else {
@@ -52748,7 +53054,7 @@ class ActivityPRCommand extends BaseCommand {
52748
53054
  }
52749
53055
  return;
52750
53056
  }
52751
- const rows = activities.map((activity) => {
53057
+ const rows = typedActivities.map((activity) => {
52752
53058
  const activityType = this.getActivityType(activity);
52753
53059
  return [
52754
53060
  activityType.toUpperCase(),
@@ -52866,31 +53172,79 @@ class CommentPRCommand extends BaseCommand {
52866
53172
  this.contextService = contextService;
52867
53173
  }
52868
53174
  async execute(options, context) {
53175
+ if ((options.lineTo || options.lineFrom) && !options.file) {
53176
+ throw new BBError({
53177
+ code: 5001 /* VALIDATION_REQUIRED */,
53178
+ message: "--file is required when using --line-to or --line-from"
53179
+ });
53180
+ }
53181
+ if (options.file && !options.lineTo && !options.lineFrom) {
53182
+ throw new BBError({
53183
+ code: 5001 /* VALIDATION_REQUIRED */,
53184
+ message: "At least one of --line-to or --line-from is required when using --file"
53185
+ });
53186
+ }
53187
+ if (options.lineTo) {
53188
+ const parsed = Number.parseInt(options.lineTo, 10);
53189
+ if (Number.isNaN(parsed) || parsed < 1) {
53190
+ throw new BBError({
53191
+ code: 5002 /* VALIDATION_INVALID */,
53192
+ message: "--line-to must be a positive integer"
53193
+ });
53194
+ }
53195
+ }
53196
+ if (options.lineFrom) {
53197
+ const parsed = Number.parseInt(options.lineFrom, 10);
53198
+ if (Number.isNaN(parsed) || parsed < 1) {
53199
+ throw new BBError({
53200
+ code: 5002 /* VALIDATION_INVALID */,
53201
+ message: "--line-from must be a positive integer"
53202
+ });
53203
+ }
53204
+ }
52869
53205
  const repoContext = await this.contextService.requireRepoContext({
52870
53206
  ...context.globalOptions,
52871
53207
  ...options
52872
53208
  });
52873
53209
  const prId = Number.parseInt(options.id, 10);
53210
+ const inline = options.file ? {
53211
+ path: options.file,
53212
+ ...options.lineTo ? { to: Number.parseInt(options.lineTo, 10) } : {},
53213
+ ...options.lineFrom ? { from: Number.parseInt(options.lineFrom, 10) } : {}
53214
+ } : undefined;
53215
+ const body = {
53216
+ content: {
53217
+ raw: options.message
53218
+ },
53219
+ ...inline ? { inline } : {}
53220
+ };
52874
53221
  const response = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsPullRequestIdCommentsPost({
52875
53222
  workspace: repoContext.workspace,
52876
53223
  repoSlug: repoContext.repoSlug,
52877
53224
  pullRequestId: prId,
52878
- body: {
52879
- type: "pullrequest_comment",
52880
- content: {
52881
- raw: options.message
52882
- }
52883
- }
53225
+ body
52884
53226
  });
52885
53227
  if (context.globalOptions.json) {
52886
- this.output.json({
53228
+ const jsonOutput = {
52887
53229
  success: true,
52888
53230
  pullRequestId: prId,
52889
53231
  comment: response.data
52890
- });
53232
+ };
53233
+ if (inline) {
53234
+ jsonOutput.inline = inline;
53235
+ }
53236
+ this.output.json(jsonOutput);
52891
53237
  return;
52892
53238
  }
52893
- this.output.success(`Added comment to pull request #${prId}`);
53239
+ if (inline) {
53240
+ if (inline.to) {
53241
+ this.output.success(`Added inline comment on ${inline.path}:${inline.to} to pull request #${prId}`);
53242
+ } else {
53243
+ this.output.success(`Added inline comment on ${inline.path} (old line ${inline.from}) to pull request #${prId}`);
53244
+ }
53245
+ } else {
53246
+ this.output.success(`Added comment to pull request #${prId}`);
53247
+ }
52894
53248
  }
52895
53249
  }
52896
53250
 
@@ -52911,13 +53265,20 @@ class ListCommentsPRCommand extends BaseCommand {
52911
53265
  ...options
52912
53266
  });
52913
53267
  const prId = Number.parseInt(options.id, 10);
52914
- const response = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsPullRequestIdCommentsGet({
52915
- workspace: repoContext.workspace,
52916
- repoSlug: repoContext.repoSlug,
52917
- pullRequestId: prId
53268
+ const limit = parseLimit(options.limit);
53269
+ const values = await collectPages({
53270
+ limit,
53271
+ fetchPage: async (page, pagelen) => {
53272
+ const response = await this.pullrequestsApi.repositoriesWorkspaceRepoSlugPullrequestsPullRequestIdCommentsGet({
53273
+ workspace: repoContext.workspace,
53274
+ repoSlug: repoContext.repoSlug,
53275
+ pullRequestId: prId
53276
+ }, {
53277
+ params: { page, pagelen }
53278
+ });
53279
+ return response.data;
53280
+ }
52918
53281
  });
52919
- const data = response.data;
52920
- const values = data.values ? Array.from(data.values) : [];
52921
53282
  if (context.globalOptions.json) {
52922
53283
  this.output.json({
52923
53284
  pullRequestId: prId,
@@ -53306,25 +53667,6 @@ class ChecksPRCommand extends BaseCommand {
53306
53667
  }
53307
53668
  }
53308
53669
 
53309
- // src/types/config.ts
53310
- var SETTABLE_CONFIG_KEYS = [
53311
- "defaultWorkspace",
53312
- "skipVersionCheck",
53313
- "versionCheckInterval"
53314
- ];
53315
- var READABLE_CONFIG_KEYS = [
53316
- "username",
53317
- "defaultWorkspace",
53318
- "skipVersionCheck",
53319
- "versionCheckInterval"
53320
- ];
53321
- function isSettableConfigKey(key) {
53322
- return SETTABLE_CONFIG_KEYS.includes(key);
53323
- }
53324
- function isReadableConfigKey(key) {
53325
- return READABLE_CONFIG_KEYS.includes(key);
53326
- }
53327
-
53328
53670
  // src/commands/config/get.command.ts
53329
53671
  class GetConfigCommand extends BaseCommand {
53330
53672
  configService;
@@ -53338,12 +53680,21 @@ class GetConfigCommand extends BaseCommand {
53338
53680
  async execute(options, context) {
53339
53681
  const { key } = options;
53340
53682
  if (GetConfigCommand.HIDDEN_KEYS.includes(key)) {
53341
- throw new Error(`Cannot display '${key}' - use 'bb auth token' to get authentication credentials`);
53683
+ throw new BBError({
53684
+ code: 4003 /* CONFIG_INVALID_KEY */,
53685
+ message: `Cannot display '${key}' - use 'bb auth token' to get authentication credentials`,
53686
+ context: { key }
53687
+ });
53342
53688
  }
53343
53689
  if (!isReadableConfigKey(key)) {
53344
- throw new Error(`Unknown config key '${key}'. Valid keys: username, defaultWorkspace`);
53690
+ throw new BBError({
53691
+ code: 4003 /* CONFIG_INVALID_KEY */,
53692
+ message: `Unknown config key '${key}'. Valid keys: ${READABLE_CONFIG_KEYS.join(", ")}`,
53693
+ context: { key }
53694
+ });
53345
53695
  }
53346
- const value = await this.configService.getValue(key);
53696
+ const rawValue = await this.configService.getValue(key);
53697
+ const value = normalizeReadableConfigValue(key, rawValue);
53347
53698
  if (context.globalOptions.json) {
53348
53699
  this.output.json({
53349
53700
  key,
@@ -53351,7 +53702,7 @@ class GetConfigCommand extends BaseCommand {
53351
53702
  });
53352
53703
  return;
53353
53704
  }
53354
- this.output.text(String(value || ""));
53705
+ this.output.text(String(value ?? ""));
53355
53706
  }
53356
53707
  }
53357
53708
 
@@ -53368,21 +53719,30 @@ class SetConfigCommand extends BaseCommand {
53368
53719
  async execute(options, context) {
53369
53720
  const { key, value } = options;
53370
53721
  if (SetConfigCommand.PROTECTED_KEYS.includes(key)) {
53371
- throw new Error(`Cannot set '${key}' directly. Use 'bb auth login' to configure authentication.`);
53722
+ throw new BBError({
53723
+ code: 4003 /* CONFIG_INVALID_KEY */,
53724
+ message: `Cannot set '${key}' directly. Use 'bb auth login' to configure authentication.`,
53725
+ context: { key }
53726
+ });
53372
53727
  }
53373
53728
  if (!isSettableConfigKey(key)) {
53374
- throw new Error(`Unknown config key '${key}'. Valid keys: defaultWorkspace`);
53729
+ throw new BBError({
53730
+ code: 4003 /* CONFIG_INVALID_KEY */,
53731
+ message: `Unknown config key '${key}'. Valid keys: ${SETTABLE_CONFIG_KEYS.join(", ")}`,
53732
+ context: { key }
53733
+ });
53375
53734
  }
53376
- await this.configService.setValue(key, value);
53735
+ const parsedValue = parseSettableConfigValue(key, value);
53736
+ await this.configService.setValue(key, parsedValue);
53377
53737
  if (context.globalOptions.json) {
53378
53738
  this.output.json({
53379
53739
  success: true,
53380
53740
  key,
53381
- value
53741
+ value: parsedValue
53382
53742
  });
53383
53743
  return;
53384
53744
  }
53385
- this.output.success(`Set ${key} = ${value}`);
53745
+ this.output.success(`Set ${key} = ${parsedValue}`);
53386
53746
  }
53387
53747
  }
53388
53748
 
@@ -53397,21 +53757,37 @@ class ListConfigCommand extends BaseCommand {
53397
53757
  }
53398
53758
  async execute(_options, context) {
53399
53759
  const config = await this.configService.getConfig();
53400
- const displayConfig = {
53401
- username: config.username || "",
53402
- defaultWorkspace: config.defaultWorkspace || "",
53403
- apiToken: config.apiToken ? "********" : ""
53404
- };
53760
+ const displayConfig = {};
53761
+ if (config.username) {
53762
+ displayConfig.username = config.username;
53763
+ }
53764
+ if (config.defaultWorkspace) {
53765
+ displayConfig.defaultWorkspace = config.defaultWorkspace;
53766
+ }
53767
+ if (config.apiToken) {
53768
+ displayConfig.apiToken = "********";
53769
+ }
53770
+ const skipVersionCheck = coerceSkipVersionCheckValue(config.skipVersionCheck);
53771
+ if (skipVersionCheck !== undefined) {
53772
+ displayConfig.skipVersionCheck = skipVersionCheck;
53773
+ }
53774
+ const versionCheckInterval = coerceVersionCheckIntervalValue(config.versionCheckInterval);
53775
+ if (versionCheckInterval !== undefined) {
53776
+ displayConfig.versionCheckInterval = versionCheckInterval;
53777
+ }
53405
53778
  if (context.globalOptions.json) {
53406
53779
  this.output.json({
53407
53780
  configPath: this.configService.getConfigPath(),
53408
- config: Object.fromEntries(Object.entries(displayConfig).filter(([, value]) => value !== ""))
53781
+ config: displayConfig
53409
53782
  });
53410
53783
  return;
53411
53784
  }
53412
53785
  this.output.text(this.output.dim(`Config file: ${this.configService.getConfigPath()}`));
53413
53786
  this.output.text("");
53414
- const rows = Object.entries(displayConfig).filter(([, value]) => value !== "").map(([key, value]) => [key, value]);
53787
+ const rows = Object.entries(displayConfig).map(([key, value]) => [
53788
+ key,
53789
+ String(value)
53790
+ ]);
53415
53791
  if (rows.length === 0) {
53416
53792
  this.output.text("No configuration set");
53417
53793
  return;
@@ -53448,7 +53824,11 @@ class InstallCompletionCommand extends BaseCommand {
53448
53824
  this.output.success("Shell completions installed successfully!");
53449
53825
  this.output.text("Restart your shell or source your profile to enable completions.");
53450
53826
  } catch (error) {
53451
- throw new Error(`Failed to install completions: ${error}`);
53827
+ throw new BBError({
53828
+ code: 9999 /* UNKNOWN */,
53829
+ message: `Failed to install completions: ${error}`,
53830
+ cause: error instanceof Error ? error : undefined
53831
+ });
53452
53832
  }
53453
53833
  }
53454
53834
  }
@@ -53479,7 +53859,11 @@ class UninstallCompletionCommand extends BaseCommand {
53479
53859
  }
53480
53860
  this.output.success("Shell completions uninstalled successfully!");
53481
53861
  } catch (error) {
53482
- throw new Error(`Failed to uninstall completions: ${error}`);
53862
+ throw new BBError({
53863
+ code: 9999 /* UNKNOWN */,
53864
+ message: `Failed to uninstall completions: ${error}`,
53865
+ cause: error instanceof Error ? error : undefined
53866
+ });
53483
53867
  }
53484
53868
  }
53485
53869
  }
@@ -53898,7 +54282,7 @@ prCmd.command("checkout <id>").description("Checkout a pull request locally").ac
53898
54282
  const context = createContext(cli);
53899
54283
  await runCommand(ServiceTokens.CheckoutPRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
53900
54284
  });
53901
- prCmd.command("diff [id]").description("View pull request diff").option("--color <when>", "Colorize output (auto, always, never)", "auto").option("--name-only", "Show only names of changed files").option("--stat", "Show diffstat").option("-w, --web", "Open diff in web browser").action(async (id, options) => {
54285
+ prCmd.command("diff [id]").description("View pull request diff").option("--color <when>", "Colorize output (auto, always, never)", "auto").option("--name-only", "Show only names of changed files").option("--stat", "Show diffstat").option("--web", "Open diff in web browser").action(async (id, options) => {
53902
54286
  const context = createContext(cli);
53903
54287
  await runCommand(ServiceTokens.DiffPRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
53904
54288
  });
@@ -53907,9 +54291,9 @@ prCommentsCmd.command("list <id>").description("List comments on a pull request"
53907
54291
  const context = createContext(cli);
53908
54292
  await runCommand(ServiceTokens.ListCommentsPRCommand, withGlobalOptions({ id, ...options }, context), cli, context);
53909
54293
  });
53910
- prCommentsCmd.command("add <id> <message>").description("Add a comment to a pull request").action(async (id, message, options) => {
54294
+ prCommentsCmd.command("add <id> <message>").description("Add a comment to a pull request").option("--file <path>", "File path in the diff for inline comment").option("--line-to <number>", "Line number in the new file version").option("--line-from <number>", "Line number in the old file version").action(async (id, message, options) => {
53911
54295
  const context = createContext(cli);
53912
- await runCommand(ServiceTokens.CommentPRCommand, withGlobalOptions({ id, message }, context), cli, context);
54296
+ await runCommand(ServiceTokens.CommentPRCommand, withGlobalOptions({ id, message, ...options }, context), cli, context);
53913
54297
  });
53914
54298
  prCommentsCmd.command("edit <pr-id> <comment-id> <message>").description("Edit a comment on a pull request").action(async (prId, commentId, message, options) => {
53915
54299
  const context = createContext(cli);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilatos/bitbucket-cli",
3
- "version": "1.7.1",
3
+ "version": "1.8.1",
4
4
  "description": "A command-line interface for Bitbucket Cloud",
5
5
  "author": "",
6
6
  "license": "MIT",