@bragduck/cli 2.5.0 → 2.7.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, JiraError, ConfluenceError;
396
396
  var init_errors = __esm({
397
397
  "src/utils/errors.ts"() {
398
398
  "use strict";
@@ -465,6 +465,24 @@ 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
+ };
474
+ JiraError = class extends BragduckError {
475
+ constructor(message, details) {
476
+ super(message, "JIRA_ERROR", details);
477
+ this.name = "JiraError";
478
+ }
479
+ };
480
+ ConfluenceError = class extends BragduckError {
481
+ constructor(message, details) {
482
+ super(message, "CONFLUENCE_ERROR", details);
483
+ this.name = "ConfluenceError";
484
+ }
485
+ };
468
486
  }
469
487
  });
470
488
 
@@ -1613,9 +1631,13 @@ async function authCommand(subcommand) {
1613
1631
  await authStatus();
1614
1632
  } else if (subcommand === "bitbucket") {
1615
1633
  await authBitbucket();
1634
+ } else if (subcommand === "gitlab") {
1635
+ await authGitLab();
1636
+ } else if (subcommand === "atlassian") {
1637
+ await authAtlassian();
1616
1638
  } else {
1617
1639
  logger.error(`Unknown auth subcommand: ${subcommand}`);
1618
- logger.info("Available subcommands: login, status, bitbucket");
1640
+ logger.info("Available subcommands: login, status, bitbucket, gitlab, atlassian");
1619
1641
  process.exit(1);
1620
1642
  }
1621
1643
  }
@@ -1787,6 +1809,150 @@ User: ${user.display_name}
1787
1809
  process.exit(1);
1788
1810
  }
1789
1811
  }
