@bragduck/cli 2.4.0 → 2.5.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.
@@ -392,7 +392,7 @@ var init_storage_service = __esm({
392
392
  });
393
393
 
394
394
  // src/utils/errors.ts
395
- var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError;
395
+ var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError;
396
396
  var init_errors = __esm({
397
397
  "src/utils/errors.ts"() {
398
398
  "use strict";
@@ -459,6 +459,12 @@ var init_errors = __esm({
459
459
  this.name = "GitHubError";
460
460
  }
461
461
  };
462
+ BitbucketError = class extends BragduckError {
463
+ constructor(message, details) {
464
+ super(message, "BITBUCKET_ERROR", details);
465
+ this.name = "BitbucketError";
466
+ }
467
+ };
462
468
  }
463
469
  });
464
470
 
@@ -1437,6 +1443,7 @@ init_storage_service();
1437
1443
  init_logger();
1438
1444
  import boxen from "boxen";
1439
1445
  import chalk3 from "chalk";
1446
+ import { input } from "@inquirer/prompts";
1440
1447
 
1441
1448
  // src/ui/theme.ts
1442
1449
  init_esm_shims();
@@ -1604,9 +1611,11 @@ async function authCommand(subcommand) {
1604
1611
  await authLogin();
1605
1612
  } else if (subcommand === "status") {
1606
1613
  await authStatus();
1614
+ } else if (subcommand === "bitbucket") {
1615
+ await authBitbucket();
1607
1616
  } else {
1608
1617
  logger.error(`Unknown auth subcommand: ${subcommand}`);
1609
- logger.info("Available subcommands: login, status");
1618
+ logger.info("Available subcommands: login, status, bitbucket");
1610
1619
  process.exit(1);
1611
1620
  }
1612
1621
  }
@@ -1700,6 +1709,84 @@ async function authStatus() {
1700
1709
  }
1701
1710
  logger.log("");
1702
1711
  }
1712
+ async function authBitbucket() {
1713
+ logger.log("");
1714
+ logger.log(
1715
+ boxen(
1716
+ theme.info("Bitbucket API Token Authentication") + "\n\nCreate an API Token at:\n" + colors.highlight("https://bitbucket.org/account/settings/api-token/new") + "\n\nRequired scopes:\n \u2022 pullrequest:read\n \u2022 repository:read\n \u2022 account:read\n\n" + theme.warning("Note: API tokens expire (max 1 year)"),
1717
+ boxStyles.info
1718
+ )
1719
+ );
1720
+ logger.log("");
1721
+ try {
1722
+ const email = await input({
1723
+ message: "Atlassian account email:",
1724
+ validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
1725
+ });
1726
+ const apiToken = await input({
1727
+ message: "API Token:",
1728
+ validate: (value) => value.length > 0 ? true : "API token cannot be empty"
1729
+ });
1730
+ const tokenExpiry = await input({
1731
+ message: "Token expiry date (YYYY-MM-DD, optional):",
1732
+ default: "",
1733
+ validate: (value) => {
1734
+ if (!value) return true;
1735
+ const date = new Date(value);
1736
+ return !isNaN(date.getTime()) ? true : "Please enter a valid date (YYYY-MM-DD)";
1737
+ }
1738
+ });
1739
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
1740
+ const response = await fetch("https://api.bitbucket.org/2.0/user", {
1741
+ headers: { Authorization: `Basic ${auth}` }
1742
+ });
1743
+ if (!response.ok) {
1744
+ logger.log("");
1745
+ logger.log(
1746
+ boxen(
1747
+ theme.error("\u2717 Authentication Failed") + "\n\nInvalid email or API token",
1748
+ boxStyles.error
1749
+ )
1750
+ );
1751
+ logger.log("");
1752
+ process.exit(1);
1753
+ }
1754
+ const user = await response.json();
1755
+ const credentials = {
1756
+ accessToken: apiToken,
1757
+ username: email
1758
+ // Store email in username field
1759
+ };
1760
+ if (tokenExpiry) {
1761
+ const expiryDate = new Date(tokenExpiry);
1762
+ credentials.expiresAt = expiryDate.getTime();
1763
+ }
1764
+ await storageService.setServiceCredentials("bitbucket", credentials);
1765
+ logger.log("");
1766
+ logger.log(
1767
+ boxen(
1768
+ theme.success("\u2713 Successfully authenticated with Bitbucket") + `
1769
+
1770
+ Email: ${email}
1771
+ User: ${user.display_name}
1772
+ ` + (tokenExpiry ? `Expires: ${tokenExpiry}` : ""),
1773
+ boxStyles.success
1774
+ )
1775
+ );
1776
+ logger.log("");
1777
+ } catch (error) {
1778
+ const err = error;
1779
+ logger.log("");
1780
+ logger.log(
1781
+ boxen(
1782
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
1783
+ boxStyles.error
1784
+ )
1785
+ );
1786
+ logger.log("");
1787
+ process.exit(1);
1788
+ }
1789
+ }
1703
1790
 
