@bragduck/cli 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/bragduck.js +371 -5
- 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;
|
|
396
396
|
var init_errors = __esm({
|
|
397
397
|
"src/utils/errors.ts"() {
|
|
398
398
|
"use strict";
|
|
@@ -465,6 +465,12 @@ var init_errors = __esm({
|
|
|
465
465
|
this.name = "BitbucketError";
|
|
466
466
|
}
|
|
467
467
|
};
|
|
468
|
+
GitLabError = class extends BragduckError {
|
|
469
|
+
constructor(message, details) {
|
|
470
|
+
super(message, "GITLAB_ERROR", details);
|
|
471
|
+
this.name = "GitLabError";
|
|
472
|
+
}
|
|
473
|
+
};
|
|
468
474
|
}
|
|
469
475
|
});
|
|
470
476
|
|
|
@@ -1613,9 +1619,11 @@ async function authCommand(subcommand) {
|
|
|
1613
1619
|
await authStatus();
|
|
1614
1620
|
} else if (subcommand === "bitbucket") {
|
|
1615
1621
|
await authBitbucket();
|
|
1622
|
+
} else if (subcommand === "gitlab") {
|
|
1623
|
+
await authGitLab();
|
|
1616
1624
|
} else {
|
|
1617
1625
|
logger.error(`Unknown auth subcommand: ${subcommand}`);
|
|
1618
|
-
logger.info("Available subcommands: login, status, bitbucket");
|
|
1626
|
+
logger.info("Available subcommands: login, status, bitbucket, gitlab");
|
|
1619
1627
|
process.exit(1);
|
|
1620
1628
|
}
|
|
1621
1629
|
}
|
|
@@ -1787,6 +1795,69 @@ User: ${user.display_name}
|
|
|
1787
1795
|
process.exit(1);
|
|
1788
1796
|
}
|
|
1789
1797
|
}
|
|
1798
|
+
async function authGitLab() {
|
|
1799
|
+
logger.log("");
|
|
1800
|
+
logger.log(
|
|
1801
|
+
boxen(
|
|
1802
|
+
theme.info("GitLab Personal Access Token Authentication") + "\n\nCreate a Personal Access Token at:\n" + colors.highlight("https://gitlab.com/-/profile/personal_access_tokens") + "\n\nRequired scopes:\n \u2022 api (full API access)\n \u2022 read_api (read API)\n \u2022 read_user (read user info)\n\n" + theme.warning("For self-hosted GitLab, check your instance docs"),
|
|
1803
|
+
boxStyles.info
|
|
1804
|
+
)
|
|
1805
|
+
);
|
|
1806
|
+
logger.log("");
|
|
1807
|
+
try {
|
|
1808
|
+
const instanceUrl = await input({
|
|
1809
|
+
message: "GitLab instance URL (press Enter for gitlab.com):",
|
|
1810
|
+
default: "https://gitlab.com"
|
|
1811
|
+
});
|
|
1812
|
+
const accessToken = await input({
|
|
1813
|
+
message: "Personal Access Token:",
|
|
1814
|
+
validate: (value) => value.length > 0 ? true : "Token cannot be empty"
|
|
1815
|
+
});
|
|
1816
|
+
const testUrl = `${instanceUrl.replace(/\/$/, "")}/api/v4/user`;
|
|
1817
|
+
const response = await fetch(testUrl, {
|
|
1818
|
+
headers: { "PRIVATE-TOKEN": accessToken }
|
|
1819
|
+
});
|
|
1820
|
+
if (!response.ok) {
|
|
1821
|
+
logger.log("");
|
|
1822
|
+
logger.log(
|
|
1823
|
+
boxen(
|
|
1824
|
+
theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL or access token",
|
|
1825
|
+
boxStyles.error
|
|
1826
|
+
)
|
|
1827
|
+
);
|
|
1828
|
+
logger.log("");
|
|
1829
|
+
process.exit(1);
|
|
1830
|
+
}
|
|
1831
|
+
const user = await response.json();
|
|
1832
|
+
const credentials = {
|
|
1833
|
+
accessToken,
|
|
1834
|
+
instanceUrl: instanceUrl === "https://gitlab.com" ? void 0 : instanceUrl
|
|
1835
|
+
};
|
|
1836
|
+
await storageService.setServiceCredentials("gitlab", credentials);
|
|
1837
|
+
logger.log("");
|
|
1838
|
+
logger.log(
|
|
1839
|
+
boxen(
|
|
1840
|
+
theme.success("\u2713 Successfully authenticated with GitLab") + `
|
|
1841
|
+
|
|
1842
|
+
Instance: ${instanceUrl}
|
|
1843
|
+
User: ${user.name} (@${user.username})`,
|
|
1844
|
+
boxStyles.success
|
|
1845
|
+
)
|
|
1846
|
+
);
|
|
1847
|
+
logger.log("");
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
const err = error;
|
|
1850
|
+
logger.log("");
|
|
1851
|
+
logger.log(
|
|
1852
|
+
boxen(
|
|
1853
|
+
theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
|
|
1854
|
+
boxStyles.error
|
|
1855
|
+
)
|
|
1856
|
+
);
|
|
1857
|
+
logger.log("");
|
|
1858
|
+
process.exit(1);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1790
1861
|
|
|
1791
1862
|
// src/commands/sync.ts
|
|
1792
1863
|
init_esm_shims();
|
|
@@ -1911,6 +1982,8 @@ var SourceDetector = class {
|
|
|
1911
1982
|
return true;
|
|
1912
1983
|
} else if (type === "bitbucket" || type === "atlassian") {
|
|
1913
1984
|
return await storageService.isServiceAuthenticated("bitbucket");
|
|
1985
|
+
} else if (type === "gitlab") {
|
|
1986
|
+
return await storageService.isServiceAuthenticated("gitlab");
|
|
1914
1987
|
}
|
|
1915
1988
|
return false;
|
|
1916
1989
|
} catch {
|
|
@@ -2690,6 +2763,299 @@ var BitbucketSyncAdapter = class {
|
|
|
2690
2763
|
};
|
|
2691
2764
|
var bitbucketSyncAdapter = new BitbucketSyncAdapter();
|
|
2692
2765
|
|
|
2766
|
+
// src/sync/gitlab-adapter.ts
|
|
2767
|
+
init_esm_shims();
|
|
2768
|
+
|
|
2769
|
+
// src/services/gitlab.service.ts
|
|
2770
|
+
init_esm_shims();
|
|
2771
|
+
init_errors();
|
|
2772
|
+
init_logger();
|
|
2773
|
+
init_storage_service();
|
|
2774
|
+
import { exec as exec5 } from "child_process";
|
|
2775
|
+
import { promisify as promisify5 } from "util";
|
|
2776
|
+
import { URLSearchParams as URLSearchParams3 } from "url";
|
|
2777
|
+
var execAsync5 = promisify5(exec5);
|
|
2778
|
+
var GitLabService = class {
|
|
2779
|
+
DEFAULT_INSTANCE = "https://gitlab.com";
|
|
2780
|
+
MAX_DESCRIPTION_LENGTH = 5e3;
|
|
2781
|
+
/**
|
|
2782
|
+
* Get stored GitLab credentials
|
|
2783
|
+
*/
|
|
2784
|
+
async getCredentials() {
|
|
2785
|
+
const creds = await storageService.getServiceCredentials("gitlab");
|
|
2786
|
+
if (!creds || !creds.accessToken) {
|
|
2787
|
+
throw new GitLabError("Not authenticated with GitLab", {
|
|
2788
|
+
hint: "Run: bragduck auth gitlab"
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
if (creds.expiresAt && creds.expiresAt < Date.now()) {
|
|
2792
|
+
throw new GitLabError("Access token has expired", {
|
|
2793
|
+
hint: "Run: bragduck auth gitlab"
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
return {
|
|
2797
|
+
accessToken: creds.accessToken,
|
|
2798
|
+
instanceUrl: creds.instanceUrl || this.DEFAULT_INSTANCE
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Make authenticated request to GitLab API
|
|
2803
|
+
*/
|
|
2804
|
+
async request(endpoint) {
|
|
2805
|
+
const { accessToken, instanceUrl } = await this.getCredentials();
|
|
2806
|
+
const baseUrl = `${instanceUrl}/api/v4`;
|
|
2807
|
+
const url = `${baseUrl}${endpoint}`;
|
|
2808
|
+
logger.debug(`GitLab API: GET ${endpoint}`);
|
|
2809
|
+
const response = await fetch(url, {
|
|
2810
|
+
headers: {
|
|
2811
|
+
"PRIVATE-TOKEN": accessToken,
|
|
2812
|
+
Accept: "application/json"
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
if (!response.ok) {
|
|
2816
|
+
const statusText = response.statusText;
|
|
2817
|
+
const status = response.status;
|
|
2818
|
+
if (status === 401) {
|
|
2819
|
+
throw new GitLabError("Invalid or expired access token", {
|
|
2820
|
+
hint: "Run: bragduck auth gitlab",
|
|
2821
|
+
originalError: statusText
|
|
2822
|
+
});
|
|
2823
|
+
} else if (status === 403) {
|
|
2824
|
+
throw new GitLabError("Insufficient token permissions", {
|
|
2825
|
+
hint: "Token requires scopes: api, read_api, read_user",
|
|
2826
|
+
originalError: statusText
|
|
2827
|
+
});
|
|
2828
|
+
} else if (status === 404) {
|
|
2829
|
+
throw new GitLabError("Resource not found", {
|
|
2830
|
+
hint: "Check repository URL and token permissions",
|
|
2831
|
+
originalError: statusText
|
|
2832
|
+
});
|
|
2833
|
+
} else if (status === 429) {
|
|
2834
|
+
throw new GitLabError("Rate limit exceeded", {
|
|
2835
|
+
hint: "Wait a moment and try again",
|
|
2836
|
+
originalError: statusText
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
throw new GitLabError(`API request failed: ${statusText}`, {
|
|
2840
|
+
originalError: statusText
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
return response.json();
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Extract namespace and project from git remote URL
|
|
2847
|
+
*/
|
|
2848
|
+
parseRemoteUrl(url) {
|
|
2849
|
+
const match = url.match(/[:/]([\w-]+)\/([\w-]+?)(?:\.git)?$/);
|
|
2850
|
+
if (match && match[1] && match[2]) {
|
|
2851
|
+
return {
|
|
2852
|
+
namespace: match[1],
|
|
2853
|
+
project: match[2]
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
return null;
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Get project path from git remote
|
|
2860
|
+
*/
|
|
2861
|
+
async getProjectFromGit() {
|
|
2862
|
+
try {
|
|
2863
|
+
const { stdout } = await execAsync5("git remote get-url origin");
|
|
2864
|
+
const remoteUrl = stdout.trim();
|
|
2865
|
+
const parsed = this.parseRemoteUrl(remoteUrl);
|
|
2866
|
+
if (!parsed) {
|
|
2867
|
+
throw new GitLabError("Could not parse GitLab project from remote URL", {
|
|
2868
|
+
url: remoteUrl
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
return {
|
|
2872
|
+
...parsed,
|
|
2873
|
+
projectPath: `${parsed.namespace}/${parsed.project}`
|
|
2874
|
+
};
|
|
2875
|
+
} catch (error) {
|
|
2876
|
+
if (error instanceof GitLabError) {
|
|
2877
|
+
throw error;
|
|
2878
|
+
}
|
|
2879
|
+
throw new GitError("Could not get git remote URL");
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Validate that this is a GitLab repository and credentials work
|
|
2884
|
+
*/
|
|
2885
|
+
async validateGitLabRepository() {
|
|
2886
|
+
await gitService.validateRepository();
|
|
2887
|
+
const { projectPath } = await this.getProjectFromGit();
|
|
2888
|
+
const encodedPath = encodeURIComponent(projectPath);
|
|
2889
|
+
try {
|
|
2890
|
+
await this.request(`/projects/${encodedPath}`);
|
|
2891
|
+
} catch (error) {
|
|
2892
|
+
if (error instanceof GitLabError) {
|
|
2893
|
+
throw error;
|
|
2894
|
+
}
|
|
2895
|
+
throw new GitLabError("Could not access GitLab project via API", {
|
|
2896
|
+
hint: "Check that this is a GitLab repository and your token is valid",
|
|
2897
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Get repository information
|
|
2903
|
+
*/
|
|
2904
|
+
async getRepositoryInfo() {
|
|
2905
|
+
const { projectPath } = await this.getProjectFromGit();
|
|
2906
|
+
const encodedPath = encodeURIComponent(projectPath);
|
|
2907
|
+
const project = await this.request(`/projects/${encodedPath}`);
|
|
2908
|
+
const [owner, name] = projectPath.split("/");
|
|
2909
|
+
return {
|
|
2910
|
+
owner: owner || "",
|
|
2911
|
+
name: name || "",
|
|
2912
|
+
fullName: project.path_with_namespace,
|
|
2913
|
+
url: project.web_url
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* Fetch merged MRs with pagination
|
|
2918
|
+
*/
|
|
2919
|
+
async getMergedMRs(options = {}) {
|
|
2920
|
+
const { projectPath } = await this.getProjectFromGit();
|
|
2921
|
+
const encodedPath = encodeURIComponent(projectPath);
|
|
2922
|
+
const params = new URLSearchParams3({
|
|
2923
|
+
state: "merged",
|
|
2924
|
+
order_by: "updated_at",
|
|
2925
|
+
sort: "desc",
|
|
2926
|
+
per_page: "100"
|
|
2927
|
+
});
|
|
2928
|
+
if (options.days) {
|
|
2929
|
+
const since = /* @__PURE__ */ new Date();
|
|
2930
|
+
since.setDate(since.getDate() - options.days);
|
|
2931
|
+
params.append("updated_after", since.toISOString());
|
|
2932
|
+
}
|
|
2933
|
+
if (options.author) {
|
|
2934
|
+
params.append("author_username", options.author);
|
|
2935
|
+
}
|
|
2936
|
+
const allMRs = [];
|
|
2937
|
+
let page = 1;
|
|
2938
|
+
while (true) {
|
|
2939
|
+
params.set("page", page.toString());
|
|
2940
|
+
const endpoint = `/projects/${encodedPath}/merge_requests?${params}`;
|
|
2941
|
+
const mrs = await this.request(endpoint);
|
|
2942
|
+
allMRs.push(...mrs);
|
|
2943
|
+
logger.debug(
|
|
2944
|
+
`Fetched ${mrs.length} MRs (total: ${allMRs.length})${mrs.length === 100 ? ", fetching next page..." : ""}`
|
|
2945
|
+
);
|
|
2946
|
+
if (options.limit && allMRs.length >= options.limit) {
|
|
2947
|
+
return allMRs.slice(0, options.limit);
|
|
2948
|
+
}
|
|
2949
|
+
if (mrs.length < 100) {
|
|
2950
|
+
break;
|
|
2951
|
+
}
|
|
2952
|
+
page++;
|
|
2953
|
+
}
|
|
2954
|
+
return allMRs;
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Get MRs by current user
|
|
2958
|
+
*/
|
|
2959
|
+
async getMRsByCurrentUser(options = {}) {
|
|
2960
|
+
const username = await this.getCurrentGitLabUser();
|
|
2961
|
+
if (!username) {
|
|
2962
|
+
throw new GitLabError("Could not get current user username");
|
|
2963
|
+
}
|
|
2964
|
+
return this.getMergedMRs({
|
|
2965
|
+
...options,
|
|
2966
|
+
author: username
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Get current authenticated user
|
|
2971
|
+
*/
|
|
2972
|
+
async getCurrentGitLabUser() {
|
|
2973
|
+
try {
|
|
2974
|
+
const user = await this.request("/user");
|
|
2975
|
+
return user.username;
|
|
2976
|
+
} catch {
|
|
2977
|
+
return null;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
/**
|
|
2981
|
+
* Transform GitLab MR to GitCommit format
|
|
2982
|
+
*/
|
|
2983
|
+
transformMRToCommit(mr) {
|
|
2984
|
+
const message = this.formatMessage(mr.title, mr.description);
|
|
2985
|
+
return {
|
|
2986
|
+
sha: `mr-${mr.iid}`,
|
|
2987
|
+
// Use iid (project-scoped)
|
|
2988
|
+
message,
|
|
2989
|
+
author: mr.author.username,
|
|
2990
|
+
authorEmail: "",
|
|
2991
|
+
// Not available in MR response
|
|
2992
|
+
date: mr.created_at,
|
|
2993
|
+
url: mr.web_url,
|
|
2994
|
+
diffStats: {
|
|
2995
|
+
filesChanged: mr.diff_stats?.file_count || 0,
|
|
2996
|
+
insertions: mr.diff_stats?.additions || 0,
|
|
2997
|
+
deletions: mr.diff_stats?.deletions || 0
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Format MR message with title and description
|
|
3003
|
+
*/
|
|
3004
|
+
formatMessage(title, description) {
|
|
3005
|
+
if (!description) return title;
|
|
3006
|
+
const truncated = description.substring(0, this.MAX_DESCRIPTION_LENGTH);
|
|
3007
|
+
return `${title}
|
|
3008
|
+
|
|
3009
|
+
${truncated}`;
|
|
3010
|
+
}
|
|
3011
|
+
};
|
|
3012
|
+
var gitlabService = new GitLabService();
|
|
3013
|
+
|
|
3014
|
+
// src/sync/gitlab-adapter.ts
|
|
3015
|
+
var GitLabSyncAdapter = class {
|
|
3016
|
+
name = "gitlab";
|
|
3017
|
+
async validate() {
|
|
3018
|
+
await gitlabService.validateGitLabRepository();
|
|
3019
|
+
}
|
|
3020
|
+
async getRepositoryInfo() {
|
|
3021
|
+
const info = await gitlabService.getRepositoryInfo();
|
|
3022
|
+
return {
|
|
3023
|
+
owner: info.owner,
|
|
3024
|
+
name: info.name,
|
|
3025
|
+
fullName: info.fullName,
|
|
3026
|
+
url: info.url
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
async fetchWorkItems(options) {
|
|
3030
|
+
let mrs;
|
|
3031
|
+
if (options.author) {
|
|
3032
|
+
mrs = await gitlabService.getMergedMRs({
|
|
3033
|
+
days: options.days,
|
|
3034
|
+
limit: options.limit,
|
|
3035
|
+
author: options.author
|
|
3036
|
+
});
|
|
3037
|
+
} else {
|
|
3038
|
+
mrs = await gitlabService.getMRsByCurrentUser({
|
|
3039
|
+
days: options.days,
|
|
3040
|
+
limit: options.limit
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
return mrs.map((mr) => gitlabService.transformMRToCommit(mr));
|
|
3044
|
+
}
|
|
3045
|
+
async isAuthenticated() {
|
|
3046
|
+
try {
|
|
3047
|
+
await gitlabService.validateGitLabRepository();
|
|
3048
|
+
return true;
|
|
3049
|
+
} catch {
|
|
3050
|
+
return false;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
async getCurrentUser() {
|
|
3054
|
+
return gitlabService.getCurrentGitLabUser();
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
var gitlabSyncAdapter = new GitLabSyncAdapter();
|
|
3058
|
+
|
|
2693
3059
|
// src/sync/adapter-factory.ts
|
|
2694
3060
|
var AdapterFactory = class {
|
|
2695
3061
|
/**
|
|
@@ -2704,7 +3070,7 @@ var AdapterFactory = class {
|
|
|
2704
3070
|
return bitbucketSyncAdapter;
|
|
2705
3071
|
// Bitbucket Cloud and Server use same adapter
|
|
2706
3072
|
case "gitlab":
|
|
2707
|
-
|
|
3073
|
+
return gitlabSyncAdapter;
|
|
2708
3074
|
default:
|
|
2709
3075
|
throw new Error(`Unknown source type: ${source}`);
|
|
2710
3076
|
}
|
|
@@ -2713,7 +3079,7 @@ var AdapterFactory = class {
|
|
|
2713
3079
|
* Check if adapter is available for source
|
|
2714
3080
|
*/
|
|
2715
3081
|
static isSupported(source) {
|
|
2716
|
-
return source === "github" || source === "bitbucket" || source === "atlassian";
|
|
3082
|
+
return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab";
|
|
2717
3083
|
}
|
|
2718
3084
|
};
|
|
2719
3085
|
|
|
@@ -4158,7 +4524,7 @@ var packageJsonPath = join7(__dirname6, "../../package.json");
|
|
|
4158
4524
|
var packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
|
|
4159
4525
|
var program = new Command();
|
|
4160
4526
|
program.name("bragduck").description("CLI tool for managing developer achievements and brags\nAliases: bd, duck, brag").version(packageJson.version, "-v, --version", "Display version number").helpOption("-h, --help", "Display help information").option("--skip-version-check", "Skip automatic version check on startup").option("--debug", "Enable debug mode (shows detailed logs)");
|
|
4161
|
-
program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status, bitbucket)").action(async (subcommand) => {
|
|
4527
|
+
program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status, bitbucket, gitlab)").action(async (subcommand) => {
|
|
4162
4528
|
try {
|
|
4163
4529
|
await authCommand(subcommand);
|
|
4164
4530
|
} catch (error) {
|