1812
+ async function authGitLab() {
1813
+ logger.log("");
1814
+ logger.log(
1815
+ boxen(
1816
+ 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"),
1817
+ boxStyles.info
1818
+ )
1819
+ );
1820
+ logger.log("");
1821
+ try {
1822
+ const instanceUrl = await input({
1823
+ message: "GitLab instance URL (press Enter for gitlab.com):",
1824
+ default: "https://gitlab.com"
1825
+ });
1826
+ const accessToken = await input({
1827
+ message: "Personal Access Token:",
1828
+ validate: (value) => value.length > 0 ? true : "Token cannot be empty"
1829
+ });
1830
+ const testUrl = `${instanceUrl.replace(/\/$/, "")}/api/v4/user`;
1831
+ const response = await fetch(testUrl, {
1832
+ headers: { "PRIVATE-TOKEN": accessToken }
1833
+ });
1834
+ if (!response.ok) {
1835
+ logger.log("");
1836
+ logger.log(
1837
+ boxen(
1838
+ theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL or access token",
1839
+ boxStyles.error
1840
+ )
1841
+ );
1842
+ logger.log("");
1843
+ process.exit(1);
1844
+ }
1845
+ const user = await response.json();
1846
+ const credentials = {
1847
+ accessToken,
1848
+ instanceUrl: instanceUrl === "https://gitlab.com" ? void 0 : instanceUrl
1849
+ };
1850
+ await storageService.setServiceCredentials("gitlab", credentials);
1851
+ logger.log("");
1852
+ logger.log(
1853
+ boxen(
1854
+ theme.success("\u2713 Successfully authenticated with GitLab") + `
1855
+
1856
+ Instance: ${instanceUrl}
1857
+ User: ${user.name} (@${user.username})`,
1858
+ boxStyles.success
1859
+ )
1860
+ );
1861
+ logger.log("");
1862
+ } catch (error) {
1863
+ const err = error;
1864
+ logger.log("");
1865
+ logger.log(
1866
+ boxen(
1867
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
1868
+ boxStyles.error
1869
+ )
1870
+ );
1871
+ logger.log("");
1872
+ process.exit(1);
1873
+ }
1874
+ }
1875
+ async function authAtlassian() {
1876
+ logger.log("");
1877
+ logger.log(
1878
+ boxen(
1879
+ theme.info("Atlassian API Token Authentication") + "\n\nThis token works for Jira, Confluence, and Bitbucket\n\nCreate an API Token at:\n" + colors.highlight("https://id.atlassian.com/manage-profile/security/api-tokens") + "\n\nRequired access:\n \u2022 Jira: Read issues\n \u2022 Confluence: Read pages\n \u2022 Bitbucket: Read repositories",
1880
+ boxStyles.info
1881
+ )
1882
+ );
1883
+ logger.log("");
1884
+ try {
1885
+ const instanceUrl = await input({
1886
+ message: "Atlassian instance URL (e.g., company.atlassian.net):",
1887
+ validate: (value) => value.length > 0 ? true : "Instance URL cannot be empty"
1888
+ });
1889
+ const email = await input({
1890
+ message: "Atlassian account email:",
1891
+ validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
1892
+ });
1893
+ const apiToken = await input({
1894
+ message: "API Token:",
1895
+ validate: (value) => value.length > 0 ? true : "API token cannot be empty"
1896
+ });
1897
+ const testInstanceUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
1898
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
1899
+ const response = await fetch(`${testInstanceUrl}/rest/api/2/myself`, {
1900
+ headers: { Authorization: `Basic ${auth}` }
1901
+ });
1902
+ if (!response.ok) {
1903
+ logger.log("");
1904
+ logger.log(
1905
+ boxen(
1906
+ theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL, email, or API token\nMake sure the instance URL is correct (e.g., company.atlassian.net)",
1907
+ boxStyles.error
1908
+ )
1909
+ );
1910
+ logger.log("");
1911
+ process.exit(1);
1912
+ }
1913
+ const user = await response.json();
1914
+ const credentials = {
1915
+ accessToken: apiToken,
1916
+ username: email,
1917
+ instanceUrl: instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`
1918
+ };
1919
+ await storageService.setServiceCredentials("jira", credentials);
1920
+ await storageService.setServiceCredentials("confluence", credentials);
1921
+ await storageService.setServiceCredentials("bitbucket", {
1922
+ ...credentials,
1923
+ instanceUrl: "https://api.bitbucket.org"
1924
+ // Bitbucket uses different API base
1925
+ });
1926
+ logger.log("");
1927
+ logger.log(
1928
+ boxen(
1929
+ theme.success("\u2713 Successfully authenticated with Atlassian") + `
1930
+
1931
+ Instance: ${instanceUrl}
1932
+ User: ${user.displayName}
1933
+ Email: ${user.emailAddress}
1934
+
1935
+ Services configured:
1936
+ \u2022 Jira
1937
+ \u2022 Confluence
1938
+ \u2022 Bitbucket`,
1939
+ boxStyles.success
1940
+ )
1941
+ );
1942
+ logger.log("");
1943
+ } catch (error) {
1944
+ const err = error;
1945
+ logger.log("");
1946
+ logger.log(
1947
+ boxen(
1948
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
1949
+ boxStyles.error
1950
+ )
1951
+ );
1952
+ logger.log("");
1953
+ process.exit(1);
1954
+ }
1955
+ }
1790
1956
 
1791
1957
  // src/commands/sync.ts
1792
1958
  init_esm_shims();
@@ -1911,6 +2077,12 @@ var SourceDetector = class {
1911
2077
  return true;
1912
2078
  } else if (type === "bitbucket" || type === "atlassian") {
1913
2079
  return await storageService.isServiceAuthenticated("bitbucket");
2080
+ } else if (type === "gitlab") {
2081
+ return await storageService.isServiceAuthenticated("gitlab");
2082
+ } else if (type === "jira") {
2083
+ return await storageService.isServiceAuthenticated("jira");
2084
+ } else if (type === "confluence") {
2085
+ return await storageService.isServiceAuthenticated("confluence");
1914
2086
  }
1915
2087
  return false;
1916
2088
  } catch {
@@ -2690,102 +2862,928 @@ var BitbucketSyncAdapter = class {
2690
2862
  };
2691
2863
  var bitbucketSyncAdapter = new BitbucketSyncAdapter();
2692
2864
 
2693
- // src/sync/adapter-factory.ts
2694
- var AdapterFactory = class {
2865
+ // src/sync/gitlab-adapter.ts
2866
+ init_esm_shims();
2867
+
2868
+ // src/services/gitlab.service.ts
2869
+ init_esm_shims();
2870
+ init_errors();
2871
+ init_logger();
2872
+ init_storage_service();
2873
+ import { exec as exec5 } from "child_process";
2874
+ import { promisify as promisify5 } from "util";
2875
+ import { URLSearchParams as URLSearchParams3 } from "url";
2876
+ var execAsync5 = promisify5(exec5);
2877
+ var GitLabService = class {
2878
+ DEFAULT_INSTANCE = "https://gitlab.com";
2879
+ MAX_DESCRIPTION_LENGTH = 5e3;
2695
2880
  /**
2696
- * Get adapter for a specific source type
2881
+ * Get stored GitLab credentials
2697
2882
  */
2698
- static getAdapter(source) {
2699
- switch (source) {
2700
- case "github":
2701
- return githubSyncAdapter;
2702
- case "bitbucket":
2703
- case "atlassian":
2704
- return bitbucketSyncAdapter;
2705
- // Bitbucket Cloud and Server use same adapter
2706
- case "gitlab":
2707
- throw new Error(`${source} adapter not yet implemented`);
2708
- default:
2709
- throw new Error(`Unknown source type: ${source}`);
2883
+ async getCredentials() {
2884
+ const creds = await storageService.getServiceCredentials("gitlab");
2885
+ if (!creds || !creds.accessToken) {
2886
+ throw new GitLabError("Not authenticated with GitLab", {
2887
+ hint: "Run: bragduck auth gitlab"
2888
+ });
2889
+ }
2890
+ if (creds.expiresAt && creds.expiresAt < Date.now()) {
2891
+ throw new GitLabError("Access token has expired", {
2892
+ hint: "Run: bragduck auth gitlab"
2893
+ });
2710
2894
  }
2895
+ return {
2896
+ accessToken: creds.accessToken,
2897
+ instanceUrl: creds.instanceUrl || this.DEFAULT_INSTANCE
2898
+ };
2711
2899
  }
2712
2900
  /**
2713
- * Check if adapter is available for source
2901
+ * Make authenticated request to GitLab API
2714
2902
  */
2715
- static isSupported(source) {
2716
- return source === "github" || source === "bitbucket" || source === "atlassian";
2903
+ async request(endpoint) {
2904
+ const { accessToken, instanceUrl } = await this.getCredentials();
2905
+ const baseUrl = `${instanceUrl}/api/v4`;
2906
+ const url = `${baseUrl}${endpoint}`;
2907
+ logger.debug(`GitLab API: GET ${endpoint}`);
2908
+ const response = await fetch(url, {
2909
+ headers: {
2910
+ "PRIVATE-TOKEN": accessToken,
2911
+ Accept: "application/json"
2912
+ }
2913
+ });
2914
+ if (!response.ok) {
2915
+ const statusText = response.statusText;
2916
+ const status = response.status;
2917
+ if (status === 401) {
2918
+ throw new GitLabError("Invalid or expired access token", {
2919
+ hint: "Run: bragduck auth gitlab",
2920
+ originalError: statusText
2921
+ });
2922
+ } else if (status === 403) {
2923
+ throw new GitLabError("Insufficient token permissions", {
2924
+ hint: "Token requires scopes: api, read_api, read_user",
2925
+ originalError: statusText
2926
+ });
2927
+ } else if (status === 404) {
2928
+ throw new GitLabError("Resource not found", {
2929
+ hint: "Check repository URL and token permissions",
2930
+ originalError: statusText
2931
+ });
2932
+ } else if (status === 429) {
2933
+ throw new GitLabError("Rate limit exceeded", {
2934
+ hint: "Wait a moment and try again",
2935
+ originalError: statusText
2936
+ });
2937
+ }
2938
+ throw new GitLabError(`API request failed: ${statusText}`, {
2939
+ originalError: statusText
2940
+ });
2941
+ }
2942
+ return response.json();
2717
2943
  }
2718
- };
2719
-
2720
- // src/commands/sync.ts
2721
- init_logger();
2722
-
2723
- // src/utils/auth-helper.ts
2724
- init_esm_shims();
2725
- init_auth_service();
2726
- import boxen5 from "boxen";
2727
-
2728
- // src/commands/init.ts
2729
- init_esm_shims();
2730
- init_auth_service();
2731
- init_logger();
2732
- import ora from "ora";
2733
- import boxen3 from "boxen";
2734
- import chalk5 from "chalk";
2735
- async function initCommand() {
2736
- logger.log("");
2737
- logger.log(
2738
- boxen3(
2739
- chalk5.yellow.bold("\u26A0 Deprecation Notice") + `
2740
-
2741
- The ${chalk5.cyan("init")} command is deprecated.
2742
- Please use ${chalk5.cyan("bragduck auth login")} instead.
2743
-
2744
- ` + chalk5.dim("This command will be removed in v3.0.0"),
2745
- {
2746
- padding: 1,
2747
- borderStyle: "round",
2748
- borderColor: "yellow",
2749
- dimBorder: true
2944
+ /**
2945
+ * Extract namespace and project from git remote URL
2946
+ */
2947
+ parseRemoteUrl(url) {
2948
+ const match = url.match(/[:/]([\w-]+)\/([\w-]+?)(?:\.git)?$/);
2949
+ if (match && match[1] && match[2]) {
2950
+ return {
2951
+ namespace: match[1],
2952
+ project: match[2]
2953
+ };
2954
+ }
2955
+ return null;
2956
+ }
2957
+ /**
2958
+ * Get project path from git remote
2959
+ */
2960
+ async getProjectFromGit() {
2961
+ try {
2962
+ const { stdout } = await execAsync5("git remote get-url origin");
2963
+ const remoteUrl = stdout.trim();
2964
+ const parsed = this.parseRemoteUrl(remoteUrl);
2965
+ if (!parsed) {
2966
+ throw new GitLabError("Could not parse GitLab project from remote URL", {
2967
+ url: remoteUrl
2968
+ });
2750
2969
  }
2751
- )
2752
- );
2753
- logger.log("");
2754
- logger.info("Starting authentication flow...");
2755
- logger.log("");
2756
- const isAuthenticated = await authService.isAuthenticated();
2757
- if (isAuthenticated) {
2758
- const userInfo = authService.getUserInfo();
2759
- if (userInfo) {
2760
- logger.log(
2761
- boxen3(
2762
- `${chalk5.yellow("Already authenticated!")}
2763
-
2764
- ${chalk5.gray("User:")} ${userInfo.name}
2765
- ${chalk5.gray("Email:")} ${userInfo.email}
2766
-
2767
- ${chalk5.dim("Run")} ${chalk5.cyan("bragduck logout")} ${chalk5.dim("to sign out")}`,
2768
- {
2769
- padding: 1,
2770
- margin: 1,
2771
- borderStyle: "round",
2772
- borderColor: "yellow"
2773
- }
2774
- )
2775
- );
2776
- logger.log("");
2777
- globalThis.setTimeout(() => {
2778
- process.exit(0);
2779
- }, 100);
2780
- return;
2970
+ return {
2971
+ ...parsed,
2972
+ projectPath: `${parsed.namespace}/${parsed.project}`
2973
+ };
2974
+ } catch (error) {
2975
+ if (error instanceof GitLabError) {
2976
+ throw error;
2977
+ }
2978
+ throw new GitError("Could not get git remote URL");
2781
2979
  }
2782
2980
  }
2783
- const spinner = ora("Opening browser for authentication...").start();
2784
- try {
2785
- spinner.text = "Waiting for authentication...";
2786
- const userInfo = await authService.login();
2787
- spinner.succeed("Authentication successful!");
2788
- logger.log("");
2981
+ /**
2982
+ * Validate that this is a GitLab repository and credentials work
2983
+ */
2984
+ async validateGitLabRepository() {
2985
+ await gitService.validateRepository();
2986
+ const { projectPath } = await this.getProjectFromGit();
2987
+ const encodedPath = encodeURIComponent(projectPath);
2988
+ try {
2989
+ await this.request(`/projects/${encodedPath}`);
2990
+ } catch (error) {
2991
+ if (error instanceof GitLabError) {
2992
+ throw error;
2993
+ }
2994
+ throw new GitLabError("Could not access GitLab project via API", {
2995
+ hint: "Check that this is a GitLab repository and your token is valid",
2996
+ originalError: error instanceof Error ? error.message : String(error)
2997
+ });
2998
+ }
2999
+ }
3000
+ /**
3001
+ * Get repository information
3002
+ */
3003
+ async getRepositoryInfo() {
3004
+ const { projectPath } = await this.getProjectFromGit();
3005
+ const encodedPath = encodeURIComponent(projectPath);
3006
+ const project = await this.request(`/projects/${encodedPath}`);
3007
+ const [owner, name] = projectPath.split("/");
3008
+ return {
3009
+ owner: owner || "",
3010
+ name: name || "",
3011
+ fullName: project.path_with_namespace,
3012
+ url: project.web_url
3013
+ };
3014
+ }
3015
+ /**
3016
+ * Fetch merged MRs with pagination
3017
+ */
3018
+ async getMergedMRs(options = {}) {
3019
+ const { projectPath } = await this.getProjectFromGit();
3020
+ const encodedPath = encodeURIComponent(projectPath);
3021
+ const params = new URLSearchParams3({
3022
+ state: "merged",
3023
+ order_by: "updated_at",
3024
+ sort: "desc",
3025
+ per_page: "100"
3026
+ });
3027
+ if (options.days) {
3028
+ const since = /* @__PURE__ */ new Date();
3029
+ since.setDate(since.getDate() - options.days);
3030
+ params.append("updated_after", since.toISOString());
3031
+ }
3032
+ if (options.author) {
3033
+ params.append("author_username", options.author);
3034
+ }
3035
+ const allMRs = [];
3036
+ let page = 1;
3037
+ while (true) {
3038
+ params.set("page", page.toString());
3039
+ const endpoint = `/projects/${encodedPath}/merge_requests?${params}`;
3040
+ const mrs = await this.request(endpoint);
3041
+ allMRs.push(...mrs);
3042
+ logger.debug(
3043
+ `Fetched ${mrs.length} MRs (total: ${allMRs.length})${mrs.length === 100 ? ", fetching next page..." : ""}`
3044
+ );
3045
+ if (options.limit && allMRs.length >= options.limit) {
3046
+ return allMRs.slice(0, options.limit);
3047
+ }
3048
+ if (mrs.length < 100) {
3049
+ break;
3050
+ }
3051
+ page++;
3052
+ }
3053
+ return allMRs;
3054
+ }
3055
+ /**
3056
+ * Get MRs by current user
3057
+ */
3058
+ async getMRsByCurrentUser(options = {}) {
3059
+ const username = await this.getCurrentGitLabUser();
3060
+ if (!username) {
3061
+ throw new GitLabError("Could not get current user username");
3062
+ }
3063
+ return this.getMergedMRs({
3064
+ ...options,
3065
+ author: username
3066
+ });
3067
+ }
3068
+ /**
3069
+ * Get current authenticated user
3070
+ */
3071
+ async getCurrentGitLabUser() {
3072
+ try {
3073
+ const user = await this.request("/user");
3074
+ return user.username;
3075
+ } catch {
3076
+ return null;
3077
+ }
3078
+ }
3079
+ /**
3080
+ * Transform GitLab MR to GitCommit format
3081
+ */
3082
+ transformMRToCommit(mr) {
3083
+ const message = this.formatMessage(mr.title, mr.description);
3084
+ return {
3085
+ sha: `mr-${mr.iid}`,
3086
+ // Use iid (project-scoped)
3087
+ message,
3088
+ author: mr.author.username,
3089
+ authorEmail: "",
3090
+ // Not available in MR response
3091
+ date: mr.created_at,
3092
+ url: mr.web_url,
3093
+ diffStats: {
3094
+ filesChanged: mr.diff_stats?.file_count || 0,
3095
+ insertions: mr.diff_stats?.additions || 0,
3096
+ deletions: mr.diff_stats?.deletions || 0
3097
+ }
3098
+ };
3099
+ }
3100
+ /**
3101
+ * Format MR message with title and description
3102
+ */
3103
+ formatMessage(title, description) {
3104
+ if (!description) return title;
3105
+ const truncated = description.substring(0, this.MAX_DESCRIPTION_LENGTH);
3106
+ return `${title}
3107
+
3108
+ ${truncated}`;
3109
+ }
3110
+ };
3111
+ var gitlabService = new GitLabService();
3112
+
3113
+ // src/sync/gitlab-adapter.ts
3114
+ var GitLabSyncAdapter = class {
3115
+ name = "gitlab";
3116
+ async validate() {
3117
+ await gitlabService.validateGitLabRepository();
3118
+ }
3119
+ async getRepositoryInfo() {
3120
+ const info = await gitlabService.getRepositoryInfo();
3121
+ return {
3122
+ owner: info.owner,
3123
+ name: info.name,
3124
+ fullName: info.fullName,
3125
+ url: info.url
3126
+ };
3127
+ }
3128
+ async fetchWorkItems(options) {
3129
+ let mrs;
3130
+ if (options.author) {
3131
+ mrs = await gitlabService.getMergedMRs({
3132
+ days: options.days,
3133
+ limit: options.limit,
3134
+ author: options.author
3135
+ });
3136
+ } else {
3137
+ mrs = await gitlabService.getMRsByCurrentUser({
3138
+ days: options.days,
3139
+ limit: options.limit
3140
+ });
3141
+ }
3142
+ return mrs.map((mr) => gitlabService.transformMRToCommit(mr));
3143
+ }
3144
+ async isAuthenticated() {
3145
+ try {
3146
+ await gitlabService.validateGitLabRepository();
3147
+ return true;
3148
+ } catch {
3149
+ return false;
3150
+ }
3151
+ }
3152
+ async getCurrentUser() {
3153
+ return gitlabService.getCurrentGitLabUser();
3154
+ }
3155
+ };
3156
+ var gitlabSyncAdapter = new GitLabSyncAdapter();
3157
+
3158
+ // src/sync/jira-adapter.ts
3159
+ init_esm_shims();
3160
+
3161
+ // src/services/jira.service.ts
3162
+ init_esm_shims();
3163
+ init_errors();
3164
+ init_logger();
3165
+ init_storage_service();
3166
+ var JiraService = class {
3167
+ MAX_DESCRIPTION_LENGTH = 5e3;
3168
+ /**
3169
+ * Get stored Jira credentials
3170
+ */
3171
+ async getCredentials() {
3172
+ const creds = await storageService.getServiceCredentials("jira");
3173
+ if (!creds || !creds.username || !creds.accessToken || !creds.instanceUrl) {
3174
+ throw new JiraError("Not authenticated with Jira", {
3175
+ hint: "Run: bragduck auth atlassian"
3176
+ });
3177
+ }
3178
+ if (creds.expiresAt && creds.expiresAt < Date.now()) {
3179
+ throw new JiraError("API token has expired", {
3180
+ hint: "Run: bragduck auth atlassian"
3181
+ });
3182
+ }
3183
+ return {
3184
+ email: creds.username,
3185
+ apiToken: creds.accessToken,
3186
+ instanceUrl: creds.instanceUrl
3187
+ };
3188
+ }
3189
+ /**
3190
+ * Make authenticated request to Jira API
3191
+ */
3192
+ async request(endpoint, method = "GET", body) {
3193
+ const { email, apiToken, instanceUrl } = await this.getCredentials();
3194
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
3195
+ const baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
3196
+ logger.debug(`Jira API: ${method} ${endpoint}`);
3197
+ const options = {
3198
+ method,
3199
+ headers: {
3200
+ Authorization: `Basic ${auth}`,
3201
+ "Content-Type": "application/json",
3202
+ Accept: "application/json"
3203
+ }
3204
+ };
3205
+ if (body) {
3206
+ options.body = JSON.stringify(body);
3207
+ }
3208
+ const response = await fetch(`${baseUrl}${endpoint}`, options);
3209
+ if (!response.ok) {
3210
+ const statusText = response.statusText;
3211
+ const status = response.status;
3212
+ if (status === 401) {
3213
+ throw new JiraError("Invalid or expired API token", {
3214
+ hint: "Run: bragduck auth atlassian",
3215
+ originalError: statusText
3216
+ });
3217
+ } else if (status === 403) {
3218
+ throw new JiraError("Forbidden - check token permissions", {
3219
+ hint: "Token needs: read access to issues",
3220
+ originalError: statusText
3221
+ });
3222
+ } else if (status === 404) {
3223
+ throw new JiraError("Resource not found", {
3224
+ originalError: statusText
3225
+ });
3226
+ } else if (status === 429) {
3227
+ throw new JiraError("Rate limit exceeded", {
3228
+ hint: "Wait a few minutes before trying again",
3229
+ originalError: statusText
3230
+ });
3231
+ }
3232
+ throw new JiraError(`API request failed: ${statusText}`, {
3233
+ originalError: statusText
3234
+ });
3235
+ }
3236
+ return response.json();
3237
+ }
3238
+ /**
3239
+ * Validate Jira instance and credentials
3240
+ */
3241
+ async validateJiraInstance() {
3242
+ try {
3243
+ await this.request("/rest/api/2/myself");
3244
+ } catch (error) {
3245
+ if (error instanceof JiraError) {
3246
+ throw error;
3247
+ }
3248
+ throw new JiraError("Could not access Jira instance via API", {
3249
+ hint: "Check that the instance URL is correct and your credentials are valid",
3250
+ originalError: error instanceof Error ? error.message : String(error)
3251
+ });
3252
+ }
3253
+ }
3254
+ /**
3255
+ * Get current user's email
3256
+ */
3257
+ async getCurrentUser() {
3258
+ try {
3259
+ const user = await this.request("/rest/api/2/myself");
3260
+ return user.emailAddress;
3261
+ } catch {
3262
+ return null;
3263
+ }
3264
+ }
3265
+ /**
3266
+ * Build JQL query from options
3267
+ */
3268
+ buildJQL(options) {
3269
+ const queries = [];
3270
+ queries.push("status IN (Done, Resolved, Closed)");
3271
+ if (options.days) {
3272
+ queries.push(`updated >= -${options.days}d`);
3273
+ }
3274
+ if (options.author) {
3275
+ queries.push(`creator = "${options.author}"`);
3276
+ }
3277
+ if (options.jql) {
3278
+ queries.push(`(${options.jql})`);
3279
+ }
3280
+ return queries.join(" AND ");
3281
+ }
3282
+ /**
3283
+ * Estimate issue complexity for impact scoring
3284
+ */
3285
+ estimateComplexity(issue) {
3286
+ const typeScores = {
3287
+ Epic: 500,
3288
+ Story: 200,
3289
+ Task: 100,
3290
+ Bug: 100,
3291
+ "Sub-task": 50
3292
+ };
3293
+ return typeScores[issue.fields.issuetype.name] || 100;
3294
+ }
3295
+ /**
3296
+ * Fetch issues with optional filtering
3297
+ */
3298
+ async getIssues(options = {}) {
3299
+ const jql = this.buildJQL(options);
3300
+ const fields = [
3301
+ "summary",
3302
+ "description",
3303
+ "created",
3304
+ "updated",
3305
+ "resolutiondate",
3306
+ "creator",
3307
+ "status",
3308
+ "issuetype"
3309
+ ];
3310
+ const allIssues = [];
3311
+ let startAt = 0;
3312
+ const maxResults = 100;
3313
+ while (true) {
3314
+ const endpoint = `/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=${fields.join(",")}`;
3315
+ const response = await this.request(endpoint);
3316
+ allIssues.push(...response.issues);
3317
+ logger.debug(
3318
+ `Fetched ${response.issues.length} issues (total: ${allIssues.length} of ${response.total})${startAt + maxResults < response.total ? ", fetching next page..." : ""}`
3319
+ );
3320
+ if (startAt + maxResults >= response.total) {
3321
+ break;
3322
+ }
3323
+ if (options.limit && allIssues.length >= options.limit) {
3324
+ return allIssues.slice(0, options.limit).map((issue) => this.transformIssueToCommit(issue));
3325
+ }
3326
+ startAt += maxResults;
3327
+ }
3328
+ return allIssues.map((issue) => this.transformIssueToCommit(issue));
3329
+ }
3330
+ /**
3331
+ * Fetch issues for the current authenticated user
3332
+ */
3333
+ async getIssuesByCurrentUser(options = {}) {
3334
+ const email = await this.getCurrentUser();
3335
+ if (!email) {
3336
+ throw new JiraError("Could not get current user");
3337
+ }
3338
+ return this.getIssues({
3339
+ ...options,
3340
+ author: email
3341
+ });
3342
+ }
3343
+ /**
3344
+ * Transform Jira issue to GitCommit format with external fields
3345
+ */
3346
+ transformIssueToCommit(issue, instanceUrl) {
3347
+ let message = issue.fields.summary;
3348
+ if (issue.fields.description) {
3349
+ const truncatedDesc = issue.fields.description.substring(0, this.MAX_DESCRIPTION_LENGTH);
3350
+ message = `${issue.fields.summary}
3351
+
3352
+ ${truncatedDesc}`;
3353
+ }
3354
+ const date = issue.fields.resolutiondate || issue.fields.updated;
3355
+ let baseUrl = "https://jira.atlassian.net";
3356
+ if (instanceUrl) {
3357
+ baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
3358
+ } else {
3359
+ try {
3360
+ const creds = storageService.getServiceCredentials("jira");
3361
+ if (creds?.instanceUrl) {
3362
+ baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
3363
+ }
3364
+ } catch {
3365
+ }
3366
+ }
3367
+ const url = `${baseUrl}/browse/${issue.key}`;
3368
+ return {
3369
+ sha: issue.key,
3370
+ message,
3371
+ author: issue.fields.creator.displayName,
3372
+ authorEmail: issue.fields.creator.emailAddress,
3373
+ date,
3374
+ url,
3375
+ diffStats: {
3376
+ filesChanged: 0,
3377
+ insertions: this.estimateComplexity(issue),
3378
+ deletions: 0
3379
+ },
3380
+ externalId: issue.key,
3381
+ externalType: "issue",
3382
+ externalSource: "jira",
3383
+ externalUrl: url
3384
+ };
3385
+ }
3386
+ };
3387
+ var jiraService = new JiraService();
3388
+
3389
+ // src/sync/jira-adapter.ts
3390
+ var jiraSyncAdapter = {
3391
+ name: "jira",
3392
+ async validate() {
3393
+ await jiraService.validateJiraInstance();
3394
+ },
3395
+ async getRepositoryInfo() {
3396
+ const user = await jiraService.getCurrentUser();
3397
+ const creds = await jiraService.getCredentials();
3398
+ const userName = user || "Unknown User";
3399
+ const baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
3400
+ return {
3401
+ owner: userName,
3402
+ name: "Jira Issues",
3403
+ fullName: `${userName}'s Jira Issues`,
3404
+ url: baseUrl
3405
+ };
3406
+ },
3407
+ async fetchWorkItems(options) {
3408
+ const author = options.author === "current" ? await this.getCurrentUser() : options.author;
3409
+ return jiraService.getIssues({
3410
+ days: options.days,
3411
+ limit: options.limit,
3412
+ author: author || void 0
3413
+ });
3414
+ },
3415
+ async isAuthenticated() {
3416
+ try {
3417
+ await this.validate();
3418
+ return true;
3419
+ } catch {
3420
+ return false;
3421
+ }
3422
+ },
3423
+ async getCurrentUser() {
3424
+ return jiraService.getCurrentUser();
3425
+ }
3426
+ };
3427
+
3428
+ // src/sync/confluence-adapter.ts
3429
+ init_esm_shims();
3430
+
3431
+ // src/services/confluence.service.ts
3432
+ init_esm_shims();
3433
+ init_errors();
3434
+ init_logger();
3435
+ init_storage_service();
3436
+ var ConfluenceService = class {
3437
+ /**
3438
+ * Get stored Confluence credentials
3439
+ */
3440
+ async getCredentials() {
3441
+ const creds = await storageService.getServiceCredentials("confluence");
3442
+ if (!creds || !creds.username || !creds.accessToken || !creds.instanceUrl) {
3443
+ throw new ConfluenceError("Not authenticated with Confluence", {
3444
+ hint: "Run: bragduck auth atlassian"
3445
+ });
3446
+ }
3447
+ if (creds.expiresAt && creds.expiresAt < Date.now()) {
3448
+ throw new ConfluenceError("API token has expired", {
3449
+ hint: "Run: bragduck auth atlassian"
3450
+ });
3451
+ }
3452
+ return {
3453
+ email: creds.username,
3454
+ apiToken: creds.accessToken,
3455
+ instanceUrl: creds.instanceUrl
3456
+ };
3457
+ }
3458
+ /**
3459
+ * Make authenticated request to Confluence API
3460
+ */
3461
+ async request(endpoint, method = "GET", body) {
3462
+ const { email, apiToken, instanceUrl } = await this.getCredentials();
3463
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
3464
+ const baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
3465
+ logger.debug(`Confluence API: ${method} ${endpoint}`);
3466
+ const options = {
3467
+ method,
3468
+ headers: {
3469
+ Authorization: `Basic ${auth}`,
3470
+ "Content-Type": "application/json",
3471
+ Accept: "application/json"
3472
+ }
3473
+ };
3474
+ if (body) {
3475
+ options.body = JSON.stringify(body);
3476
+ }
3477
+ const response = await fetch(`${baseUrl}${endpoint}`, options);
3478
+ if (!response.ok) {
3479
+ const statusText = response.statusText;
3480
+ const status = response.status;
3481
+ if (status === 401) {
3482
+ throw new ConfluenceError("Invalid or expired API token", {
3483
+ hint: "Run: bragduck auth atlassian",
3484
+ originalError: statusText
3485
+ });
3486
+ } else if (status === 403) {
3487
+ throw new ConfluenceError("Forbidden - check token permissions", {
3488
+ hint: "Token needs: read access to pages",
3489
+ originalError: statusText
3490
+ });
3491
+ } else if (status === 404) {
3492
+ throw new ConfluenceError("Resource not found", {
3493
+ originalError: statusText
3494
+ });
3495
+ } else if (status === 429) {
3496
+ throw new ConfluenceError("Rate limit exceeded", {
3497
+ hint: "Wait a few minutes before trying again",
3498
+ originalError: statusText
3499
+ });
3500
+ }
3501
+ throw new ConfluenceError(`API request failed: ${statusText}`, {
3502
+ originalError: statusText
3503
+ });
3504
+ }
3505
+ return response.json();
3506
+ }
3507
+ /**
3508
+ * Validate Confluence instance and credentials
3509
+ */
3510
+ async validateConfluenceInstance() {
3511
+ try {
3512
+ await this.request("/wiki/rest/api/content?type=page&limit=1");
3513
+ } catch (error) {
3514
+ if (error instanceof ConfluenceError) {
3515
+ throw error;
3516
+ }
3517
+ throw new ConfluenceError("Could not access Confluence instance via API", {
3518
+ hint: "Check that the instance URL is correct and your credentials are valid",
3519
+ originalError: error instanceof Error ? error.message : String(error)
3520
+ });
3521
+ }
3522
+ }
3523
+ /**
3524
+ * Get current user's email
3525
+ */
3526
+ async getCurrentUser() {
3527
+ try {
3528
+ const creds = await this.getCredentials();
3529
+ return creds.email;
3530
+ } catch {
3531
+ return null;
3532
+ }
3533
+ }
3534
+ /**
3535
+ * Build CQL query from options
3536
+ */
3537
+ buildCQL(options) {
3538
+ const queries = [];
3539
+ queries.push("type=page");
3540
+ if (options.days) {
3541
+ queries.push(`lastModified >= now("-${options.days}d")`);
3542
+ }
3543
+ if (options.author) {
3544
+ queries.push(`creator = "${options.author}"`);
3545
+ }
3546
+ if (options.cql) {
3547
+ queries.push(`(${options.cql})`);
3548
+ }
3549
+ return queries.join(" AND ");
3550
+ }
3551
+ /**
3552
+ * Estimate page size for impact scoring
3553
+ */
3554
+ estimatePageSize(page) {
3555
+ const content = page.body?.storage?.value || "";
3556
+ return Math.ceil(content.length / 80);
3557
+ }
3558
+ /**
3559
+ * Fetch pages with optional filtering
3560
+ */
3561
+ async getPages(options = {}) {
3562
+ const cql = this.buildCQL(options);
3563
+ const allPages = [];
3564
+ let start = 0;
3565
+ const limit = 100;
3566
+ while (true) {
3567
+ const params = {
3568
+ type: "page",
3569
+ status: "current",
3570
+ start: start.toString(),
3571
+ limit: limit.toString(),
3572
+ expand: "version,body.storage"
3573
+ };
3574
+ if (cql) {
3575
+ params.cql = cql;
3576
+ }
3577
+ const queryString = Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
3578
+ const endpoint = `/wiki/rest/api/content?${queryString}`;
3579
+ const response = await this.request(endpoint);
3580
+ allPages.push(...response.results);
3581
+ logger.debug(
3582
+ `Fetched ${response.results.length} pages (total: ${allPages.length})${response.results.length === limit ? ", fetching next page..." : ""}`
3583
+ );
3584
+ if (response.results.length < limit) {
3585
+ break;
3586
+ }
3587
+ if (options.limit && allPages.length >= options.limit) {
3588
+ return allPages.slice(0, options.limit).map((page) => this.transformPageToCommit(page));
3589
+ }
3590
+ start += limit;
3591
+ }
3592
+ return allPages.map((page) => this.transformPageToCommit(page));
3593
+ }
3594
+ /**
3595
+ * Fetch pages for the current authenticated user
3596
+ */
3597
+ async getPagesByCurrentUser(options = {}) {
3598
+ const email = await this.getCurrentUser();
3599
+ if (!email) {
3600
+ throw new ConfluenceError("Could not get current user");
3601
+ }
3602
+ return this.getPages({
3603
+ ...options,
3604
+ author: email
3605
+ });
3606
+ }
3607
+ /**
3608
+ * Transform Confluence page to GitCommit format with external fields
3609
+ */
3610
+ transformPageToCommit(page, instanceUrl) {
3611
+ const message = `${page.title}
3612
+
3613
+ [Confluence Page v${page.version.number}]`;
3614
+ let baseUrl = "https://confluence.atlassian.net";
3615
+ if (instanceUrl) {
3616
+ baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
3617
+ } else {
3618
+ try {
3619
+ const creds = storageService.getServiceCredentials("confluence");
3620
+ if (creds?.instanceUrl) {
3621
+ baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
3622
+ }
3623
+ } catch {
3624
+ }
3625
+ }
3626
+ const url = `${baseUrl}/wiki${page._links.webui}`;
3627
+ return {
3628
+ sha: page.id,
3629
+ message,
3630
+ author: page.version.by.displayName,
3631
+ authorEmail: page.version.by.email,
3632
+ date: page.version.when,
3633
+ url,
3634
+ diffStats: {
3635
+ filesChanged: 1,
3636
+ insertions: this.estimatePageSize(page),
3637
+ deletions: 0
3638
+ },
3639
+ externalId: page.id,
3640
+ externalType: "page",
3641
+ externalSource: "confluence",
3642
+ externalUrl: url
3643
+ };
3644
+ }
3645
+ };
3646
+ var confluenceService = new ConfluenceService();
3647
+
3648
+ // src/sync/confluence-adapter.ts
3649
+ var confluenceSyncAdapter = {
3650
+ name: "confluence",
3651
+ async validate() {
3652
+ await confluenceService.validateConfluenceInstance();
3653
+ },
3654
+ async getRepositoryInfo() {
3655
+ const user = await confluenceService.getCurrentUser();
3656
+ const creds = await confluenceService.getCredentials();
3657
+ const userName = user || "Unknown User";
3658
+ const baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
3659
+ return {
3660
+ owner: userName,
3661
+ name: "Confluence Pages",
3662
+ fullName: `${userName}'s Confluence Pages`,
3663
+ url: `${baseUrl}/wiki`
3664
+ };
3665
+ },
3666
+ async fetchWorkItems(options) {
3667
+ const author = options.author === "current" ? await this.getCurrentUser() : options.author;
3668
+ return confluenceService.getPages({
3669
+ days: options.days,
3670
+ limit: options.limit,
3671
+ author: author || void 0
3672
+ });
3673
+ },
3674
+ async isAuthenticated() {
3675
+ try {
3676
+ await this.validate();
3677
+ return true;
3678
+ } catch {
3679
+ return false;
3680
+ }
3681
+ },
3682
+ async getCurrentUser() {
3683
+ return confluenceService.getCurrentUser();
3684
+ }
3685
+ };
3686
+
3687
+ // src/sync/adapter-factory.ts
3688
+ var AdapterFactory = class {
3689
+ /**
3690
+ * Get adapter for a specific source type
3691
+ */
3692
+ static getAdapter(source) {
3693
+ switch (source) {
3694
+ case "github":
3695
+ return githubSyncAdapter;
3696
+ case "bitbucket":
3697
+ case "atlassian":
3698
+ return bitbucketSyncAdapter;
3699
+ // Bitbucket Cloud and Server use same adapter
3700
+ case "gitlab":
3701
+ return gitlabSyncAdapter;
3702
+ case "jira":
3703
+ return jiraSyncAdapter;
3704
+ case "confluence":
3705
+ return confluenceSyncAdapter;
3706
+ default:
3707
+ throw new Error(`Unknown source type: ${source}`);
3708
+ }
3709
+ }
3710
+ /**
3711
+ * Check if adapter is available for source
3712
+ */
3713
+ static isSupported(source) {
3714
+ return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab" || source === "jira" || source === "confluence";
3715
+ }
3716
+ };
3717
+
3718
+ // src/commands/sync.ts
3719
+ init_logger();
3720
+
3721
+ // src/utils/auth-helper.ts
3722
+ init_esm_shims();
3723
+ init_auth_service();
3724
+ import boxen5 from "boxen";
3725
+
3726
+ // src/commands/init.ts
3727
+ init_esm_shims();
3728
+ init_auth_service();
3729
+ init_logger();
3730
+ import ora from "ora";
3731
+ import boxen3 from "boxen";
3732
+ import chalk5 from "chalk";
3733
+ async function initCommand() {
3734
+ logger.log("");
3735
+ logger.log(
3736
+ boxen3(
3737
+ chalk5.yellow.bold("\u26A0 Deprecation Notice") + `
3738
+
3739
+ The ${chalk5.cyan("init")} command is deprecated.
3740
+ Please use ${chalk5.cyan("bragduck auth login")} instead.
3741
+
3742
+ ` + chalk5.dim("This command will be removed in v3.0.0"),
3743
+ {
3744
+ padding: 1,
3745
+ borderStyle: "round",
3746
+ borderColor: "yellow",
3747
+ dimBorder: true
3748
+ }
3749
+ )
3750
+ );
3751
+ logger.log("");
3752
+ logger.info("Starting authentication flow...");
3753
+ logger.log("");
3754
+ const isAuthenticated = await authService.isAuthenticated();
3755
+ if (isAuthenticated) {
3756
+ const userInfo = authService.getUserInfo();
3757
+ if (userInfo) {
3758
+ logger.log(
3759
+ boxen3(
3760
+ `${chalk5.yellow("Already authenticated!")}
3761
+
3762
+ ${chalk5.gray("User:")} ${userInfo.name}
3763
+ ${chalk5.gray("Email:")} ${userInfo.email}
3764
+
3765
+ ${chalk5.dim("Run")} ${chalk5.cyan("bragduck logout")} ${chalk5.dim("to sign out")}`,
3766
+ {
3767
+ padding: 1,
3768
+ margin: 1,
3769
+ borderStyle: "round",
3770
+ borderColor: "yellow"
3771
+ }
3772
+ )
3773
+ );
3774
+ logger.log("");
3775
+ globalThis.setTimeout(() => {
3776
+ process.exit(0);
3777
+ }, 100);
3778
+ return;
3779
+ }
3780
+ }
3781
+ const spinner = ora("Opening browser for authentication...").start();
3782
+ try {
3783
+ spinner.text = "Waiting for authentication...";
3784
+ const userInfo = await authService.login();
3785
+ spinner.succeed("Authentication successful!");
3786
+ logger.log("");
2789
3787
  logger.log(
2790
3788
  boxen3(
2791
3789
  `${chalk5.green.bold("\u2713 Successfully authenticated!")}
@@ -3278,25 +4276,46 @@ async function syncCommand(options = {}) {
3278
4276
  logger.debug(`Subscription tier "${subscriptionStatus.tier}" - proceeding with sync`);
3279
4277
  const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Detecting repository source");
3280
4278
  detectionSpinner.start();
3281
- const detectionResult = await sourceDetector.detectSources();
3282
- if (detectionResult.detected.length === 0) {
3283
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "No supported sources detected");
3284
- logger.log("");
3285
- logger.info("Make sure you are in a git repository with a remote URL");
3286
- return;
4279
+ let sourceType = options.source;
4280
+ if (!sourceType) {
4281
+ try {
4282
+ const detectionResult = await sourceDetector.detectSources();
4283
+ sourceType = detectionResult.recommended;
4284
+ if (!sourceType && detectionResult.detected.length === 0) {
4285
+ failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "No supported sources detected");
4286
+ logger.log("");
4287
+ logger.info("Make sure you are in a git repository with a remote URL");
4288
+ logger.info("Or use --source flag for non-git sources: --source jira|confluence");
4289
+ return;
4290
+ }
4291
+ } catch {
4292
+ failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Not a git repository");
4293
+ logger.log("");
4294
+ logger.info("Use --source flag to specify source: --source jira|confluence");
4295
+ return;
4296
+ }
3287
4297
  }
3288
- const sourceType = options.source || detectionResult.recommended;
3289
4298
  if (!sourceType) {
3290
4299
  failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Could not determine source");
4300
+ logger.log("");
4301
+ logger.info("Use --source flag: --source github|gitlab|bitbucket|jira|confluence");
3291
4302
  return;
3292
4303
  }
3293
4304
  if (!AdapterFactory.isSupported(sourceType)) {
3294
4305
  failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source ${sourceType} not yet supported`);
3295
4306
  logger.log("");
3296
- logger.info(`Currently supported: GitHub`);
3297
- logger.info(`Coming soon: GitLab, Atlassian, Bitbucket`);
4307
+ logger.info(`Currently supported: GitHub, GitLab, Bitbucket, Jira, Confluence`);
3298
4308
  return;
3299
4309
  }
4310
+ if (sourceType === "jira" || sourceType === "confluence") {
4311
+ const creds = await storageService.getServiceCredentials(sourceType);
4312
+ if (!creds || !creds.instanceUrl) {
4313
+ failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `No ${sourceType} instance configured`);
4314
+ logger.log("");
4315
+ logger.info(`Run: ${theme.command("bragduck auth atlassian")}`);
4316
+ return;
4317
+ }
4318
+ }
3300
4319
  succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source: ${theme.value(sourceType)}`);
3301
4320
  logger.log("");
3302
4321
  const adapter = AdapterFactory.getAdapter(sourceType);
@@ -3458,7 +4477,12 @@ async function syncCommand(options = {}) {
3458
4477
  impact_score: refined.suggested_impactLevel,
3459
4478
  impact_description: refined.impact_description,
3460
4479
  attachments: originalCommit?.url ? [originalCommit.url] : [],
3461
- orgId: selectedOrgId || void 0
4480
+ orgId: selectedOrgId || void 0,
4481
+ // External fields for non-git sources (Jira, Confluence, etc.)
4482
+ externalId: originalCommit?.externalId,
4483
+ externalType: originalCommit?.externalType,
4484
+ externalSource: originalCommit?.externalSource,
4485
+ externalUrl: originalCommit?.externalUrl
3462
4486
  };
3463
4487
  })
3464
4488
  };
@@ -4158,7 +5182,7 @@ var packageJsonPath = join7(__dirname6, "../../package.json");
4158
5182
  var packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
4159
5183
  var program = new Command();
4160
5184
  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) => {
5185
+ program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status, bitbucket, gitlab)").action(async (subcommand) => {
4162
5186
  try {
4163
5187
  await authCommand(subcommand);
4164
5188
  } catch (error) {