@bragduck/cli 2.5.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, BitbucketError;
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";
@@ -465,6 +465,12 @@ var init_errors = __esm({
465
465
  this.name = "BitbucketError";
466
466
  }
467
467
  };
468
+ GitLabError = class extends BragduckError {
469
+ constructor(message, details) {
470
+ super(message, "GITLAB_ERROR", details);
471
+ this.name = "GitLabError";
472
+ }
473
+ };
468
474
  }
469
475
  });
470
476
 
@@ -1613,9 +1619,11 @@ async function authCommand(subcommand) {
1613
1619
  await authStatus();
1614
1620
  } else if (subcommand === "bitbucket") {
1615
1621
  await authBitbucket();
1622
+ } else if (subcommand === "gitlab") {
1623
+ await authGitLab();
1616
1624
  } else {
1617
1625
  logger.error(`Unknown auth subcommand: ${subcommand}`);
1618
- logger.info("Available subcommands: login, status, bitbucket");
1626
+ logger.info("Available subcommands: login, status, bitbucket, gitlab");
1619
1627
  process.exit(1);
1620
1628
  }
1621
1629
  }
@@ -1787,6 +1795,69 @@ User: ${user.display_name}
1787
1795
  process.exit(1);
1788
1796
  }
1789
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
+ }
1790
1861
 
1791
1862
  // src/commands/sync.ts
1792
1863
  init_esm_shims();
@@ -1911,6 +1982,8 @@ var SourceDetector = class {
1911
1982
  return true;
1912
1983
  } else if (type === "bitbucket" || type === "atlassian") {
1913
1984
  return await storageService.isServiceAuthenticated("bitbucket");
1985
+ } else if (type === "gitlab") {
1986
+ return await storageService.isServiceAuthenticated("gitlab");
1914
1987
  }
1915
1988
  return false;
1916
1989
  } catch {
@@ -2690,6 +2763,299 @@ var BitbucketSyncAdapter = class {
2690
2763
  };
2691
2764
  var bitbucketSyncAdapter = new BitbucketSyncAdapter();
2692
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
+
2693
3059
  // src/sync/adapter-factory.ts
2694
3060
  var AdapterFactory = class {
2695
3061
  /**
@@ -2704,7 +3070,7 @@ var AdapterFactory = class {
2704
3070
  return bitbucketSyncAdapter;
2705
3071
  // Bitbucket Cloud and Server use same adapter
2706
3072
  case "gitlab":
2707
- throw new Error(`${source} adapter not yet implemented`);
3073
+ return gitlabSyncAdapter;
2708
3074
  default:
2709
3075
  throw new Error(`Unknown source type: ${source}`);
2710
3076
  }
@@ -2713,7 +3079,7 @@ var AdapterFactory = class {
2713
3079
  * Check if adapter is available for source
2714
3080
  */
2715
3081
  static isSupported(source) {
2716
- return source === "github" || source === "bitbucket" || source === "atlassian";
3082
+ return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab";
2717
3083
  }
2718
3084
  };
2719
3085
 
@@ -4158,7 +4524,7 @@ var packageJsonPath = join7(__dirname6, "../../package.json");
4158
4524
  var packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
4159
4525
  var program = new Command();
4160
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)");
4161
- program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status, bitbucket)").action(async (subcommand) => {
4527
+ program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status, bitbucket, gitlab)").action(async (subcommand) => {
4162
4528
  try {
4163
4529
  await authCommand(subcommand);
4164
4530
  } catch (error) {