1704
1791
  // src/commands/sync.ts
1705
1792
  init_esm_shims();
@@ -1720,6 +1807,7 @@ import boxen6 from "boxen";
1720
1807
  // src/utils/source-detector.ts
1721
1808
  init_esm_shims();
1722
1809
  init_errors();
1810
+ init_storage_service();
1723
1811
  import { exec as exec2 } from "child_process";
1724
1812
  import { promisify as promisify2 } from "util";
1725
1813
  var execAsync2 = promisify2(exec2);
@@ -1821,6 +1909,8 @@ var SourceDetector = class {
1821
1909
  if (type === "github") {
1822
1910
  await execAsync2("command gh auth status");
1823
1911
  return true;
1912
+ } else if (type === "bitbucket" || type === "atlassian") {
1913
+ return await storageService.isServiceAuthenticated("bitbucket");
1824
1914
  }
1825
1915
  return false;
1826
1916
  } catch {
@@ -2318,6 +2408,288 @@ var GitHubSyncAdapter = class {
2318
2408
  };
2319
2409
  var githubSyncAdapter = new GitHubSyncAdapter();
2320
2410
 
2411
+ // src/sync/bitbucket-adapter.ts
2412
+ init_esm_shims();
2413
+
2414
+ // src/services/bitbucket.service.ts
2415
+ init_esm_shims();
2416
+ init_errors();
2417
+ init_logger();
2418
+ init_storage_service();
2419
+ import { exec as exec4 } from "child_process";
2420
+ import { promisify as promisify4 } from "util";
2421
+ var execAsync4 = promisify4(exec4);
2422
+ var BitbucketService = class {
2423
+ BITBUCKET_API_BASE = "https://api.bitbucket.org/2.0";
2424
+ MAX_DESCRIPTION_LENGTH = 5e3;
2425
+ /**
2426
+ * Get stored Bitbucket credentials
2427
+ */
2428
+ async getCredentials() {
2429
+ const creds = await storageService.getServiceCredentials("bitbucket");
2430
+ if (!creds || !creds.username || !creds.accessToken) {
2431
+ throw new BitbucketError("Not authenticated with Bitbucket", {
2432
+ hint: "Run: bragduck auth bitbucket"
2433
+ });
2434
+ }
2435
+ if (creds.expiresAt && creds.expiresAt < Date.now()) {
2436
+ throw new BitbucketError("API token has expired", {
2437
+ hint: "Run: bragduck auth bitbucket"
2438
+ });
2439
+ }
2440
+ return {
2441
+ email: creds.username,
2442
+ // username field stores email
2443
+ apiToken: creds.accessToken
2444
+ };
2445
+ }
2446
+ /**
2447
+ * Make authenticated request to Bitbucket API
2448
+ */
2449
+ async request(endpoint) {
2450
+ const { email, apiToken } = await this.getCredentials();
2451
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
2452
+ logger.debug(`Bitbucket API: GET ${endpoint}`);
2453
+ const response = await fetch(`${this.BITBUCKET_API_BASE}${endpoint}`, {
2454
+ headers: {
2455
+ Authorization: `Basic ${auth}`,
2456
+ Accept: "application/json"
2457
+ }
2458
+ });
2459
+ if (!response.ok) {
2460
+ const statusText = response.statusText;
2461
+ const status = response.status;
2462
+ if (status === 401) {
2463
+ throw new BitbucketError("Invalid or expired API token", {
2464
+ hint: "Run: bragduck auth bitbucket",
2465
+ originalError: statusText
2466
+ });
2467
+ } else if (status === 403) {
2468
+ throw new BitbucketError("Forbidden - check token permissions", {
2469
+ hint: "Token needs: pullrequest:read, repository:read, account:read",
2470
+ originalError: statusText
2471
+ });
2472
+ } else if (status === 404) {
2473
+ throw new BitbucketError("Repository or resource not found", {
2474
+ originalError: statusText
2475
+ });
2476
+ } else if (status === 429) {
2477
+ throw new BitbucketError("Rate limit exceeded", {
2478
+ hint: "Wait a few minutes before trying again",
2479
+ originalError: statusText
2480
+ });
2481
+ }
2482
+ throw new BitbucketError(`API request failed: ${statusText}`, {
2483
+ originalError: statusText
2484
+ });
2485
+ }
2486
+ return response.json();
2487
+ }
2488
+ /**
2489
+ * Extract workspace and repo from git remote URL
2490
+ */
2491
+ parseRemoteUrl(url) {
2492
+ const match = url.match(/bitbucket\.org[:/]([^/]+)\/([^/.]+)/);
2493
+ if (match && match[1] && match[2]) {
2494
+ return {
2495
+ workspace: match[1],
2496
+ repo: match[2].replace(/\.git$/, "")
2497
+ };
2498
+ }
2499
+ return null;
2500
+ }
2501
+ /**
2502
+ * Get repository info from git remote
2503
+ */
2504
+ async getRepoFromGit() {
2505
+ try {
2506
+ const { stdout } = await execAsync4("command git remote get-url origin");
2507
+ const remoteUrl = stdout.trim();
2508
+ const parsed = this.parseRemoteUrl(remoteUrl);
2509
+ if (!parsed) {
2510
+ throw new BitbucketError("Could not parse Bitbucket repository from git remote", {
2511
+ hint: "Ensure this is a Bitbucket repository"
2512
+ });
2513
+ }
2514
+ return parsed;
2515
+ } catch (error) {
2516
+ if (error instanceof BitbucketError) {
2517
+ throw error;
2518
+ }
2519
+ throw new GitError("Could not get git remote URL");
2520
+ }
2521
+ }
2522
+ /**
2523
+ * Validate that this is a Bitbucket repository and credentials work
2524
+ */
2525
+ async validateBitbucketRepository() {
2526
+ await gitService.validateRepository();
2527
+ const { workspace, repo } = await this.getRepoFromGit();
2528
+ try {
2529
+ await this.request(`/repositories/${workspace}/${repo}`);
2530
+ } catch (error) {
2531
+ if (error instanceof BitbucketError) {
2532
+ throw error;
2533
+ }
2534
+ throw new BitbucketError("Could not access Bitbucket repository via API", {
2535
+ hint: "Check that this is a Bitbucket repository and your credentials are valid",
2536
+ originalError: error instanceof Error ? error.message : String(error)
2537
+ });
2538
+ }
2539
+ }
2540
+ /**
2541
+ * Get repository information
2542
+ */
2543
+ async getRepositoryInfo() {
2544
+ const { workspace, repo } = await this.getRepoFromGit();
2545
+ const bitbucketRepo = await this.request(
2546
+ `/repositories/${workspace}/${repo}`
2547
+ );
2548
+ return {
2549
+ owner: workspace,
2550
+ name: bitbucketRepo.name,
2551
+ fullName: bitbucketRepo.full_name,
2552
+ url: bitbucketRepo.links.html.href
2553
+ };
2554
+ }
2555
+ /**
2556
+ * Get current user's account ID
2557
+ */
2558
+ async getCurrentUserAccountId() {
2559
+ const user = await this.request("/user");
2560
+ return user.account_id;
2561
+ }
2562
+ /**
2563
+ * Get current user's username (nickname)
2564
+ */
2565
+ async getCurrentGitHubUser() {
2566
+ try {
2567
+ const user = await this.request("/user");
2568
+ return user.nickname;
2569
+ } catch {
2570
+ return null;
2571
+ }
2572
+ }
2573
+ /**
2574
+ * Fetch merged pull requests with optional filtering
2575
+ */
2576
+ async getMergedPRs(options = {}) {
2577
+ const { workspace, repo } = await this.getRepoFromGit();
2578
+ const queries = ['state="MERGED"'];
2579
+ if (options.days) {
2580
+ const since = /* @__PURE__ */ new Date();
2581
+ since.setDate(since.getDate() - options.days);
2582
+ queries.push(`updated_on>=${since.toISOString()}`);
2583
+ }
2584
+ if (options.author) {
2585
+ queries.push(`author.account_id="${options.author}"`);
2586
+ }
2587
+ const queryString = queries.join(" AND ");
2588
+ const allPRs = [];
2589
+ let endpoint = `/repositories/${workspace}/${repo}/pullrequests?q=${encodeURIComponent(queryString)}&pagelen=100`;
2590
+ while (endpoint) {
2591
+ const response = await this.request(endpoint);
2592
+ allPRs.push(...response.values);
2593
+ logger.debug(
2594
+ `Fetched ${response.values.length} PRs (total: ${allPRs.length})${response.next ? ", fetching next page..." : ""}`
2595
+ );
2596
+ if (options.limit && allPRs.length >= options.limit) {
2597
+ return allPRs.slice(0, options.limit);
2598
+ }
2599
+ if (response.next) {
2600
+ const url = new URL(response.next);
2601
+ endpoint = url.pathname + url.search;
2602
+ } else {
2603
+ endpoint = "";
2604
+ }
2605
+ }
2606
+ return allPRs;
2607
+ }
2608
+ /**
2609
+ * Fetch PRs for the current authenticated user
2610
+ */
2611
+ async getPRsByCurrentUser(options = {}) {
2612
+ const accountId = await this.getCurrentUserAccountId();
2613
+ return this.getMergedPRs({
2614
+ ...options,
2615
+ author: accountId
2616
+ });
2617
+ }
2618
+ /**
2619
+ * Transform Bitbucket PR to GitCommit format
2620
+ */
2621
+ transformPRToCommit(pr) {
2622
+ let message = pr.title;
2623
+ if (pr.description) {
2624
+ const truncatedDesc = pr.description.substring(0, this.MAX_DESCRIPTION_LENGTH);
2625
+ message = `${pr.title}
2626
+
2627
+ ${truncatedDesc}`;
2628
+ }
2629
+ return {
2630
+ sha: `pr-${pr.id}`,
2631
+ message,
2632
+ author: pr.author.nickname,
2633
+ authorEmail: "",
2634
+ // Not available in Bitbucket API
2635
+ date: pr.created_on,
2636
+ url: pr.links.html.href,
2637
+ diffStats: {
2638
+ filesChanged: 0,
2639
+ // Would require separate API call to diffstat endpoint
2640
+ insertions: 0,
2641
+ deletions: 0
2642
+ }
2643
+ };
2644
+ }
2645
+ };
2646
+ var bitbucketService = new BitbucketService();
2647
+
2648
+ // src/sync/bitbucket-adapter.ts
2649
+ var BitbucketSyncAdapter = class {
2650
+ name = "bitbucket";
2651
+ async validate() {
2652
+ await bitbucketService.validateBitbucketRepository();
2653
+ }
2654
+ async getRepositoryInfo() {
2655
+ const info = await bitbucketService.getRepositoryInfo();
2656
+ return {
2657
+ owner: info.owner,
2658
+ name: info.name,
2659
+ fullName: info.fullName,
2660
+ url: info.url
2661
+ };
2662
+ }
2663
+ async fetchWorkItems(options) {
2664
+ let prs;
2665
+ if (options.author) {
2666
+ prs = await bitbucketService.getMergedPRs({
2667
+ days: options.days,
2668
+ limit: options.limit,
2669
+ author: options.author
2670
+ });
2671
+ } else {
2672
+ prs = await bitbucketService.getPRsByCurrentUser({
2673
+ days: options.days,
2674
+ limit: options.limit
2675
+ });
2676
+ }
2677
+ return prs.map((pr) => bitbucketService.transformPRToCommit(pr));
2678
+ }
2679
+ async isAuthenticated() {
2680
+ try {
2681
+ await bitbucketService.validateBitbucketRepository();
2682
+ return true;
2683
+ } catch {
2684
+ return false;
2685
+ }
2686
+ }
2687
+ async getCurrentUser() {
2688
+ return bitbucketService.getCurrentGitHubUser();
2689
+ }
2690
+ };
2691
+ var bitbucketSyncAdapter = new BitbucketSyncAdapter();
2692
+
2321
2693
  // src/sync/adapter-factory.ts
2322
2694
  var AdapterFactory = class {
2323
2695
  /**
@@ -2327,9 +2699,11 @@ var AdapterFactory = class {
2327
2699
  switch (source) {
2328
2700
  case "github":
2329
2701
  return githubSyncAdapter;
2330
- case "gitlab":
2331
2702
  case "bitbucket":
2332
2703
  case "atlassian":
2704
+ return bitbucketSyncAdapter;
2705
+ // Bitbucket Cloud and Server use same adapter
2706
+ case "gitlab":
2333
2707
  throw new Error(`${source} adapter not yet implemented`);
2334
2708
  default:
2335
2709
  throw new Error(`Unknown source type: ${source}`);
@@ -2339,7 +2713,7 @@ var AdapterFactory = class {
2339
2713
  * Check if adapter is available for source
2340
2714
  */
2341
2715
  static isSupported(source) {
2342
- return source === "github";
2716
+ return source === "github" || source === "bitbucket" || source === "atlassian";
2343
2717
  }
2344
2718
  };
2345
2719
 
@@ -2479,7 +2853,7 @@ init_logger();
2479
2853
 
2480
2854
  // src/ui/prompts.ts
2481
2855
  init_esm_shims();
2482
- import { checkbox, confirm, input, select, editor } from "@inquirer/prompts";
2856
+ import { checkbox, confirm, input as input2, select, editor } from "@inquirer/prompts";
2483
2857
  import boxen4 from "boxen";
2484
2858
 
2485
2859
  // src/ui/formatters.ts
@@ -2641,7 +3015,7 @@ async function promptDaysToScan(defaultDays = 30) {
2641
3015
  default: "30"
2642
3016
  });
