@bragduck/cli 2.24.2 → 2.27.2
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 +818 -128
- package/dist/bin/bragduck.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/bragduck.js
CHANGED
|
@@ -1689,13 +1689,13 @@ init_esm_shims();
|
|
|
1689
1689
|
init_errors();
|
|
1690
1690
|
import { existsSync as existsSync2 } from "fs";
|
|
1691
1691
|
import { join as join4 } from "path";
|
|
1692
|
-
function validateGitRepository(
|
|
1693
|
-
const gitDir = join4(
|
|
1692
|
+
function validateGitRepository(path4) {
|
|
1693
|
+
const gitDir = join4(path4, ".git");
|
|
1694
1694
|
if (!existsSync2(gitDir)) {
|
|
1695
1695
|
throw new GitError(
|
|
1696
1696
|
"Not a git repository. Please run this command from within a git repository.",
|
|
1697
1697
|
{
|
|
1698
|
-
path:
|
|
1698
|
+
path: path4,
|
|
1699
1699
|
hint: 'Run "git init" to initialize a git repository, or navigate to an existing one'
|
|
1700
1700
|
}
|
|
1701
1701
|
);
|
|
@@ -1912,11 +1912,11 @@ var GitHubService = class {
|
|
|
1912
1912
|
* Unsets GITHUB_TOKEN and GH_TOKEN to prevent .envrc tokens from interfering
|
|
1913
1913
|
* with gh CLI's own authentication
|
|
1914
1914
|
*/
|
|
1915
|
-
async execGhCommand(command) {
|
|
1915
|
+
async execGhCommand(command, cwd) {
|
|
1916
1916
|
const env = { ...process.env };
|
|
1917
1917
|
delete env.GITHUB_TOKEN;
|
|
1918
1918
|
delete env.GH_TOKEN;
|
|
1919
|
-
return execAsync2(command, { env });
|
|
1919
|
+
return execAsync2(command, { env, cwd: cwd || process.cwd() });
|
|
1920
1920
|
}
|
|
1921
1921
|
/**
|
|
1922
1922
|
* Check if GitHub CLI is installed and available
|
|
@@ -1977,12 +1977,14 @@ var GitHubService = class {
|
|
|
1977
1977
|
/**
|
|
1978
1978
|
* Validate that the current repository is hosted on GitHub
|
|
1979
1979
|
*/
|
|
1980
|
-
async validateGitHubRepository() {
|
|
1980
|
+
async validateGitHubRepository(repoPath) {
|
|
1981
1981
|
try {
|
|
1982
1982
|
await this.ensureGitHubCLI();
|
|
1983
1983
|
await this.ensureAuthentication();
|
|
1984
|
-
|
|
1985
|
-
|
|
1984
|
+
if (!repoPath) {
|
|
1985
|
+
await gitService.validateRepository();
|
|
1986
|
+
}
|
|
1987
|
+
const { stdout } = await this.execGhCommand("command gh repo view --json url", repoPath);
|
|
1986
1988
|
const data = JSON.parse(stdout);
|
|
1987
1989
|
if (!data.url) {
|
|
1988
1990
|
throw new GitHubError("This repository is not hosted on GitHub", {
|
|
@@ -2011,11 +2013,12 @@ var GitHubService = class {
|
|
|
2011
2013
|
/**
|
|
2012
2014
|
* Get GitHub repository information
|
|
2013
2015
|
*/
|
|
2014
|
-
async getRepositoryInfo() {
|
|
2016
|
+
async getRepositoryInfo(repoPath) {
|
|
2015
2017
|
try {
|
|
2016
2018
|
await this.ensureGitHubCLI();
|
|
2017
2019
|
const { stdout } = await this.execGhCommand(
|
|
2018
|
-
"command gh repo view --json owner,name,url,nameWithOwner"
|
|
2020
|
+
"command gh repo view --json owner,name,url,nameWithOwner",
|
|
2021
|
+
repoPath
|
|
2019
2022
|
);
|
|
2020
2023
|
const data = JSON.parse(stdout);
|
|
2021
2024
|
return {
|
|
@@ -2037,9 +2040,9 @@ var GitHubService = class {
|
|
|
2037
2040
|
/**
|
|
2038
2041
|
* Get the current authenticated GitHub user
|
|
2039
2042
|
*/
|
|
2040
|
-
async getCurrentGitHubUser() {
|
|
2043
|
+
async getCurrentGitHubUser(repoPath) {
|
|
2041
2044
|
try {
|
|
2042
|
-
const { stdout } = await this.execGhCommand("command gh api user --jq .login");
|
|
2045
|
+
const { stdout } = await this.execGhCommand("command gh api user --jq .login", repoPath);
|
|
2043
2046
|
return stdout.trim() || null;
|
|
2044
2047
|
} catch {
|
|
2045
2048
|
logger.debug("Failed to get GitHub user");
|
|
@@ -2049,7 +2052,7 @@ var GitHubService = class {
|
|
|
2049
2052
|
/**
|
|
2050
2053
|
* Fetch merged PRs from GitHub
|
|
2051
2054
|
*/
|
|
2052
|
-
async getMergedPRs(options = {}) {
|
|
2055
|
+
async getMergedPRs(options = {}, repoPath) {
|
|
2053
2056
|
const { days = 30, limit, author } = options;
|
|
2054
2057
|
try {
|
|
2055
2058
|
await this.ensureGitHubCLI();
|
|
@@ -2065,7 +2068,7 @@ var GitHubService = class {
|
|
|
2065
2068
|
const limitArg = limit ? `--limit ${limit}` : "";
|
|
2066
2069
|
const command = `command gh pr list --state merged --json ${this.PR_SEARCH_FIELDS} --search "${searchQuery}" ${limitArg}`;
|
|
2067
2070
|
logger.debug(`Running: ${command}`);
|
|
2068
|
-
const { stdout } = await this.execGhCommand(command);
|
|
2071
|
+
const { stdout } = await this.execGhCommand(command, repoPath);
|
|
2069
2072
|
const prs = JSON.parse(stdout);
|
|
2070
2073
|
logger.debug(`Found ${prs.length} merged PRs`);
|
|
2071
2074
|
return prs;
|
|
@@ -2082,17 +2085,20 @@ var GitHubService = class {
|
|
|
2082
2085
|
/**
|
|
2083
2086
|
* Get merged PRs by current user (PRs authored by the current user)
|
|
2084
2087
|
*/
|
|
2085
|
-
async getPRsByCurrentUser(options = {}) {
|
|
2086
|
-
const currentUser = await this.getCurrentGitHubUser();
|
|
2088
|
+
async getPRsByCurrentUser(options = {}, repoPath) {
|
|
2089
|
+
const currentUser = await this.getCurrentGitHubUser(repoPath);
|
|
2087
2090
|
if (!currentUser) {
|
|
2088
2091
|
logger.warning("Could not determine GitHub user, returning all PRs");
|
|
2089
|
-
return this.getMergedPRs(options);
|
|
2092
|
+
return this.getMergedPRs(options, repoPath);
|
|
2090
2093
|
}
|
|
2091
2094
|
logger.debug(`Filtering PRs by author: ${currentUser}`);
|
|
2092
|
-
return this.getMergedPRs(
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2095
|
+
return this.getMergedPRs(
|
|
2096
|
+
{
|
|
2097
|
+
...options,
|
|
2098
|
+
author: currentUser
|
|
2099
|
+
},
|
|
2100
|
+
repoPath
|
|
2101
|
+
);
|
|
2096
2102
|
}
|
|
2097
2103
|
/**
|
|
2098
2104
|
* Transform a GitHub PR into a GitCommit structure
|
|
@@ -2659,11 +2665,10 @@ var SourceDetector = class {
|
|
|
2659
2665
|
/**
|
|
2660
2666
|
* Detect all possible sources from git remotes
|
|
2661
2667
|
*/
|
|
2662
|
-
async detectSources(options = {}) {
|
|
2668
|
+
async detectSources(options = {}, repoPath) {
|
|
2663
2669
|
const detected = [];
|
|
2664
2670
|
try {
|
|
2665
|
-
const
|
|
2666
|
-
const remotes = this.parseRemotes(stdout);
|
|
2671
|
+
const remotes = await this.parseRemotes(repoPath);
|
|
2667
2672
|
for (const remote of remotes) {
|
|
2668
2673
|
const source = this.parseRemoteUrl(remote.url);
|
|
2669
2674
|
if (source) {
|
|
@@ -2728,7 +2733,16 @@ var SourceDetector = class {
|
|
|
2728
2733
|
/**
|
|
2729
2734
|
* Parse git remote -v output
|
|
2730
2735
|
*/
|
|
2731
|
-
parseRemotes(
|
|
2736
|
+
async parseRemotes(repoPath) {
|
|
2737
|
+
const { stdout } = await execAsync3("git remote -v", {
|
|
2738
|
+
cwd: repoPath || process.cwd()
|
|
2739
|
+
});
|
|
2740
|
+
return this.parseRemotesOutput(stdout);
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Parse git remote -v output string
|
|
2744
|
+
*/
|
|
2745
|
+
parseRemotesOutput(output) {
|
|
2732
2746
|
const lines = output.split("\n").filter(Boolean);
|
|
2733
2747
|
const remotes = /* @__PURE__ */ new Map();
|
|
2734
2748
|
for (const line of lines) {
|
|
@@ -2835,11 +2849,15 @@ init_esm_shims();
|
|
|
2835
2849
|
init_esm_shims();
|
|
2836
2850
|
var GitHubSyncAdapter = class {
|
|
2837
2851
|
name = "github";
|
|
2852
|
+
repoPath;
|
|
2853
|
+
constructor(repoPath) {
|
|
2854
|
+
this.repoPath = repoPath;
|
|
2855
|
+
}
|
|
2838
2856
|
async validate() {
|
|
2839
|
-
await githubService.validateGitHubRepository();
|
|
2857
|
+
await githubService.validateGitHubRepository(this.repoPath);
|
|
2840
2858
|
}
|
|
2841
2859
|
async getRepositoryInfo() {
|
|
2842
|
-
const info = await githubService.getRepositoryInfo();
|
|
2860
|
+
const info = await githubService.getRepositoryInfo(this.repoPath);
|
|
2843
2861
|
return {
|
|
2844
2862
|
owner: info.owner,
|
|
2845
2863
|
name: info.name,
|
|
@@ -2848,31 +2866,54 @@ var GitHubSyncAdapter = class {
|
|
|
2848
2866
|
};
|
|
2849
2867
|
}
|
|
2850
2868
|
async fetchWorkItems(options) {
|
|
2851
|
-
|
|
2852
|
-
if (
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2869
|
+
const scanMode = options.scanMode || "prs";
|
|
2870
|
+
if (scanMode === "commits") {
|
|
2871
|
+
const gitSvc = new GitService(this.repoPath || process.cwd());
|
|
2872
|
+
if (options.author) {
|
|
2873
|
+
return gitSvc.getCommitsWithStats({
|
|
2874
|
+
days: options.days,
|
|
2875
|
+
limit: options.limit,
|
|
2876
|
+
author: options.author
|
|
2877
|
+
});
|
|
2878
|
+
} else {
|
|
2879
|
+
return gitSvc.getCommitsByCurrentUser({
|
|
2880
|
+
days: options.days,
|
|
2881
|
+
limit: options.limit
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2858
2884
|
} else {
|
|
2859
|
-
prs
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2885
|
+
let prs;
|
|
2886
|
+
if (options.author) {
|
|
2887
|
+
prs = await githubService.getMergedPRs(
|
|
2888
|
+
{
|
|
2889
|
+
days: options.days,
|
|
2890
|
+
limit: options.limit,
|
|
2891
|
+
author: options.author
|
|
2892
|
+
},
|
|
2893
|
+
this.repoPath
|
|
2894
|
+
);
|
|
2895
|
+
} else {
|
|
2896
|
+
prs = await githubService.getPRsByCurrentUser(
|
|
2897
|
+
{
|
|
2898
|
+
days: options.days,
|
|
2899
|
+
limit: options.limit
|
|
2900
|
+
},
|
|
2901
|
+
this.repoPath
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
return prs.map((pr) => githubService.transformPRToCommit(pr));
|
|
2863
2905
|
}
|
|
2864
|
-
return prs.map((pr) => githubService.transformPRToCommit(pr));
|
|
2865
2906
|
}
|
|
2866
2907
|
async isAuthenticated() {
|
|
2867
2908
|
try {
|
|
2868
|
-
await githubService.validateGitHubRepository();
|
|
2909
|
+
await githubService.validateGitHubRepository(this.repoPath);
|
|
2869
2910
|
return true;
|
|
2870
2911
|
} catch {
|
|
2871
2912
|
return false;
|
|
2872
2913
|
}
|
|
2873
2914
|
}
|
|
2874
2915
|
async getCurrentUser() {
|
|
2875
|
-
return githubService.getCurrentGitHubUser();
|
|
2916
|
+
return githubService.getCurrentGitHubUser(this.repoPath);
|
|
2876
2917
|
}
|
|
2877
2918
|
};
|
|
2878
2919
|
var githubSyncAdapter = new GitHubSyncAdapter();
|
|
@@ -3117,6 +3158,10 @@ var bitbucketService = new BitbucketService();
|
|
|
3117
3158
|
// src/sync/bitbucket-adapter.ts
|
|
3118
3159
|
var BitbucketSyncAdapter = class {
|
|
3119
3160
|
name = "bitbucket";
|
|
3161
|
+
repoPath;
|
|
3162
|
+
constructor(repoPath) {
|
|
3163
|
+
this.repoPath = repoPath;
|
|
3164
|
+
}
|
|
3120
3165
|
async validate() {
|
|
3121
3166
|
await bitbucketService.validateBitbucketRepository();
|
|
3122
3167
|
}
|
|
@@ -3130,20 +3175,37 @@ var BitbucketSyncAdapter = class {
|
|
|
3130
3175
|
};
|
|
3131
3176
|
}
|
|
3132
3177
|
async fetchWorkItems(options) {
|
|
3133
|
-
|
|
3134
|
-
if (
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3178
|
+
const scanMode = options.scanMode || "prs";
|
|
3179
|
+
if (scanMode === "commits") {
|
|
3180
|
+
const gitSvc = new GitService(this.repoPath || process.cwd());
|
|
3181
|
+
if (options.author) {
|
|
3182
|
+
return gitSvc.getCommitsWithStats({
|
|
3183
|
+
days: options.days,
|
|
3184
|
+
limit: options.limit,
|
|
3185
|
+
author: options.author
|
|
3186
|
+
});
|
|
3187
|
+
} else {
|
|
3188
|
+
return gitSvc.getCommitsByCurrentUser({
|
|
3189
|
+
days: options.days,
|
|
3190
|
+
limit: options.limit
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3140
3193
|
} else {
|
|
3141
|
-
prs
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3194
|
+
let prs;
|
|
3195
|
+
if (options.author) {
|
|
3196
|
+
prs = await bitbucketService.getMergedPRs({
|
|
3197
|
+
days: options.days,
|
|
3198
|
+
limit: options.limit,
|
|
3199
|
+
author: options.author
|
|
3200
|
+
});
|
|
3201
|
+
} else {
|
|
3202
|
+
prs = await bitbucketService.getPRsByCurrentUser({
|
|
3203
|
+
days: options.days,
|
|
3204
|
+
limit: options.limit
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
return prs.map((pr) => bitbucketService.transformPRToCommit(pr));
|
|
3145
3208
|
}
|
|
3146
|
-
return prs.map((pr) => bitbucketService.transformPRToCommit(pr));
|
|
3147
3209
|
}
|
|
3148
3210
|
async isAuthenticated() {
|
|
3149
3211
|
try {
|
|
@@ -3157,7 +3219,7 @@ var BitbucketSyncAdapter = class {
|
|
|
3157
3219
|
return bitbucketService.getCurrentGitHubUser();
|
|
3158
3220
|
}
|
|
3159
3221
|
};
|
|
3160
|
-
var bitbucketSyncAdapter = new BitbucketSyncAdapter();
|
|
3222
|
+
var bitbucketSyncAdapter = new BitbucketSyncAdapter(void 0);
|
|
3161
3223
|
|
|
3162
3224
|
// src/sync/gitlab-adapter.ts
|
|
3163
3225
|
init_esm_shims();
|
|
@@ -3410,6 +3472,10 @@ var gitlabService = new GitLabService();
|
|
|
3410
3472
|
// src/sync/gitlab-adapter.ts
|
|
3411
3473
|
var GitLabSyncAdapter = class {
|
|
3412
3474
|
name = "gitlab";
|
|
3475
|
+
repoPath;
|
|
3476
|
+
constructor(repoPath) {
|
|
3477
|
+
this.repoPath = repoPath;
|
|
3478
|
+
}
|
|
3413
3479
|
async validate() {
|
|
3414
3480
|
await gitlabService.validateGitLabRepository();
|
|
3415
3481
|
}
|
|
@@ -3423,20 +3489,37 @@ var GitLabSyncAdapter = class {
|
|
|
3423
3489
|
};
|
|
3424
3490
|
}
|
|
3425
3491
|
async fetchWorkItems(options) {
|
|
3426
|
-
|
|
3427
|
-
if (
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3492
|
+
const scanMode = options.scanMode || "prs";
|
|
3493
|
+
if (scanMode === "commits") {
|
|
3494
|
+
const gitSvc = new GitService(this.repoPath || process.cwd());
|
|
3495
|
+
if (options.author) {
|
|
3496
|
+
return gitSvc.getCommitsWithStats({
|
|
3497
|
+
days: options.days,
|
|
3498
|
+
limit: options.limit,
|
|
3499
|
+
author: options.author
|
|
3500
|
+
});
|
|
3501
|
+
} else {
|
|
3502
|
+
return gitSvc.getCommitsByCurrentUser({
|
|
3503
|
+
days: options.days,
|
|
3504
|
+
limit: options.limit
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3433
3507
|
} else {
|
|
3434
|
-
mrs
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3508
|
+
let mrs;
|
|
3509
|
+
if (options.author) {
|
|
3510
|
+
mrs = await gitlabService.getMergedMRs({
|
|
3511
|
+
days: options.days,
|
|
3512
|
+
limit: options.limit,
|
|
3513
|
+
author: options.author
|
|
3514
|
+
});
|
|
3515
|
+
} else {
|
|
3516
|
+
mrs = await gitlabService.getMRsByCurrentUser({
|
|
3517
|
+
days: options.days,
|
|
3518
|
+
limit: options.limit
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
return mrs.map((mr) => gitlabService.transformMRToCommit(mr));
|
|
3438
3522
|
}
|
|
3439
|
-
return mrs.map((mr) => gitlabService.transformMRToCommit(mr));
|
|
3440
3523
|
}
|
|
3441
3524
|
async isAuthenticated() {
|
|
3442
3525
|
try {
|
|
@@ -3450,7 +3533,7 @@ var GitLabSyncAdapter = class {
|
|
|
3450
3533
|
return gitlabService.getCurrentGitLabUser();
|
|
3451
3534
|
}
|
|
3452
3535
|
};
|
|
3453
|
-
var gitlabSyncAdapter = new GitLabSyncAdapter();
|
|
3536
|
+
var gitlabSyncAdapter = new GitLabSyncAdapter(void 0);
|
|
3454
3537
|
|
|
3455
3538
|
// src/sync/jira-adapter.ts
|
|
3456
3539
|
init_esm_shims();
|
|
@@ -3570,6 +3653,32 @@ var JiraService = class {
|
|
|
3570
3653
|
return null;
|
|
3571
3654
|
}
|
|
3572
3655
|
}
|
|
3656
|
+
/**
|
|
3657
|
+
* Check if a user object matches the given identifier (accountId, email, or username)
|
|
3658
|
+
*/
|
|
3659
|
+
isMatchingUser(candidate, userIdentifier) {
|
|
3660
|
+
return candidate.email === userIdentifier || candidate.emailAddress === userIdentifier || candidate.accountId === userIdentifier || candidate.username === userIdentifier || candidate.name === userIdentifier;
|
|
3661
|
+
}
|
|
3662
|
+
/**
|
|
3663
|
+
* Filter issues to only those where the user made contributions within the date range.
|
|
3664
|
+
* Excludes issues where the user's only involvement is a static role (creator/assignee)
|
|
3665
|
+
* with a date outside the scan period.
|
|
3666
|
+
*/
|
|
3667
|
+
filterIssuesByUserContribution(issues, userIdentifier, sinceDate) {
|
|
3668
|
+
const results = [];
|
|
3669
|
+
for (const issue of issues) {
|
|
3670
|
+
const userChanges = (issue.changelog?.histories || []).filter(
|
|
3671
|
+
(h) => this.isMatchingUser(h.author, userIdentifier) && new Date(h.created) >= sinceDate
|
|
3672
|
+
);
|
|
3673
|
+
const isCreatorInRange = this.isMatchingUser(issue.fields.creator, userIdentifier) && new Date(issue.fields.created) >= sinceDate;
|
|
3674
|
+
if (userChanges.length > 0 || isCreatorInRange) {
|
|
3675
|
+
results.push({ issue, userChanges });
|
|
3676
|
+
} else {
|
|
3677
|
+
logger.debug(`Excluding issue ${issue.key} - no user contributions in date range`);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
return results;
|
|
3681
|
+
}
|
|
3573
3682
|
/**
|
|
3574
3683
|
* Build JQL query from options
|
|
3575
3684
|
*/
|
|
@@ -3699,7 +3808,7 @@ var JiraService = class {
|
|
|
3699
3808
|
);
|
|
3700
3809
|
break;
|
|
3701
3810
|
}
|
|
3702
|
-
const endpoint = `/rest/api/3/search/jql?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=${fields.join(",")}`;
|
|
3811
|
+
const endpoint = `/rest/api/3/search/jql?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=${fields.join(",")}&expand=changelog`;
|
|
3703
3812
|
try {
|
|
3704
3813
|
const response = await this.request(endpoint);
|
|
3705
3814
|
if (response.issues.length === 0) {
|
|
@@ -3725,14 +3834,7 @@ var JiraService = class {
|
|
|
3725
3834
|
break;
|
|
3726
3835
|
}
|
|
3727
3836
|
if (options.limit && allIssues.length >= options.limit) {
|
|
3728
|
-
|
|
3729
|
-
const limitedIssues = allIssues.slice(0, options.limit);
|
|
3730
|
-
const commits2 = [];
|
|
3731
|
-
for (const issue of limitedIssues) {
|
|
3732
|
-
const commit = await this.transformIssueToCommit(issue, void 0, email2 || void 0);
|
|
3733
|
-
commits2.push(commit);
|
|
3734
|
-
}
|
|
3735
|
-
return commits2;
|
|
3837
|
+
break;
|
|
3736
3838
|
}
|
|
3737
3839
|
startAt += maxResults;
|
|
3738
3840
|
} catch (error) {
|
|
@@ -3756,9 +3858,23 @@ var JiraService = class {
|
|
|
3756
3858
|
throw error;
|
|
3757
3859
|
}
|
|
3758
3860
|
}
|
|
3861
|
+
const issuesToProcess = options.limit ? allIssues.slice(0, options.limit) : allIssues;
|
|
3759
3862
|
const email = await this.getCurrentUser();
|
|
3863
|
+
const sinceDate = options.days ? new Date(Date.now() - options.days * 24 * 60 * 60 * 1e3) : void 0;
|
|
3864
|
+
if (sinceDate && email) {
|
|
3865
|
+
const filtered = this.filterIssuesByUserContribution(issuesToProcess, email, sinceDate);
|
|
3866
|
+
logger.debug(
|
|
3867
|
+
`Date-scoped filtering: ${issuesToProcess.length} issues -> ${filtered.length} with user contributions in range`
|
|
3868
|
+
);
|
|
3869
|
+
const commits2 = [];
|
|
3870
|
+
for (const { issue, userChanges } of filtered) {
|
|
3871
|
+
const commit = await this.transformIssueToCommit(issue, void 0, email, userChanges);
|
|
3872
|
+
commits2.push(commit);
|
|
3873
|
+
}
|
|
3874
|
+
return commits2;
|
|
3875
|
+
}
|
|
3760
3876
|
const commits = [];
|
|
3761
|
-
for (const issue of
|
|
3877
|
+
for (const issue of issuesToProcess) {
|
|
3762
3878
|
const commit = await this.transformIssueToCommit(issue, void 0, email || void 0);
|
|
3763
3879
|
commits.push(commit);
|
|
3764
3880
|
}
|
|
@@ -3780,7 +3896,7 @@ var JiraService = class {
|
|
|
3780
3896
|
/**
|
|
3781
3897
|
* Transform Jira issue to GitCommit format with contribution-specific data
|
|
3782
3898
|
*/
|
|
3783
|
-
async transformIssueToCommit(issue, instanceUrl, userEmail) {
|
|
3899
|
+
async transformIssueToCommit(issue, instanceUrl, userEmail, userChanges) {
|
|
3784
3900
|
let contribution = null;
|
|
3785
3901
|
if (userEmail) {
|
|
3786
3902
|
contribution = await this.determineJiraContributionType(issue, userEmail);
|
|
@@ -3822,6 +3938,8 @@ ${truncatedDesc}`;
|
|
|
3822
3938
|
date = issue.fields.created;
|
|
3823
3939
|
} else if (contribution?.type === "assigned-resolved" && issue.fields.resolutiondate) {
|
|
3824
3940
|
date = issue.fields.resolutiondate;
|
|
3941
|
+
} else if (userChanges && userChanges.length > 0) {
|
|
3942
|
+
date = userChanges[userChanges.length - 1].created;
|
|
3825
3943
|
} else {
|
|
3826
3944
|
date = issue.fields.updated;
|
|
3827
3945
|
}
|
|
@@ -4011,6 +4129,59 @@ var ConfluenceService = class {
|
|
|
4011
4129
|
return null;
|
|
4012
4130
|
}
|
|
4013
4131
|
}
|
|
4132
|
+
/**
|
|
4133
|
+
* Check if a user object matches the given identifier (accountId, email, or username)
|
|
4134
|
+
*/
|
|
4135
|
+
isMatchingUser(candidate, userIdentifier) {
|
|
4136
|
+
return candidate.email === userIdentifier || candidate.emailAddress === userIdentifier || candidate.accountId === userIdentifier || candidate.username === userIdentifier || candidate.name === userIdentifier;
|
|
4137
|
+
}
|
|
4138
|
+
/**
|
|
4139
|
+
* Fetch full version history for a page
|
|
4140
|
+
*/
|
|
4141
|
+
async getPageVersionHistory(pageId) {
|
|
4142
|
+
const allVersions = [];
|
|
4143
|
+
let start = 0;
|
|
4144
|
+
const limit = 50;
|
|
4145
|
+
while (true) {
|
|
4146
|
+
const response = await this.request(
|
|
4147
|
+
`/wiki/rest/api/content/${pageId}/version?start=${start}&limit=${limit}`
|
|
4148
|
+
);
|
|
4149
|
+
allVersions.push(...response.results);
|
|
4150
|
+
if (response.size < limit) break;
|
|
4151
|
+
start += limit;
|
|
4152
|
+
}
|
|
4153
|
+
return allVersions;
|
|
4154
|
+
}
|
|
4155
|
+
/**
|
|
4156
|
+
* Filter pages to only those where the user made contributions within the date range.
|
|
4157
|
+
* Fetches version history per page and checks if the user has versions in range.
|
|
4158
|
+
*/
|
|
4159
|
+
async filterPagesByUserContribution(pages, userIdentifier, sinceDate) {
|
|
4160
|
+
const results = [];
|
|
4161
|
+
for (let i = 0; i < pages.length; i++) {
|
|
4162
|
+
const page = pages[i];
|
|
4163
|
+
if (i > 0) {
|
|
4164
|
+
await new Promise((resolve) => globalThis.setTimeout(resolve, 100));
|
|
4165
|
+
}
|
|
4166
|
+
try {
|
|
4167
|
+
const versions = await this.getPageVersionHistory(page.id);
|
|
4168
|
+
const userVersions = versions.filter(
|
|
4169
|
+
(v) => this.isMatchingUser(v.by, userIdentifier) && new Date(v.when) >= sinceDate
|
|
4170
|
+
);
|
|
4171
|
+
const userCommentsInRange = page.children?.comment?.results?.filter(
|
|
4172
|
+
(comment) => this.isMatchingUser(comment.version?.by || {}, userIdentifier) && new Date(comment.version?.when || 0) >= sinceDate
|
|
4173
|
+
) || [];
|
|
4174
|
+
if (userVersions.length > 0 || userCommentsInRange.length > 0) {
|
|
4175
|
+
results.push({ page, userVersions });
|
|
4176
|
+
} else {
|
|
4177
|
+
logger.debug(`Excluding page "${page.title}" - no user contributions in date range`);
|
|
4178
|
+
}
|
|
4179
|
+
} catch (error) {
|
|
4180
|
+
logger.debug(`Skipping version history for page ${page.id}: ${error}`);
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
return results;
|
|
4184
|
+
}
|
|
4014
4185
|
/**
|
|
4015
4186
|
* Build CQL query from options
|
|
4016
4187
|
* Returns empty string if no filters need CQL (will use simple endpoint instead)
|
|
@@ -4046,28 +4217,31 @@ var ConfluenceService = class {
|
|
|
4046
4217
|
* Determine the type of contribution the current user made to a page
|
|
4047
4218
|
* Returns: 'created' | 'edited' | 'commented'
|
|
4048
4219
|
*/
|
|
4049
|
-
async determineContributionType(page, userEmail) {
|
|
4050
|
-
|
|
4220
|
+
async determineContributionType(page, userEmail, userVersions) {
|
|
4221
|
+
const createdByUser = page.history?.createdBy ? this.isMatchingUser(page.history.createdBy, userEmail) : false;
|
|
4222
|
+
if (createdByUser) {
|
|
4051
4223
|
return {
|
|
4052
4224
|
type: "created",
|
|
4053
4225
|
details: `Created page with ${page.version?.number || 1} version${(page.version?.number || 1) > 1 ? "s" : ""}`
|
|
4054
4226
|
};
|
|
4055
4227
|
}
|
|
4056
|
-
const hasEdits = page.version?.by
|
|
4228
|
+
const hasEdits = userVersions ? userVersions.some((v) => !v.minorEdit || v.number > 1) : page.version?.by ? this.isMatchingUser(page.version.by, userEmail) && (page.version?.number || 0) > 1 : false;
|
|
4057
4229
|
const userComments = page.children?.comment?.results?.filter(
|
|
4058
|
-
(comment) => comment.version?.by
|
|
4230
|
+
(comment) => comment.version?.by ? this.isMatchingUser(comment.version.by, userEmail) : false
|
|
4059
4231
|
) || [];
|
|
4060
4232
|
const hasComments = userComments.length > 0;
|
|
4061
4233
|
if (hasEdits && hasComments) {
|
|
4234
|
+
const editCount = userVersions?.length || 1;
|
|
4062
4235
|
return {
|
|
4063
4236
|
type: "edited",
|
|
4064
|
-
details: `Edited page (
|
|
4237
|
+
details: `Edited page (${editCount} edit${editCount > 1 ? "s" : ""}) and added ${userComments.length} comment${userComments.length > 1 ? "s" : ""}`
|
|
4065
4238
|
};
|
|
4066
4239
|
}
|
|
4067
4240
|
if (hasEdits) {
|
|
4241
|
+
const editCount = userVersions?.length || 1;
|
|
4068
4242
|
return {
|
|
4069
4243
|
type: "edited",
|
|
4070
|
-
details: `Edited page
|
|
4244
|
+
details: `Edited page (${editCount} edit${editCount > 1 ? "s" : ""})`
|
|
4071
4245
|
};
|
|
4072
4246
|
}
|
|
4073
4247
|
if (hasComments) {
|
|
@@ -4165,14 +4339,7 @@ var ConfluenceService = class {
|
|
|
4165
4339
|
break;
|
|
4166
4340
|
}
|
|
4167
4341
|
if (options.limit && allPages.length >= options.limit) {
|
|
4168
|
-
|
|
4169
|
-
const limitedPages = allPages.slice(0, options.limit);
|
|
4170
|
-
const commits2 = [];
|
|
4171
|
-
for (const page of limitedPages) {
|
|
4172
|
-
const commit = await this.transformPageToCommit(page, void 0, email2 || void 0);
|
|
4173
|
-
commits2.push(commit);
|
|
4174
|
-
}
|
|
4175
|
-
return commits2;
|
|
4342
|
+
break;
|
|
4176
4343
|
}
|
|
4177
4344
|
start += limit;
|
|
4178
4345
|
} catch (error) {
|
|
@@ -4196,9 +4363,23 @@ var ConfluenceService = class {
|
|
|
4196
4363
|
throw error;
|
|
4197
4364
|
}
|
|
4198
4365
|
}
|
|
4366
|
+
const pagesToProcess = options.limit ? allPages.slice(0, options.limit) : allPages;
|
|
4199
4367
|
const email = await this.getCurrentUser();
|
|
4368
|
+
const sinceDate = options.days ? new Date(Date.now() - options.days * 24 * 60 * 60 * 1e3) : void 0;
|
|
4369
|
+
if (sinceDate && email) {
|
|
4370
|
+
const filtered = await this.filterPagesByUserContribution(pagesToProcess, email, sinceDate);
|
|
4371
|
+
logger.debug(
|
|
4372
|
+
`Date-scoped filtering: ${pagesToProcess.length} pages -> ${filtered.length} with user contributions in range`
|
|
4373
|
+
);
|
|
4374
|
+
const commits2 = [];
|
|
4375
|
+
for (const { page, userVersions } of filtered) {
|
|
4376
|
+
const commit = await this.transformPageToCommit(page, void 0, email, userVersions);
|
|
4377
|
+
commits2.push(commit);
|
|
4378
|
+
}
|
|
4379
|
+
return commits2;
|
|
4380
|
+
}
|
|
4200
4381
|
const commits = [];
|
|
4201
|
-
for (const page of
|
|
4382
|
+
for (const page of pagesToProcess) {
|
|
4202
4383
|
const commit = await this.transformPageToCommit(page, void 0, email || void 0);
|
|
4203
4384
|
commits.push(commit);
|
|
4204
4385
|
}
|
|
@@ -4220,10 +4401,10 @@ var ConfluenceService = class {
|
|
|
4220
4401
|
/**
|
|
4221
4402
|
* Transform Confluence page to GitCommit format with contribution-specific data
|
|
4222
4403
|
*/
|
|
4223
|
-
async transformPageToCommit(page, instanceUrl, userEmail) {
|
|
4404
|
+
async transformPageToCommit(page, instanceUrl, userEmail, userVersions) {
|
|
4224
4405
|
let contribution = null;
|
|
4225
4406
|
if (userEmail) {
|
|
4226
|
-
contribution = await this.determineContributionType(page, userEmail);
|
|
4407
|
+
contribution = await this.determineContributionType(page, userEmail, userVersions);
|
|
4227
4408
|
}
|
|
4228
4409
|
let message;
|
|
4229
4410
|
let contributionPrefix = "";
|
|
@@ -4261,7 +4442,14 @@ ${contribution.details}
|
|
|
4261
4442
|
const impactScore = contribution ? this.calculateContributionImpact(contribution.type, baseSize) : baseSize;
|
|
4262
4443
|
const author = page.history?.createdBy?.displayName || page.version?.by?.displayName || "Unknown Author";
|
|
4263
4444
|
const authorEmail = page.history?.createdBy?.email || page.version?.by?.email || "unknown@example.com";
|
|
4264
|
-
|
|
4445
|
+
let date;
|
|
4446
|
+
if (contribution?.type === "created") {
|
|
4447
|
+
date = page.history?.createdDate || page.version?.when;
|
|
4448
|
+
} else if (userVersions && userVersions.length > 0) {
|
|
4449
|
+
date = userVersions[userVersions.length - 1].when;
|
|
4450
|
+
} else {
|
|
4451
|
+
date = page.version?.when || (/* @__PURE__ */ new Date()).toISOString();
|
|
4452
|
+
}
|
|
4265
4453
|
return {
|
|
4266
4454
|
sha: page.id,
|
|
4267
4455
|
message,
|
|
@@ -4327,16 +4515,15 @@ var AdapterFactory = class {
|
|
|
4327
4515
|
/**
|
|
4328
4516
|
* Get adapter for a specific source type
|
|
4329
4517
|
*/
|
|
4330
|
-
static getAdapter(source) {
|
|
4518
|
+
static getAdapter(source, repoPath) {
|
|
4331
4519
|
switch (source) {
|
|
4332
4520
|
case "github":
|
|
4333
|
-
return githubSyncAdapter;
|
|
4521
|
+
return repoPath ? new GitHubSyncAdapter(repoPath) : githubSyncAdapter;
|
|
4334
4522
|
case "bitbucket":
|
|
4335
4523
|
case "atlassian":
|
|
4336
|
-
return bitbucketSyncAdapter;
|
|
4337
|
-
// Bitbucket Cloud and Server use same adapter
|
|
4524
|
+
return repoPath ? new BitbucketSyncAdapter(repoPath) : bitbucketSyncAdapter;
|
|
4338
4525
|
case "gitlab":
|
|
4339
|
-
return gitlabSyncAdapter;
|
|
4526
|
+
return repoPath ? new GitLabSyncAdapter(repoPath) : gitlabSyncAdapter;
|
|
4340
4527
|
case "jira":
|
|
4341
4528
|
return jiraSyncAdapter;
|
|
4342
4529
|
case "confluence":
|
|
@@ -4641,6 +4828,35 @@ function formatErrorMessage(message, hint) {
|
|
|
4641
4828
|
|
|
4642
4829
|
${error}${hintText}`;
|
|
4643
4830
|
}
|
|
4831
|
+
function formatMultiRepoSummary(result) {
|
|
4832
|
+
const emoji = "\u{1F389}";
|
|
4833
|
+
const title = colors.successBold(
|
|
4834
|
+
`${emoji} Successfully synced ${result.results.length} repositor${result.results.length > 1 ? "ies" : "y"}!`
|
|
4835
|
+
);
|
|
4836
|
+
let repoSummaries = "";
|
|
4837
|
+
for (const repoResult of result.results) {
|
|
4838
|
+
const repoHeader = `
|
|
4839
|
+
|
|
4840
|
+
${colors.highlight(repoResult.repoName)} (${colors.primary(repoResult.service)}): ${theme.count(repoResult.created)} brag${repoResult.created !== 1 ? "s" : ""} created`;
|
|
4841
|
+
let bragsList = "";
|
|
4842
|
+
if (repoResult.createdBrags && repoResult.createdBrags.length > 0) {
|
|
4843
|
+
for (const brag of repoResult.createdBrags) {
|
|
4844
|
+
const dateFormatted = formatDate(brag.date);
|
|
4845
|
+
bragsList += `
|
|
4846
|
+
${colors.dim("\u2022")} ${colors.white(brag.title)} ${colors.dim(`(${dateFormatted})`)}`;
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
repoSummaries += repoHeader + bragsList;
|
|
4850
|
+
}
|
|
4851
|
+
const total = `
|
|
4852
|
+
|
|
4853
|
+
${theme.secondary("Total: ")}${theme.count(result.totalCreated)} brag${result.totalCreated !== 1 ? "s" : ""} created across ${result.results.length} repositor${result.results.length > 1 ? "ies" : "y"}`;
|
|
4854
|
+
const hint = theme.secondary("\n\nRun ") + theme.command("bragduck list") + theme.secondary(" to see all your brags");
|
|
4855
|
+
const url = "https://bragduck.com/app/brags";
|
|
4856
|
+
const clickableUrl = terminalLink(url, url, { fallback: () => colors.primary(url) });
|
|
4857
|
+
const webUrl = theme.secondary("\n\nOr, check ") + clickableUrl + theme.secondary(" to see all your brags");
|
|
4858
|
+
return `${title}${repoSummaries}${total}${hint}${webUrl}`;
|
|
4859
|
+
}
|
|
4644
4860
|
|
|
4645
4861
|
// src/ui/prompts.ts
|
|
4646
4862
|
async function promptSelectCommits(commits) {
|
|
@@ -4696,6 +4912,25 @@ async function promptDaysToScan(defaultDays = 30) {
|
|
|
4696
4912
|
}
|
|
4697
4913
|
return parseInt(selected, 10);
|
|
4698
4914
|
}
|
|
4915
|
+
async function promptScanMode() {
|
|
4916
|
+
const choices = [
|
|
4917
|
+
{
|
|
4918
|
+
name: "Pull Requests / Merge Requests (Recommended)",
|
|
4919
|
+
value: "prs",
|
|
4920
|
+
description: "Scan merged PRs/MRs"
|
|
4921
|
+
},
|
|
4922
|
+
{
|
|
4923
|
+
name: "Direct Commits",
|
|
4924
|
+
value: "commits",
|
|
4925
|
+
description: "Scan git commits directly"
|
|
4926
|
+
}
|
|
4927
|
+
];
|
|
4928
|
+
return await select2({
|
|
4929
|
+
message: "What would you like to scan?",
|
|
4930
|
+
choices,
|
|
4931
|
+
default: "prs"
|
|
4932
|
+
});
|
|
4933
|
+
}
|
|
4699
4934
|
async function promptSortOption() {
|
|
4700
4935
|
const choices = [
|
|
4701
4936
|
{
|
|
@@ -4928,6 +5163,441 @@ function failStepSpinner(spinner, currentStep, totalSteps, text) {
|
|
|
4928
5163
|
spinner.fail(`${stepIndicator} ${colors.error(text)}`);
|
|
4929
5164
|
}
|
|
4930
5165
|
|
|
5166
|
+
// src/utils/repo-scanner.ts
|
|
5167
|
+
init_esm_shims();
|
|
5168
|
+
import { promises as fs2 } from "fs";
|
|
5169
|
+
import path3 from "path";
|
|
5170
|
+
import { exec as exec6 } from "child_process";
|
|
5171
|
+
import { promisify as promisify6 } from "util";
|
|
5172
|
+
init_logger();
|
|
5173
|
+
var execAsync6 = promisify6(exec6);
|
|
5174
|
+
async function isGitRepository(dirPath) {
|
|
5175
|
+
try {
|
|
5176
|
+
const gitPath = path3.join(dirPath, ".git");
|
|
5177
|
+
await fs2.access(gitPath);
|
|
5178
|
+
return true;
|
|
5179
|
+
} catch {
|
|
5180
|
+
return false;
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
async function scanSubdirectories(basePath) {
|
|
5184
|
+
try {
|
|
5185
|
+
const entries = await fs2.readdir(basePath, { withFileTypes: true });
|
|
5186
|
+
return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => path3.join(basePath, entry.name));
|
|
5187
|
+
} catch (error) {
|
|
5188
|
+
logger.debug(`Error scanning subdirectories: ${error}`);
|
|
5189
|
+
return [];
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
async function getRemoteUrl(repoPath) {
|
|
5193
|
+
try {
|
|
5194
|
+
const { stdout } = await execAsync6("command git remote -v", { cwd: repoPath });
|
|
5195
|
+
const remotes = stdout.split("\n").filter(Boolean);
|
|
5196
|
+
if (remotes.length === 0) {
|
|
5197
|
+
return null;
|
|
5198
|
+
}
|
|
5199
|
+
const match = remotes[0]?.match(/^\S+\s+(\S+)\s+\(fetch\)$/);
|
|
5200
|
+
return match && match[1] ? match[1] : null;
|
|
5201
|
+
} catch {
|
|
5202
|
+
return null;
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
async function discoverRepositories() {
|
|
5206
|
+
const repositories = [];
|
|
5207
|
+
const currentDir = process.cwd();
|
|
5208
|
+
logger.debug(`Scanning for repositories in: ${currentDir}`);
|
|
5209
|
+
if (await isGitRepository(currentDir)) {
|
|
5210
|
+
logger.debug(`Current directory is a git repository`);
|
|
5211
|
+
try {
|
|
5212
|
+
const result = await sourceDetector.detectSources({}, currentDir);
|
|
5213
|
+
const remoteUrl = await getRemoteUrl(currentDir);
|
|
5214
|
+
if (result.detected.length > 0 && result.recommended && remoteUrl) {
|
|
5215
|
+
const source = result.detected.find((s) => s.type === result.recommended);
|
|
5216
|
+
if (source) {
|
|
5217
|
+
repositories.push({
|
|
5218
|
+
path: currentDir,
|
|
5219
|
+
name: path3.basename(currentDir),
|
|
5220
|
+
service: source.type,
|
|
5221
|
+
remoteUrl
|
|
5222
|
+
});
|
|
5223
|
+
logger.debug(`Added repository: ${path3.basename(currentDir)} (${source.type})`);
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
} catch (error) {
|
|
5227
|
+
logger.debug(`Error detecting sources for current directory: ${error}`);
|
|
5228
|
+
}
|
|
5229
|
+
} else {
|
|
5230
|
+
logger.info("No repository found in current directory.");
|
|
5231
|
+
logger.log("");
|
|
5232
|
+
const subdirs = await scanSubdirectories(currentDir);
|
|
5233
|
+
logger.info(
|
|
5234
|
+
`Scanning ${subdirs.length} director${subdirs.length === 1 ? "y" : "ies"} for repositories...`
|
|
5235
|
+
);
|
|
5236
|
+
logger.log("");
|
|
5237
|
+
logger.debug(`Subdirectories to scan: ${subdirs.map((d) => path3.basename(d)).join(", ")}`);
|
|
5238
|
+
for (const subdir of subdirs) {
|
|
5239
|
+
if (await isGitRepository(subdir)) {
|
|
5240
|
+
try {
|
|
5241
|
+
const result = await sourceDetector.detectSources({ allowInteractive: false }, subdir);
|
|
5242
|
+
const remoteUrl = await getRemoteUrl(subdir);
|
|
5243
|
+
if (result.detected.length > 0 && result.recommended && remoteUrl) {
|
|
5244
|
+
const source = result.detected.find((s) => s.type === result.recommended);
|
|
5245
|
+
if (source) {
|
|
5246
|
+
repositories.push({
|
|
5247
|
+
path: subdir,
|
|
5248
|
+
name: path3.basename(subdir),
|
|
5249
|
+
service: source.type,
|
|
5250
|
+
remoteUrl
|
|
5251
|
+
});
|
|
5252
|
+
logger.debug(`Added repository: ${path3.basename(subdir)} (${source.type})`);
|
|
5253
|
+
}
|
|
5254
|
+
}
|
|
5255
|
+
} catch (error) {
|
|
5256
|
+
logger.debug(`Error detecting sources for ${subdir}: ${error}`);
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
logger.debug(`Found ${repositories.length} repositories`);
|
|
5262
|
+
return repositories;
|
|
5263
|
+
}
|
|
5264
|
+
|
|
5265
|
+
// src/sync/multi-repo-sync.ts
|
|
5266
|
+
init_esm_shims();
|
|
5267
|
+
init_api_service();
|
|
5268
|
+
init_auth_service();
|
|
5269
|
+
init_storage_service();
|
|
5270
|
+
init_logger();
|
|
5271
|
+
async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, options) {
|
|
5272
|
+
const TOTAL_STEPS = 5;
|
|
5273
|
+
logger.debug(`Syncing repository: ${repo.name} at ${repo.path}`);
|
|
5274
|
+
const adapter = AdapterFactory.getAdapter(repo.service, repo.path);
|
|
5275
|
+
const repoSpinner = createStepSpinner(1, TOTAL_STEPS, "Validating repository");
|
|
5276
|
+
repoSpinner.start();
|
|
5277
|
+
const VALIDATION_TIMEOUT = 3e4;
|
|
5278
|
+
let repoInfo;
|
|
5279
|
+
try {
|
|
5280
|
+
repoInfo = await Promise.race([
|
|
5281
|
+
(async () => {
|
|
5282
|
+
await adapter.validate();
|
|
5283
|
+
return await adapter.getRepositoryInfo();
|
|
5284
|
+
})(),
|
|
5285
|
+
new Promise((_, reject) => {
|
|
5286
|
+
setTimeout(
|
|
5287
|
+
() => reject(new Error("Validation timeout after 30 seconds")),
|
|
5288
|
+
VALIDATION_TIMEOUT
|
|
5289
|
+
);
|
|
5290
|
+
})
|
|
5291
|
+
]);
|
|
5292
|
+
succeedStepSpinner(
|
|
5293
|
+
repoSpinner,
|
|
5294
|
+
1,
|
|
5295
|
+
TOTAL_STEPS,
|
|
5296
|
+
`Repository: ${theme.value(repoInfo.fullName)}`
|
|
5297
|
+
);
|
|
5298
|
+
logger.log("");
|
|
5299
|
+
} catch (error) {
|
|
5300
|
+
failStepSpinner(repoSpinner, 1, TOTAL_STEPS, "Validation failed");
|
|
5301
|
+
throw error;
|
|
5302
|
+
}
|
|
5303
|
+
const fetchSpinner = createStepSpinner(
|
|
5304
|
+
2,
|
|
5305
|
+
TOTAL_STEPS,
|
|
5306
|
+
`Fetching work items from the last ${days} days`
|
|
5307
|
+
);
|
|
5308
|
+
fetchSpinner.start();
|
|
5309
|
+
const workItems = await adapter.fetchWorkItems({
|
|
5310
|
+
days,
|
|
5311
|
+
scanMode,
|
|
5312
|
+
author: await adapter.getCurrentUser() || void 0
|
|
5313
|
+
});
|
|
5314
|
+
if (workItems.length === 0) {
|
|
5315
|
+
failStepSpinner(fetchSpinner, 2, TOTAL_STEPS, `No work items found in the last ${days} days`);
|
|
5316
|
+
logger.log("");
|
|
5317
|
+
return { repoName: repo.name, service: repo.service, created: 0, skipped: 0 };
|
|
5318
|
+
}
|
|
5319
|
+
succeedStepSpinner(
|
|
5320
|
+
fetchSpinner,
|
|
5321
|
+
2,
|
|
5322
|
+
TOTAL_STEPS,
|
|
5323
|
+
`Found ${theme.count(workItems.length)} work item${workItems.length > 1 ? "s" : ""}`
|
|
5324
|
+
);
|
|
5325
|
+
logger.log("");
|
|
5326
|
+
logger.log(formatCommitStats(workItems));
|
|
5327
|
+
logger.log("");
|
|
5328
|
+
const existingBrags = await apiService.listBrags({ limit: 100 });
|
|
5329
|
+
logger.debug(`Fetched ${existingBrags.brags.length} existing brags`);
|
|
5330
|
+
const existingUrls = new Set(existingBrags.brags.flatMap((b) => b.attachments || []));
|
|
5331
|
+
logger.debug(`Existing URLs in attachments: ${existingUrls.size}`);
|
|
5332
|
+
const duplicates = workItems.filter((c) => c.url && existingUrls.has(c.url));
|
|
5333
|
+
const newWorkItems = workItems.filter((c) => !c.url || !existingUrls.has(c.url));
|
|
5334
|
+
logger.debug(`Found ${duplicates.length} duplicates, ${newWorkItems.length} new items`);
|
|
5335
|
+
if (duplicates.length > 0) {
|
|
5336
|
+
logger.log("");
|
|
5337
|
+
logger.info(
|
|
5338
|
+
colors.dim(
|
|
5339
|
+
`\u2139 ${duplicates.length} work item${duplicates.length > 1 ? "s" : ""} already exist in Bragduck (will be skipped)`
|
|
5340
|
+
)
|
|
5341
|
+
);
|
|
5342
|
+
logger.log("");
|
|
5343
|
+
}
|
|
5344
|
+
if (newWorkItems.length === 0) {
|
|
5345
|
+
logger.log("");
|
|
5346
|
+
logger.info(theme.secondary("All work items already exist in Bragduck. Nothing to sync."));
|
|
5347
|
+
logger.log("");
|
|
5348
|
+
return { repoName: repo.name, service: repo.service, created: 0, skipped: duplicates.length };
|
|
5349
|
+
}
|
|
5350
|
+
let sortedCommits = [...newWorkItems];
|
|
5351
|
+
if (sortOption === "date") {
|
|
5352
|
+
sortedCommits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
5353
|
+
} else if (sortOption === "size") {
|
|
5354
|
+
sortedCommits.sort((a, b) => {
|
|
5355
|
+
const sizeA = (a.diffStats?.insertions || 0) + (a.diffStats?.deletions || 0);
|
|
5356
|
+
const sizeB = (b.diffStats?.insertions || 0) + (b.diffStats?.deletions || 0);
|
|
5357
|
+
return sizeB - sizeA;
|
|
5358
|
+
});
|
|
5359
|
+
} else if (sortOption === "files") {
|
|
5360
|
+
sortedCommits.sort((a, b) => {
|
|
5361
|
+
const filesA = a.diffStats?.filesChanged || 0;
|
|
5362
|
+
const filesB = b.diffStats?.filesChanged || 0;
|
|
5363
|
+
return filesB - filesA;
|
|
5364
|
+
});
|
|
5365
|
+
}
|
|
5366
|
+
let selectedShas;
|
|
5367
|
+
if (options.turbo) {
|
|
5368
|
+
selectedShas = sortedCommits.map((c) => c.sha);
|
|
5369
|
+
logger.debug(`Turbo mode: auto-selected all ${selectedShas.length} new items`);
|
|
5370
|
+
} else {
|
|
5371
|
+
selectedShas = await promptSelectCommits(sortedCommits);
|
|
5372
|
+
if (selectedShas.length === 0) {
|
|
5373
|
+
logger.log("");
|
|
5374
|
+
logger.info(theme.secondary("No work items selected. Sync cancelled."));
|
|
5375
|
+
logger.log("");
|
|
5376
|
+
return { repoName: repo.name, service: repo.service, created: 0, skipped: duplicates.length };
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
const selectedCommits = sortedCommits.filter((c) => selectedShas.includes(c.sha));
|
|
5380
|
+
logger.log(formatSelectionSummary(selectedCommits.length, selectedCommits));
|
|
5381
|
+
logger.log("");
|
|
5382
|
+
const refineSpinner = createStepSpinner(
|
|
5383
|
+
3,
|
|
5384
|
+
TOTAL_STEPS,
|
|
5385
|
+
`Refining ${theme.count(selectedCommits.length)} work item${selectedCommits.length > 1 ? "s" : ""} with AI`
|
|
5386
|
+
);
|
|
5387
|
+
refineSpinner.start();
|
|
5388
|
+
const refineRequest = {
|
|
5389
|
+
brags: selectedCommits.map((c) => ({
|
|
5390
|
+
text: c.message,
|
|
5391
|
+
date: c.date,
|
|
5392
|
+
title: c.message.split("\n")[0]
|
|
5393
|
+
}))
|
|
5394
|
+
};
|
|
5395
|
+
const refineResponse = await apiService.refineBrags(refineRequest);
|
|
5396
|
+
let refinedBrags = refineResponse.refined_brags;
|
|
5397
|
+
succeedStepSpinner(refineSpinner, 3, TOTAL_STEPS, "Work items refined successfully");
|
|
5398
|
+
logger.log("");
|
|
5399
|
+
let acceptedBrags;
|
|
5400
|
+
if (options.turbo) {
|
|
5401
|
+
acceptedBrags = refinedBrags;
|
|
5402
|
+
logger.debug(`Turbo mode: auto-accepted all ${acceptedBrags.length} refined brags`);
|
|
5403
|
+
logger.log("");
|
|
5404
|
+
} else {
|
|
5405
|
+
logger.info("Preview of refined brags:");
|
|
5406
|
+
logger.log("");
|
|
5407
|
+
logger.log(formatRefinedCommitsTable(refinedBrags, selectedCommits));
|
|
5408
|
+
logger.log("");
|
|
5409
|
+
acceptedBrags = await promptReviewBrags(refinedBrags, selectedCommits);
|
|
5410
|
+
}
|
|
5411
|
+
if (acceptedBrags.length === 0) {
|
|
5412
|
+
logger.log("");
|
|
5413
|
+
logger.info(theme.secondary("No brags selected for creation. Sync cancelled."));
|
|
5414
|
+
logger.log("");
|
|
5415
|
+
return { repoName: repo.name, service: repo.service, created: 0, skipped: duplicates.length };
|
|
5416
|
+
}
|
|
5417
|
+
logger.log("");
|
|
5418
|
+
const createSpinner2 = createStepSpinner(
|
|
5419
|
+
4,
|
|
5420
|
+
TOTAL_STEPS,
|
|
5421
|
+
`Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
|
|
5422
|
+
);
|
|
5423
|
+
createSpinner2.start();
|
|
5424
|
+
const createRequest = {
|
|
5425
|
+
brags: acceptedBrags.map((refined, index) => {
|
|
5426
|
+
const originalCommit = selectedCommits[index];
|
|
5427
|
+
const repoTag = repo.name;
|
|
5428
|
+
const tagsWithRepo = refined.suggested_tags.includes(repoTag) ? refined.suggested_tags : [repoTag, ...refined.suggested_tags];
|
|
5429
|
+
return {
|
|
5430
|
+
commit_sha: originalCommit?.sha || `brag-${index}`,
|
|
5431
|
+
title: refined.refined_title,
|
|
5432
|
+
description: refined.refined_description,
|
|
5433
|
+
tags: tagsWithRepo,
|
|
5434
|
+
repository: repoInfo.url,
|
|
5435
|
+
date: originalCommit?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5436
|
+
commit_url: originalCommit?.url || "",
|
|
5437
|
+
impact_score: refined.suggested_impactLevel,
|
|
5438
|
+
impact_description: refined.impact_description,
|
|
5439
|
+
attachments: originalCommit?.url ? [originalCommit.url] : [],
|
|
5440
|
+
orgId: orgId || void 0,
|
|
5441
|
+
// External fields for non-git sources
|
|
5442
|
+
externalId: originalCommit?.externalId,
|
|
5443
|
+
externalType: originalCommit?.externalType,
|
|
5444
|
+
externalSource: originalCommit?.externalSource,
|
|
5445
|
+
externalUrl: originalCommit?.externalUrl
|
|
5446
|
+
};
|
|
5447
|
+
})
|
|
5448
|
+
};
|
|
5449
|
+
const CREATE_TIMEOUT = 6e4;
|
|
5450
|
+
logger.debug(`Sending ${acceptedBrags.length} brags to API for creation...`);
|
|
5451
|
+
let createResponse;
|
|
5452
|
+
try {
|
|
5453
|
+
createResponse = await Promise.race([
|
|
5454
|
+
apiService.createBrags(createRequest),
|
|
5455
|
+
new Promise((_, reject) => {
|
|
5456
|
+
setTimeout(
|
|
5457
|
+
() => reject(new Error("Create brags timeout after 60 seconds")),
|
|
5458
|
+
CREATE_TIMEOUT
|
|
5459
|
+
);
|
|
5460
|
+
})
|
|
5461
|
+
]);
|
|
5462
|
+
logger.debug(`API response: ${createResponse.created} brags created`);
|
|
5463
|
+
succeedStepSpinner(
|
|
5464
|
+
createSpinner2,
|
|
5465
|
+
4,
|
|
5466
|
+
TOTAL_STEPS,
|
|
5467
|
+
`Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
|
|
5468
|
+
);
|
|
5469
|
+
logger.log("");
|
|
5470
|
+
} catch (error) {
|
|
5471
|
+
failStepSpinner(createSpinner2, 4, TOTAL_STEPS, "Failed to create brags");
|
|
5472
|
+
logger.log("");
|
|
5473
|
+
throw error;
|
|
5474
|
+
}
|
|
5475
|
+
const createdBrags = createResponse.brags.map((brag, index) => ({
|
|
5476
|
+
title: brag.title,
|
|
5477
|
+
date: selectedCommits[index]?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5478
|
+
source: repo.service
|
|
5479
|
+
}));
|
|
5480
|
+
return {
|
|
5481
|
+
repoName: repo.name,
|
|
5482
|
+
service: repo.service,
|
|
5483
|
+
created: createResponse.created,
|
|
5484
|
+
skipped: duplicates.length,
|
|
5485
|
+
createdBrags
|
|
5486
|
+
};
|
|
5487
|
+
}
|
|
5488
|
+
async function syncMultipleRepositories(repos, options) {
|
|
5489
|
+
logger.debug(`Starting multi-repo sync for ${repos.length} repositories`);
|
|
5490
|
+
let days = options.days;
|
|
5491
|
+
if (!days && options.today) {
|
|
5492
|
+
days = 1;
|
|
5493
|
+
logger.debug("Using --today flag: scanning last 24 hours (1 day)");
|
|
5494
|
+
}
|
|
5495
|
+
if (!days && !options.turbo) {
|
|
5496
|
+
const defaultDays = storageService.getConfig("defaultCommitDays") || 30;
|
|
5497
|
+
days = await promptDaysToScan(defaultDays);
|
|
5498
|
+
logger.log("");
|
|
5499
|
+
}
|
|
5500
|
+
if (!days && options.turbo) {
|
|
5501
|
+
days = storageService.getConfig("defaultCommitDays") || 30;
|
|
5502
|
+
logger.debug(`Turbo mode: using default ${days} days`);
|
|
5503
|
+
}
|
|
5504
|
+
if (!days) {
|
|
5505
|
+
days = 30;
|
|
5506
|
+
}
|
|
5507
|
+
let scanMode = "prs";
|
|
5508
|
+
if (!options.turbo) {
|
|
5509
|
+
scanMode = await promptScanMode();
|
|
5510
|
+
logger.log("");
|
|
5511
|
+
} else {
|
|
5512
|
+
logger.debug("Turbo mode: using default scan mode (prs)");
|
|
5513
|
+
}
|
|
5514
|
+
let sortOption = "date";
|
|
5515
|
+
if (!options.turbo) {
|
|
5516
|
+
sortOption = await promptSortOption();
|
|
5517
|
+
logger.log("");
|
|
5518
|
+
} else {
|
|
5519
|
+
logger.debug("Turbo mode: using default sort option (date)");
|
|
5520
|
+
}
|
|
5521
|
+
const results = [];
|
|
5522
|
+
for (let i = 0; i < repos.length; i++) {
|
|
5523
|
+
const repo = repos[i];
|
|
5524
|
+
if (!repo) continue;
|
|
5525
|
+
const current = i + 1;
|
|
5526
|
+
const total = repos.length;
|
|
5527
|
+
logger.log("");
|
|
5528
|
+
logger.log(colors.highlight(`\u2501\u2501\u2501 Syncing ${current}/${total}: ${repo.name} \u2501\u2501\u2501`));
|
|
5529
|
+
logger.log("");
|
|
5530
|
+
try {
|
|
5531
|
+
let selectedOrgId = null;
|
|
5532
|
+
if (!options.turbo) {
|
|
5533
|
+
const userInfo = authService.getUserInfo();
|
|
5534
|
+
if (userInfo?.id) {
|
|
5535
|
+
try {
|
|
5536
|
+
const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
|
|
5537
|
+
if (orgsResponse.items.length > 0) {
|
|
5538
|
+
const defaultOrgId = storageService.getConfig("defaultCompany");
|
|
5539
|
+
selectedOrgId = await promptSelectOrganisationWithDefault(
|
|
5540
|
+
orgsResponse.items,
|
|
5541
|
+
defaultOrgId
|
|
5542
|
+
);
|
|
5543
|
+
logger.log("");
|
|
5544
|
+
}
|
|
5545
|
+
} catch {
|
|
5546
|
+
logger.debug("Failed to fetch organisations, skipping org selection");
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
}
|
|
5550
|
+
const result = await syncSingleRepository(
|
|
5551
|
+
repo,
|
|
5552
|
+
days,
|
|
5553
|
+
sortOption,
|
|
5554
|
+
scanMode,
|
|
5555
|
+
selectedOrgId,
|
|
5556
|
+
options
|
|
5557
|
+
);
|
|
5558
|
+
results.push(result);
|
|
5559
|
+
logger.log("");
|
|
5560
|
+
logger.log(
|
|
5561
|
+
theme.success(
|
|
5562
|
+
`Completed ${repo.name}: ${result.created} created, ${result.skipped} skipped`
|
|
5563
|
+
)
|
|
5564
|
+
);
|
|
5565
|
+
} catch (error) {
|
|
5566
|
+
const err = error;
|
|
5567
|
+
logger.log("");
|
|
5568
|
+
logger.warning(`Failed to sync ${repo.name}: ${err.message}`);
|
|
5569
|
+
results.push({
|
|
5570
|
+
repoName: repo.name,
|
|
5571
|
+
service: repo.service,
|
|
5572
|
+
created: 0,
|
|
5573
|
+
skipped: 0,
|
|
5574
|
+
error: err.message
|
|
5575
|
+
});
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
const totalCreated = results.reduce((sum, r) => sum + r.created, 0);
|
|
5579
|
+
const totalSkipped = results.reduce((sum, r) => sum + r.skipped, 0);
|
|
5580
|
+
logger.log("");
|
|
5581
|
+
logger.log(colors.highlight("\u2501\u2501\u2501 Multi-Repository Sync Summary \u2501\u2501\u2501"));
|
|
5582
|
+
logger.log("");
|
|
5583
|
+
logger.log(`Total repositories: ${results.length}`);
|
|
5584
|
+
logger.log(`Total brags created: ${totalCreated}`);
|
|
5585
|
+
logger.log(`Total brags skipped: ${totalSkipped}`);
|
|
5586
|
+
const failed = results.filter((r) => r.error);
|
|
5587
|
+
if (failed.length > 0) {
|
|
5588
|
+
logger.log("");
|
|
5589
|
+
logger.log(colors.warning(`Failed repositories: ${failed.length}`));
|
|
5590
|
+
for (const result of failed) {
|
|
5591
|
+
logger.info(` \u2022 ${result.repoName}: ${result.error}`);
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
return {
|
|
5595
|
+
totalCreated,
|
|
5596
|
+
totalSkipped,
|
|
5597
|
+
results
|
|
5598
|
+
};
|
|
5599
|
+
}
|
|
5600
|
+
|
|
4931
5601
|
// src/commands/sync.ts
|
|
4932
5602
|
async function promptSelectService() {
|
|
4933
5603
|
const nonGitServices = ["atlassian", "jira", "confluence"];
|
|
@@ -4945,7 +5615,7 @@ async function promptSelectService() {
|
|
|
4945
5615
|
);
|
|
4946
5616
|
const serviceChoices = [
|
|
4947
5617
|
{
|
|
4948
|
-
name: "Git Contributions (
|
|
5618
|
+
name: "Git Contributions (detect repositories)",
|
|
4949
5619
|
value: "git",
|
|
4950
5620
|
description: "Sync from your local repository (GitHub, GitLab, or Bitbucket)"
|
|
4951
5621
|
}
|
|
@@ -5160,11 +5830,17 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5160
5830
|
const createRequest = {
|
|
5161
5831
|
brags: acceptedBrags.map((refined, index) => {
|
|
5162
5832
|
const originalCommit = selectedCommits[index];
|
|
5833
|
+
const isGitSource = sourceType === "github" || sourceType === "gitlab" || sourceType === "bitbucket" || sourceType === "atlassian";
|
|
5834
|
+
let tagsWithRepo = refined.suggested_tags;
|
|
5835
|
+
if (isGitSource && repoInfo.name) {
|
|
5836
|
+
const repoTag = repoInfo.name;
|
|
5837
|
+
tagsWithRepo = refined.suggested_tags.includes(repoTag) ? refined.suggested_tags : [repoTag, ...refined.suggested_tags];
|
|
5838
|
+
}
|
|
5163
5839
|
return {
|
|
5164
5840
|
commit_sha: originalCommit?.sha || `brag-${index}`,
|
|
5165
5841
|
title: refined.refined_title,
|
|
5166
5842
|
description: refined.refined_description,
|
|
5167
|
-
tags:
|
|
5843
|
+
tags: tagsWithRepo,
|
|
5168
5844
|
repository: repoInfo.url,
|
|
5169
5845
|
date: originalCommit?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5170
5846
|
commit_url: originalCommit?.url || "",
|
|
@@ -5206,9 +5882,9 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5206
5882
|
logger.log("");
|
|
5207
5883
|
throw error;
|
|
5208
5884
|
}
|
|
5209
|
-
const createdBrags = createResponse.brags.map((brag) => ({
|
|
5885
|
+
const createdBrags = createResponse.brags.map((brag, index) => ({
|
|
5210
5886
|
title: brag.title,
|
|
5211
|
-
date:
|
|
5887
|
+
date: selectedCommits[index]?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5212
5888
|
source: sourceType
|
|
5213
5889
|
}));
|
|
5214
5890
|
return { created: createResponse.created, skipped: duplicates.length, createdBrags };
|
|
@@ -5346,9 +6022,17 @@ async function syncAllAuthenticatedServices(options) {
|
|
|
5346
6022
|
);
|
|
5347
6023
|
for (const result of successful) {
|
|
5348
6024
|
const serviceLabel = result.service.charAt(0).toUpperCase() + result.service.slice(1);
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
6025
|
+
if (result.created === 0 && result.skipped > 0) {
|
|
6026
|
+
logger.info(
|
|
6027
|
+
` \u2022 ${serviceLabel}: ${colors.dim(`All ${result.skipped} item${result.skipped !== 1 ? "s" : ""} already synced`)}`
|
|
6028
|
+
);
|
|
6029
|
+
} else if (result.created === 0 && result.skipped === 0) {
|
|
6030
|
+
logger.info(` \u2022 ${serviceLabel}: ${colors.dim("No items found")}`);
|
|
6031
|
+
} else {
|
|
6032
|
+
logger.info(
|
|
6033
|
+
` \u2022 ${serviceLabel}: ${result.created} brag${result.created !== 1 ? "s" : ""} created${result.skipped > 0 ? `, ${result.skipped} skipped` : ""}`
|
|
6034
|
+
);
|
|
6035
|
+
}
|
|
5352
6036
|
}
|
|
5353
6037
|
logger.log("");
|
|
5354
6038
|
}
|
|
@@ -5429,29 +6113,33 @@ async function syncCommand(options = {}) {
|
|
|
5429
6113
|
const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Preparing sync");
|
|
5430
6114
|
detectionSpinner.start();
|
|
5431
6115
|
if (selectedSource === "git") {
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
);
|
|
5437
|
-
|
|
5438
|
-
failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Not a git repository");
|
|
5439
|
-
logger.log("");
|
|
5440
|
-
logger.error("No git repository detected in current directory");
|
|
5441
|
-
logger.log("");
|
|
5442
|
-
logger.info("Navigate to a git repository or select a different service");
|
|
5443
|
-
return;
|
|
5444
|
-
}
|
|
5445
|
-
sourceType = gitSource.type;
|
|
5446
|
-
logger.debug(`Detected git service: ${sourceType}`);
|
|
5447
|
-
} catch (error) {
|
|
5448
|
-
logger.debug(`Git detection error: ${error}`);
|
|
5449
|
-
failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Failed to detect git service");
|
|
6116
|
+
succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Discovering repositories");
|
|
6117
|
+
logger.log("");
|
|
6118
|
+
const repos = await discoverRepositories();
|
|
6119
|
+
if (repos.length === 0) {
|
|
6120
|
+
logger.log("");
|
|
6121
|
+
logger.error("\u26A0 No git repositories found");
|
|
5450
6122
|
logger.log("");
|
|
5451
|
-
logger.
|
|
6123
|
+
logger.info("Searched in:");
|
|
6124
|
+
logger.info(` \u2022 Current directory: ${process.cwd()}`);
|
|
6125
|
+
logger.info(` \u2022 Subdirectories (1 level): ${process.cwd()}/*`);
|
|
5452
6126
|
logger.log("");
|
|
6127
|
+
logger.info("Run from a directory containing git repositories.");
|
|
5453
6128
|
return;
|
|
5454
6129
|
}
|
|
6130
|
+
logger.log("");
|
|
6131
|
+
logger.info(`Found ${repos.length} repositor${repos.length > 1 ? "ies" : "y"}:`);
|
|
6132
|
+
repos.forEach((r) => logger.info(` \u2022 ${r.name} (${r.service})`));
|
|
6133
|
+
logger.log("");
|
|
6134
|
+
const result2 = await syncMultipleRepositories(repos, options);
|
|
6135
|
+
if (result2.totalCreated > 0 || result2.totalSkipped > 0) {
|
|
6136
|
+
logger.log("");
|
|
6137
|
+
logger.log(boxen6(formatMultiRepoSummary(result2), boxStyles.success));
|
|
6138
|
+
} else {
|
|
6139
|
+
logger.log("");
|
|
6140
|
+
logger.info("No brags created.");
|
|
6141
|
+
}
|
|
6142
|
+
process.exit(0);
|
|
5455
6143
|
} else {
|
|
5456
6144
|
sourceType = selectedSource;
|
|
5457
6145
|
}
|
|
@@ -5822,11 +6510,13 @@ Please use ${chalk7.cyan("bragduck sync")} instead.
|
|
|
5822
6510
|
const createRequest = {
|
|
5823
6511
|
brags: acceptedBrags.map((refined, index) => {
|
|
5824
6512
|
const originalCommit = newCommits[index];
|
|
6513
|
+
const repoTag = repoInfo.name;
|
|
6514
|
+
const tagsWithRepo = refined.suggested_tags.includes(repoTag) ? refined.suggested_tags : [repoTag, ...refined.suggested_tags];
|
|
5825
6515
|
return {
|
|
5826
6516
|
commit_sha: originalCommit?.sha || `brag-${index}`,
|
|
5827
6517
|
title: refined.refined_title,
|
|
5828
6518
|
description: refined.refined_description,
|
|
5829
|
-
tags:
|
|
6519
|
+
tags: tagsWithRepo,
|
|
5830
6520
|
repository: repoInfo.url,
|
|
5831
6521
|
date: originalCommit?.date || "",
|
|
5832
6522
|
commit_url: originalCommit?.url || "",
|