@bragduck/cli 2.4.0 → 2.6.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, GitLabError;
396
396
  var init_errors = __esm({
397
397
  "src/utils/errors.ts"() {
398
398
  "use strict";
@@ -459,6 +459,18 @@ 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
+ };
468
+ GitLabError = class extends BragduckError {
469
+ constructor(message, details) {
470
+ super(message, "GITLAB_ERROR", details);
471
+ this.name = "GitLabError";
472
+ }
473
+ };
462
474
  }
463
475
  });
464
476
 
@@ -1437,6 +1449,7 @@ init_storage_service();
1437
1449
  init_logger();
1438
1450
  import boxen from "boxen";
1439
1451
  import chalk3 from "chalk";
1452
+ import { input } from "@inquirer/prompts";
1440
1453
 
1441
1454
  // src/ui/theme.ts
1442
1455
  init_esm_shims();
@@ -1604,9 +1617,13 @@ async function authCommand(subcommand) {
1604
1617
  await authLogin();
1605
1618
  } else if (subcommand === "status") {
1606
1619
  await authStatus();
1620
+ } else if (subcommand === "bitbucket") {
1621
+ await authBitbucket();
1622
+ } else if (subcommand === "gitlab") {
1623
+ await authGitLab();
1607
1624
  } else {
1608
1625
  logger.error(`Unknown auth subcommand: ${subcommand}`);
1609
- logger.info("Available subcommands: login, status");
1626
+ logger.info("Available subcommands: login, status, bitbucket, gitlab");
1610
1627
  process.exit(1);
1611
1628
  }
1612
1629
  }
@@ -1700,6 +1717,147 @@ async function authStatus() {
1700
1717
  }
1701
1718
  logger.log("");
1702
1719
  }