2643
3017
  if (selected === "custom") {
2644
- const customDays = await input({
3018
+ const customDays = await input2({
2645
3019
  message: "Enter number of days:",
2646
3020
  default: defaultDays.toString(),
2647
3021
  validate: (value) => {
@@ -2777,7 +3151,7 @@ ${theme.label("PR Link")} ${colors.link(prUrl)}`;
2777
3151
  let editedBrag = { ...currentBrag };
2778
3152
  if (action === "edit-title" || action === "edit-both") {
2779
3153
  console.log("");
2780
- const newTitle = await input({
3154
+ const newTitle = await input2({
2781
3155
  message: "Enter new title:",
2782
3156
  default: currentBrag.refined_title
2783
3157
  });
@@ -3784,7 +4158,7 @@ var packageJsonPath = join7(__dirname6, "../../package.json");
3784
4158
  var packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
3785
4159
  var program = new Command();
3786
4160
  program.name("bragduck").description("CLI tool for managing developer achievements and brags\nAliases: bd, duck, brag").version(packageJson.version, "-v, --version", "Display version number").helpOption("-h, --help", "Display help information").option("--skip-version-check", "Skip automatic version check on startup").option("--debug", "Enable debug mode (shows detailed logs)");
3787
- program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status)").action(async (subcommand) => {
4161
+ program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status, bitbucket)").action(async (subcommand) => {
3788
4162
  try {
3789
4163
  await authCommand(subcommand);
3790
4164
  } catch (error) {