@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.
- package/dist/bin/bragduck.js +1123 -99
- package/dist/bin/bragduck.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/bragduck.js
CHANGED
|
@@ -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
|
|
2694
|
-
|
|
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
|
|
2881
|
+
* Get stored GitLab credentials
|
|
2697
2882
|
*/
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
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
|
-
*
|
|
2901
|
+
* Make authenticated request to GitLab API
|
|
2714
2902
|
*/
|
|
2715
|
-
|
|
2716
|
-
|
|
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
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
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
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
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
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
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
|
-
|
|
3282
|
-
if (
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
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) {
|