1720
+ async function authBitbucket() {
1721
+ logger.log("");
1722
+ logger.log(
1723
+ boxen(
1724
+ 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)"),
1725
+ boxStyles.info
1726
+ )
1727
+ );
1728
+ logger.log("");
1729
+ try {
1730
+ const email = await input({
1731
+ message: "Atlassian account email:",
1732
+ validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
1733
+ });
1734
+ const apiToken = await input({
1735
+ message: "API Token:",
1736
+ validate: (value) => value.length > 0 ? true : "API token cannot be empty"
1737
+ });
1738
+ const tokenExpiry = await input({
1739
+ message: "Token expiry date (YYYY-MM-DD, optional):",
1740
+ default: "",
1741
+ validate: (value) => {
1742
+ if (!value) return true;
1743
+ const date = new Date(value);
1744
+ return !isNaN(date.getTime()) ? true : "Please enter a valid date (YYYY-MM-DD)";
1745
+ }
1746
+ });
1747
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
1748
+ const response = await fetch("https://api.bitbucket.org/2.0/user", {
1749
+ headers: { Authorization: `Basic ${auth}` }
1750
+ });
1751
+ if (!response.ok) {
1752
+ logger.log("");
1753
+ logger.log(
1754
+ boxen(
1755
+ theme.error("\u2717 Authentication Failed") + "\n\nInvalid email or API token",
1756
+ boxStyles.error
1757
+ )
1758
+ );
1759
+ logger.log("");
1760
+ process.exit(1);
1761
+ }
1762
+ const user = await response.json();
1763
+ const credentials = {
1764
+ accessToken: apiToken,
1765
+ username: email
1766
+ // Store email in username field
1767
+ };
1768
+ if (tokenExpiry) {
1769
+ const expiryDate = new Date(tokenExpiry);
1770
+ credentials.expiresAt = expiryDate.getTime();
1771
+ }
1772
+ await storageService.setServiceCredentials("bitbucket", credentials);
1773
+ logger.log("");
1774
+ logger.log(
1775
+ boxen(
1776
+ theme.success("\u2713 Successfully authenticated with Bitbucket") + `
1777
+
1778
+ Email: ${email}
1779
+ User: ${user.display_name}
1780
+ ` + (tokenExpiry ? `Expires: ${tokenExpiry}` : ""),
1781
+ boxStyles.success
1782
+ )
1783
+ );
1784
+ logger.log("");
1785
+ } catch (error) {
1786
+ const err = error;
1787
+ logger.log("");
1788
+ logger.log(
1789
+ boxen(
1790
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
1791
+ boxStyles.error
1792
+ )
1793
+ );
1794
+ logger.log("");
1795
+ process.exit(1);
1796
+ }
1797
+ }
1798
+ async function authGitLab() {
1799
+ logger.log("");
1800
+ logger.log(
1801
+ boxen(
1802
+ theme.info("GitLab Personal Access Token Authentication") + "\n\nCreate a Personal Access Token at:\n" + colors.highlight("https://gitlab.com/-/profile/personal_access_tokens") + "\n\nRequired scopes:\n \u2022 api (full API access)\n \u2022 read_api (read API)\n \u2022 read_user (read user info)\n\n" + theme.warning("For self-hosted GitLab, check your instance docs"),
1803
+ boxStyles.info
1804
+ )
1805
+ );
1806
+ logger.log("");
1807
+ try {
1808
+ const instanceUrl = await input({
1809
+ message: "GitLab instance URL (press Enter for gitlab.com):",
1810
+ default: "https://gitlab.com"
1811
+ });
1812
+ const accessToken = await input({
1813
+ message: "Personal Access Token:",
1814
+ validate: (value) => value.length > 0 ? true : "Token cannot be empty"
1815
+ });
1816
+ const testUrl = `${instanceUrl.replace(/\/$/, "")}/api/v4/user`;
1817
+ const response = await fetch(testUrl, {
1818
+ headers: { "PRIVATE-TOKEN": accessToken }
1819
+ });
1820
+ if (!response.ok) {
1821
+ logger.log("");
1822
+ logger.log(
1823
+ boxen(
1824
+ theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL or access token",
1825
+ boxStyles.error
1826
+ )
1827
+ );
1828
+ logger.log("");
1829
+ process.exit(1);
1830
+ }
1831
+ const user = await response.json();
1832
+ const credentials = {
1833
+ accessToken,
1834
+ instanceUrl: instanceUrl === "https://gitlab.com" ? void 0 : instanceUrl
1835
+ };
1836
+ await storageService.setServiceCredentials("gitlab", credentials);
1837
+ logger.log("");
1838
+ logger.log(
1839
+ boxen(
1840
+ theme.success("\u2713 Successfully authenticated with GitLab") + `
1841
+
1842
+ Instance: ${instanceUrl}
1843
+ User: ${user.name} (@${user.username})`,
1844
+ boxStyles.success
1845
+ )
1846
+ );
1847
+ logger.log("");
1848
+ } catch (error) {
1849
+ const err = error;
1850
+ logger.log("");
1851
+ logger.log(
1852
+ boxen(
1853
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
1854
+ boxStyles.error
1855
+ )
1856
+ );
1857
+ logger.log("");
1858
+ process.exit(1);
1859
+ }
1860
+ }
1703
1861
 
1704
1862
  // src/commands/sync.ts
1705
1863
  init_esm_shims();
@@ -1720,6 +1878,7 @@ import boxen6 from "boxen";
1720
1878
  // src/utils/source-detector.ts
1721
1879
  init_esm_shims();
1722
1880
  init_errors();
1881
+ init_storage_service();
1723
1882
  import { exec as exec2 } from "child_process";
1724
1883
  import { promisify as promisify2 } from "util";
1725
1884
  var execAsync2 = promisify2(exec2);
@@ -1821,6 +1980,10 @@ var SourceDetector = class {
1821
1980
  if (type === "github") {
1822
1981
  await execAsync2("command gh auth status");
1823
1982
  return true;
1983
+ } else if (type === "bitbucket" || type === "atlassian") {
1984
+ return await storageService.isServiceAuthenticated("bitbucket");
1985
+ } else if (type === "gitlab") {
1986
+ return await storageService.isServiceAuthenticated("gitlab");
1824
1987
  }
1825
1988
  return false;
1826
1989
  } catch {
@@ -2318,6 +2481,581 @@ var GitHubSyncAdapter = class {
2318
2481
  };
2319
2482
  var githubSyncAdapter = new GitHubSyncAdapter();
2320
2483
 
2484
+ // src/sync/bitbucket-adapter.ts
2485
+ init_esm_shims();
2486
+
2487
+ // src/services/bitbucket.service.ts
2488
+ init_esm_shims();
2489
+ init_errors();
2490
+ init_logger();
2491
+ init_storage_service();
2492
+ import { exec as exec4 } from "child_process";
2493
+ import { promisify as promisify4 } from "util";
2494
+ var execAsync4 = promisify4(exec4);
2495
+ var BitbucketService = class {
2496
+ BITBUCKET_API_BASE = "https://api.bitbucket.org/2.0";
2497
+ MAX_DESCRIPTION_LENGTH = 5e3;
2498
+ /**
2499
+ * Get stored Bitbucket credentials
2500
+ */
2501
+ async getCredentials() {
2502
+ const creds = await storageService.getServiceCredentials("bitbucket");
2503
+ if (!creds || !creds.username || !creds.accessToken) {
2504
+ throw new BitbucketError("Not authenticated with Bitbucket", {
2505
+ hint: "Run: bragduck auth bitbucket"
2506
+ });
2507
+ }
2508
+ if (creds.expiresAt && creds.expiresAt < Date.now()) {
2509
+ throw new BitbucketError("API token has expired", {
2510
+ hint: "Run: bragduck auth bitbucket"
2511
+ });
2512
+ }
2513
+ return {
2514
+ email: creds.username,
2515
+ // username field stores email
2516
+ apiToken: creds.accessToken
2517
+ };
2518
+ }
2519
+ /**
2520
+ * Make authenticated request to Bitbucket API
2521
+ */
2522
+ async request(endpoint) {
2523
+ const { email, apiToken } = await this.getCredentials();
2524
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
2525
+ logger.debug(`Bitbucket API: GET ${endpoint}`);
2526
+ const response = await fetch(`${this.BITBUCKET_API_BASE}${endpoint}`, {
2527
+ headers: {
2528
+ Authorization: `Basic ${auth}`,
2529
+ Accept: "application/json"
2530
+ }
2531
+ });
2532
+ if (!response.ok) {
2533
+ const statusText = response.statusText;
2534
+ const status = response.status;
2535
+ if (status === 401) {
2536
+ throw new BitbucketError("Invalid or expired API token", {
2537
+ hint: "Run: bragduck auth bitbucket",
2538
+ originalError: statusText
2539
+ });
2540
+ } else if (status === 403) {
2541
+ throw new BitbucketError("Forbidden - check token permissions", {
2542
+ hint: "Token needs: pullrequest:read, repository:read, account:read",
2543
+ originalError: statusText
2544
+ });
2545
+ } else if (status === 404) {
2546
+ throw new BitbucketError("Repository or resource not found", {
2547
+ originalError: statusText
2548
+ });
2549
+ } else if (status === 429) {
2550
+ throw new BitbucketError("Rate limit exceeded", {
2551
+ hint: "Wait a few minutes before trying again",
2552
+ originalError: statusText
2553
+ });
2554
+ }
2555
+ throw new BitbucketError(`API request failed: ${statusText}`, {
2556
+ originalError: statusText
2557
+ });
2558
+ }
2559
+ return response.json();
2560
+ }
2561
+ /**
2562
+ * Extract workspace and repo from git remote URL
2563
+ */
2564
+ parseRemoteUrl(url) {
2565
+ const match = url.match(/bitbucket\.org[:/]([^/]+)\/([^/.]+)/);
2566
+ if (match && match[1] && match[2]) {
2567
+ return {
2568
+ workspace: match[1],
2569
+ repo: match[2].replace(/\.git$/, "")
2570
+ };
2571
+ }
2572
+ return null;
2573
+ }
2574
+ /**
2575
+ * Get repository info from git remote
2576
+ */
2577
+ async getRepoFromGit() {
2578
+ try {
2579
+ const { stdout } = await execAsync4("command git remote get-url origin");
2580
+ const remoteUrl = stdout.trim();
2581
+ const parsed = this.parseRemoteUrl(remoteUrl);
2582
+ if (!parsed) {
2583
+ throw new BitbucketError("Could not parse Bitbucket repository from git remote", {
2584
+ hint: "Ensure this is a Bitbucket repository"
2585
+ });
2586
+ }
2587
+ return parsed;
2588
+ } catch (error) {
2589
+ if (error instanceof BitbucketError) {
2590
+ throw error;
2591
+ }
2592
+ throw new GitError("Could not get git remote URL");
2593
+ }
2594
+ }
2595
+ /**
2596
+ * Validate that this is a Bitbucket repository and credentials work
2597
+ */
2598
+ async validateBitbucketRepository() {
2599
+ await gitService.validateRepository();
2600
+ const { workspace, repo } = await this.getRepoFromGit();
2601
+ try {
2602
+ await this.request(`/repositories/${workspace}/${repo}`);
2603
+ } catch (error) {
2604
+ if (error instanceof BitbucketError) {
2605
+ throw error;
2606
+ }
2607
+ throw new BitbucketError("Could not access Bitbucket repository via API", {
2608
+ hint: "Check that this is a Bitbucket repository and your credentials are valid",
2609
+ originalError: error instanceof Error ? error.message : String(error)
2610
+ });
2611
+ }
2612
+ }
2613
+ /**
2614
+ * Get repository information
2615
+ */
2616
+ async getRepositoryInfo() {
2617
+ const { workspace, repo } = await this.getRepoFromGit();
2618
+ const bitbucketRepo = await this.request(
2619
+ `/repositories/${workspace}/${repo}`
2620
+ );
2621
+ return {
2622
+ owner: workspace,
2623
+ name: bitbucketRepo.name,
2624
+ fullName: bitbucketRepo.full_name,
2625
+ url: bitbucketRepo.links.html.href
2626
+ };
2627
+ }
2628
+ /**
2629
+ * Get current user's account ID
2630
+ */
2631
+ async getCurrentUserAccountId() {
2632
+ const user = await this.request("/user");
2633
+ return user.account_id;
2634
+ }
2635
+ /**
2636
+ * Get current user's username (nickname)
2637
+ */
2638
+ async getCurrentGitHubUser() {
2639
+ try {
2640
+ const user = await this.request("/user");
2641
+ return user.nickname;
2642
+ } catch {
2643
+ return null;
2644
+ }
2645
+ }
2646
+ /**
2647
+ * Fetch merged pull requests with optional filtering
2648
+ */
2649
+ async getMergedPRs(options = {}) {
2650
+ const { workspace, repo } = await this.getRepoFromGit();
2651
+ const queries = ['state="MERGED"'];
2652
+ if (options.days) {
2653
+ const since = /* @__PURE__ */ new Date();
2654
+ since.setDate(since.getDate() - options.days);
2655
+ queries.push(`updated_on>=${since.toISOString()}`);
2656
+ }
2657
+ if (options.author) {
2658
+ queries.push(`author.account_id="${options.author}"`);
2659
+ }
2660
+ const queryString = queries.join(" AND ");
2661
+ const allPRs = [];
2662
+ let endpoint = `/repositories/${workspace}/${repo}/pullrequests?q=${encodeURIComponent(queryString)}&pagelen=100`;
2663
+ while (endpoint) {
2664
+ const response = await this.request(endpoint);
2665
+ allPRs.push(...response.values);
2666
+ logger.debug(
2667
+ `Fetched ${response.values.length} PRs (total: ${allPRs.length})${response.next ? ", fetching next page..." : ""}`
2668
+ );
2669
+ if (options.limit && allPRs.length >= options.limit) {
2670
+ return allPRs.slice(0, options.limit);
2671
+ }
2672
+ if (response.next) {
2673
+ const url = new URL(response.next);
2674
+ endpoint = url.pathname + url.search;
2675
+ } else {
2676
+ endpoint = "";
2677
+ }
2678
+ }
2679
+ return allPRs;
2680
+ }
2681
+ /**
2682
+ * Fetch PRs for the current authenticated user
2683
+ */
2684
+ async getPRsByCurrentUser(options = {}) {
2685
+ const accountId = await this.getCurrentUserAccountId();
2686
+ return this.getMergedPRs({
2687
+ ...options,
2688
+ author: accountId
2689
+ });
2690
+ }
2691
+ /**
2692
+ * Transform Bitbucket PR to GitCommit format
2693
+ */
2694
+ transformPRToCommit(pr) {
2695
+ let message = pr.title;
2696
+ if (pr.description) {
2697
+ const truncatedDesc = pr.description.substring(0, this.MAX_DESCRIPTION_LENGTH);
2698
+ message = `${pr.title}
2699
+
2700
+ ${truncatedDesc}`;
2701
+ }
2702
+ return {
2703
+ sha: `pr-${pr.id}`,
2704
+ message,
2705
+ author: pr.author.nickname,
2706
+ authorEmail: "",
2707
+ // Not available in Bitbucket API
2708
+ date: pr.created_on,
2709
+ url: pr.links.html.href,
2710
+ diffStats: {
2711
+ filesChanged: 0,
2712
+ // Would require separate API call to diffstat endpoint
2713
+ insertions: 0,
2714
+ deletions: 0
2715
+ }
2716
+ };
2717
+ }
2718
+ };
2719
+ var bitbucketService = new BitbucketService();
2720
+
2721
+ // src/sync/bitbucket-adapter.ts
2722
+ var BitbucketSyncAdapter = class {
2723
+ name = "bitbucket";
2724
+ async validate() {
2725
+ await bitbucketService.validateBitbucketRepository();
2726
+ }
2727
+ async getRepositoryInfo() {
2728
+ const info = await bitbucketService.getRepositoryInfo();
2729
+ return {
2730
+ owner: info.owner,
2731
+ name: info.name,
2732
+ fullName: info.fullName,
2733
+ url: info.url
2734
+ };
2735
+ }
2736
+ async fetchWorkItems(options) {
2737
+ let prs;
2738
+ if (options.author) {
2739
+ prs = await bitbucketService.getMergedPRs({
2740
+ days: options.days,
2741
+ limit: options.limit,
2742
+ author: options.author
2743
+ });
2744
+ } else {
2745
+ prs = await bitbucketService.getPRsByCurrentUser({
2746
+ days: options.days,
2747
+ limit: options.limit
2748
+ });
2749
+ }
2750
+ return prs.map((pr) => bitbucketService.transformPRToCommit(pr));
2751
+ }
2752
+ async isAuthenticated() {
2753
+ try {
2754
+ await bitbucketService.validateBitbucketRepository();
2755
+ return true;
2756
+ } catch {
2757
+ return false;
2758
+ }
2759
+ }
2760
+ async getCurrentUser() {
2761
+ return bitbucketService.getCurrentGitHubUser();
2762
+ }
2763
+ };
2764
+ var bitbucketSyncAdapter = new BitbucketSyncAdapter();
2765
+
2766
+ // src/sync/gitlab-adapter.ts
2767
+ init_esm_shims();
2768
+
2769
+ // src/services/gitlab.service.ts
2770
+ init_esm_shims();
2771
+ init_errors();
2772
+ init_logger();
2773
+ init_storage_service();
2774
+ import { exec as exec5 } from "child_process";
2775
+ import { promisify as promisify5 } from "util";
2776
+ import { URLSearchParams as URLSearchParams3 } from "url";
2777
+ var execAsync5 = promisify5(exec5);
2778
+ var GitLabService = class {
2779
+ DEFAULT_INSTANCE = "https://gitlab.com";
2780
+ MAX_DESCRIPTION_LENGTH = 5e3;
2781
+ /**
2782
+ * Get stored GitLab credentials
2783
+ */
2784
+ async getCredentials() {
2785
+ const creds = await storageService.getServiceCredentials("gitlab");
2786
+ if (!creds || !creds.accessToken) {
2787
+ throw new GitLabError("Not authenticated with GitLab", {
2788
+ hint: "Run: bragduck auth gitlab"
2789
+ });
2790
+ }
2791
+ if (creds.expiresAt && creds.expiresAt < Date.now()) {
2792
+ throw new GitLabError("Access token has expired", {
2793
+ hint: "Run: bragduck auth gitlab"
2794
+ });
2795
+ }
2796
+ return {
2797
+ accessToken: creds.accessToken,
2798
+ instanceUrl: creds.instanceUrl || this.DEFAULT_INSTANCE
2799
+ };
2800
+ }
2801
+ /**
2802
+ * Make authenticated request to GitLab API
2803
+ */
2804
+ async request(endpoint) {
2805
+ const { accessToken, instanceUrl } = await this.getCredentials();
2806
+ const baseUrl = `${instanceUrl}/api/v4`;
2807
+ const url = `${baseUrl}${endpoint}`;
2808
+ logger.debug(`GitLab API: GET ${endpoint}`);
2809
+ const response = await fetch(url, {
2810
+ headers: {
2811
+ "PRIVATE-TOKEN": accessToken,
2812
+ Accept: "application/json"
2813
+ }
2814
+ });
2815
+ if (!response.ok) {
2816
+ const statusText = response.statusText;
2817
+ const status = response.status;
2818
+ if (status === 401) {
2819
+ throw new GitLabError("Invalid or expired access token", {
2820
+ hint: "Run: bragduck auth gitlab",
2821
+ originalError: statusText
2822
+ });
2823
+ } else if (status === 403) {
2824
+ throw new GitLabError("Insufficient token permissions", {
2825
+ hint: "Token requires scopes: api, read_api, read_user",
2826
+ originalError: statusText
2827
+ });
2828
+ } else if (status === 404) {
2829
+ throw new GitLabError("Resource not found", {
2830
+ hint: "Check repository URL and token permissions",
2831
+ originalError: statusText
2832
+ });
2833
+ } else if (status === 429) {
2834
+ throw new GitLabError("Rate limit exceeded", {
2835
+ hint: "Wait a moment and try again",
2836
+ originalError: statusText
2837
+ });
2838
+ }
2839
+ throw new GitLabError(`API request failed: ${statusText}`, {
2840
+ originalError: statusText
2841
+ });
2842
+ }
2843
+ return response.json();
2844
+ }
2845
+ /**
2846
+ * Extract namespace and project from git remote URL
2847
+ */
2848
+ parseRemoteUrl(url) {
2849
+ const match = url.match(/[:/]([\w-]+)\/([\w-]+?)(?:\.git)?$/);
2850
+ if (match && match[1] && match[2]) {
2851
+ return {
2852
+ namespace: match[1],
2853
+ project: match[2]
2854
+ };
2855
+ }
2856
+ return null;
2857
+ }
2858
+ /**
2859
+ * Get project path from git remote
2860
+ */
2861
+ async getProjectFromGit() {
2862
+ try {
2863
+ const { stdout } = await execAsync5("git remote get-url origin");
2864
+ const remoteUrl = stdout.trim();
2865
+ const parsed = this.parseRemoteUrl(remoteUrl);
2866
+ if (!parsed) {
2867
+ throw new GitLabError("Could not parse GitLab project from remote URL", {
2868
+ url: remoteUrl
2869
+ });
2870
+ }
2871
+ return {
2872
+ ...parsed,
2873
+ projectPath: `${parsed.namespace}/${parsed.project}`
2874
+ };
2875
+ } catch (error) {
2876
+ if (error instanceof GitLabError) {
2877
+ throw error;
2878
+ }
2879
+ throw new GitError("Could not get git remote URL");
2880
+ }
2881
+ }
2882
+ /**
2883
+ * Validate that this is a GitLab repository and credentials work
2884
+ */
2885
+ async validateGitLabRepository() {
2886
+ await gitService.validateRepository();
2887
+ const { projectPath } = await this.getProjectFromGit();
2888
+ const encodedPath = encodeURIComponent(projectPath);
2889
+ try {
2890
+ await this.request(`/projects/${encodedPath}`);
2891
+ } catch (error) {
2892
+ if (error instanceof GitLabError) {
2893
+ throw error;
2894
+ }
2895
+ throw new GitLabError("Could not access GitLab project via API", {
2896
+ hint: "Check that this is a GitLab repository and your token is valid",
2897
+ originalError: error instanceof Error ? error.message : String(error)
2898
+ });
2899
+ }
2900
+ }
2901
+ /**
2902
+ * Get repository information
2903
+ */
2904
+ async getRepositoryInfo() {
2905
+ const { projectPath } = await this.getProjectFromGit();
2906
+ const encodedPath = encodeURIComponent(projectPath);
2907
+ const project = await this.request(`/projects/${encodedPath}`);
2908
+ const [owner, name] = projectPath.split("/");
2909
+ return {
2910
+ owner: owner || "",
2911
+ name: name || "",
2912
+ fullName: project.path_with_namespace,
2913
+ url: project.web_url
2914
+ };
2915
+ }
2916
+ /**
2917
+ * Fetch merged MRs with pagination
2918
+ */
2919
+ async getMergedMRs(options = {}) {
2920
+ const { projectPath } = await this.getProjectFromGit();
2921
+ const encodedPath = encodeURIComponent(projectPath);
2922
+ const params = new URLSearchParams3({
2923
+ state: "merged",
2924
+ order_by: "updated_at",
2925
+ sort: "desc",
2926
+ per_page: "100"
2927
+ });
2928
+ if (options.days) {
2929
+ const since = /* @__PURE__ */ new Date();
2930
+ since.setDate(since.getDate() - options.days);
2931
+ params.append("updated_after", since.toISOString());
2932
+ }
2933
+ if (options.author) {
2934
+ params.append("author_username", options.author);
2935
+ }
2936
+ const allMRs = [];
2937
+ let page = 1;
2938
+ while (true) {
2939
+ params.set("page", page.toString());
2940
+ const endpoint = `/projects/${encodedPath}/merge_requests?${params}`;
2941
+ const mrs = await this.request(endpoint);
2942
+ allMRs.push(...mrs);
2943
+ logger.debug(
2944
+ `Fetched ${mrs.length} MRs (total: ${allMRs.length})${mrs.length === 100 ? ", fetching next page..." : ""}`
2945
+ );
2946
+ if (options.limit && allMRs.length >= options.limit) {
2947
+ return allMRs.slice(0, options.limit);
2948
+ }
2949
+ if (mrs.length < 100) {
2950
+ break;
2951
+ }
2952
+ page++;
2953
+ }
2954
+ return allMRs;
2955
+ }
2956
+ /**
2957
+ * Get MRs by current user
2958
+ */
2959
+ async getMRsByCurrentUser(options = {}) {
2960
+ const username = await this.getCurrentGitLabUser();
2961
+ if (!username) {
2962
+ throw new GitLabError("Could not get current user username");
2963
+ }
2964
+ return this.getMergedMRs({
2965
+ ...options,
2966
+ author: username
2967
+ });
2968
+ }
2969
+ /**
2970
+ * Get current authenticated user
2971
+ */
2972
+ async getCurrentGitLabUser() {
2973
+ try {
2974
+ const user = await this.request("/user");
2975
+ return user.username;
2976
+ } catch {
2977
+ return null;
2978
+ }
2979
+ }
2980
+ /**
2981
+ * Transform GitLab MR to GitCommit format
2982
+ */
2983
+ transformMRToCommit(mr) {
2984
+ const message = this.formatMessage(mr.title, mr.description);
2985
+ return {
2986
+ sha: `mr-${mr.iid}`,
2987
+ // Use iid (project-scoped)
2988
+ message,
2989
+ author: mr.author.username,
2990
+ authorEmail: "",
2991
+ // Not available in MR response
2992
+ date: mr.created_at,
2993
+ url: mr.web_url,
2994
+ diffStats: {
2995
+ filesChanged: mr.diff_stats?.file_count || 0,
2996
+ insertions: mr.diff_stats?.additions || 0,
2997
+ deletions: mr.diff_stats?.deletions || 0
2998
+ }
2999
+ };
3000
+ }
3001
+ /**
3002
+ * Format MR message with title and description
3003
+ */
3004
+ formatMessage(title, description) {
3005
+ if (!description) return title;
3006
+ const truncated = description.substring(0, this.MAX_DESCRIPTION_LENGTH);
3007
+ return `${title}
3008
+
3009
+ ${truncated}`;
3010
+ }
3011
+ };
3012
+ var gitlabService = new GitLabService();
3013
+
3014
+ // src/sync/gitlab-adapter.ts
3015
+ var GitLabSyncAdapter = class {
3016
+ name = "gitlab";
3017
+ async validate() {
3018
+ await gitlabService.validateGitLabRepository();
3019
+ }
3020
+ async getRepositoryInfo() {
3021
+ const info = await gitlabService.getRepositoryInfo();
3022
+ return {
3023
+ owner: info.owner,
3024
+ name: info.name,
3025
+ fullName: info.fullName,
3026
+ url: info.url
3027
+ };
3028
+ }
3029
+ async fetchWorkItems(options) {
3030
+ let mrs;
3031
+ if (options.author) {
3032
+ mrs = await gitlabService.getMergedMRs({
3033
+ days: options.days,
3034
+ limit: options.limit,
3035
+ author: options.author
3036
+ });
3037
+ } else {
3038
+ mrs = await gitlabService.getMRsByCurrentUser({
3039
+ days: options.days,
3040
+ limit: options.limit
3041
+ });
3042
+ }
3043
+ return mrs.map((mr) => gitlabService.transformMRToCommit(mr));
3044
+ }
3045
+ async isAuthenticated() {
3046
+ try {
3047
+ await gitlabService.validateGitLabRepository();
3048
+ return true;
3049
+ } catch {
3050
+ return false;
3051
+ }
3052
+ }
3053
+ async getCurrentUser() {
3054
+ return gitlabService.getCurrentGitLabUser();
3055
+ }
3056
+ };
3057
+ var gitlabSyncAdapter = new GitLabSyncAdapter();
3058
+
2321
3059
  // src/sync/adapter-factory.ts
2322
3060
  var AdapterFactory = class {
2323
3061
  /**
@@ -2327,10 +3065,12 @@ var AdapterFactory = class {
2327
3065
  switch (source) {
2328
3066
  case "github":
2329
3067
  return githubSyncAdapter;
2330
- case "gitlab":
2331
3068
  case "bitbucket":
2332
3069
  case "atlassian":
2333
- throw new Error(`${source} adapter not yet implemented`);
3070
+ return bitbucketSyncAdapter;
3071
+ // Bitbucket Cloud and Server use same adapter
3072
+ case "gitlab":
3073
+ return gitlabSyncAdapter;
2334
3074
  default:
2335
3075
  throw new Error(`Unknown source type: ${source}`);
2336
3076
  }
@@ -2339,7 +3079,7 @@ var AdapterFactory = class {
2339
3079
  * Check if adapter is available for source
2340
3080
  */
2341
3081
  static isSupported(source) {
2342
- return source === "github";
3082
+ return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab";
2343
3083
  }
2344
3084
  };
2345
3085
 
@@ -2479,7 +3219,7 @@ init_logger();
2479
3219
 
2480
3220
  // src/ui/prompts.ts
2481
3221
  init_esm_shims();
2482
- import { checkbox, confirm, input, select, editor } from "@inquirer/prompts";
3222
+ import { checkbox, confirm, input as input2, select, editor } from "@inquirer/prompts";
2483
3223
  import boxen4 from "boxen";
2484
3224
 
2485
3225
  // src/ui/formatters.ts
@@ -2641,7 +3381,7 @@ async function promptDaysToScan(defaultDays = 30) {
2641
3381
  default: "30"
2642
3382
  });
2643
3383
  if (selected === "custom") {
2644
- const customDays = await input({
3384
+ const customDays = await input2({
2645
3385
  message: "Enter number of days:",
2646
3386
  default: defaultDays.toString(),
2647
3387
  validate: (value) => {
@@ -2777,7 +3517,7 @@ ${theme.label("PR Link")} ${colors.link(prUrl)}`;
2777
3517
  let editedBrag = { ...currentBrag };
2778
3518
  if (action === "edit-title" || action === "edit-both") {
2779
3519
  console.log("");
2780
- const newTitle = await input({
3520
+ const newTitle = await input2({
2781
3521
  message: "Enter new title:",
2782
3522
  default: currentBrag.refined_title
2783
3523
  });
@@ -3784,7 +4524,7 @@ var packageJsonPath = join7(__dirname6, "../../package.json");
3784
4524
  var packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
3785
4525
  var program = new Command();
3786
4526
  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) => {
4527
+ program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status, bitbucket, gitlab)").action(async (subcommand) => {
3788
4528
  try {
3789
4529
  await authCommand(subcommand);
3790
4530
  } catch (error) {