@bragduck/cli 2.23.0 → 2.27.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 +703 -91
- 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();
|
|
@@ -4327,16 +4410,15 @@ var AdapterFactory = class {
|
|
|
4327
4410
|
/**
|
|
4328
4411
|
* Get adapter for a specific source type
|
|
4329
4412
|
*/
|
|
4330
|
-
static getAdapter(source) {
|
|
4413
|
+
static getAdapter(source, repoPath) {
|
|
4331
4414
|
switch (source) {
|
|
4332
4415
|
case "github":
|
|
4333
|
-
return githubSyncAdapter;
|
|
4416
|
+
return repoPath ? new GitHubSyncAdapter(repoPath) : githubSyncAdapter;
|
|
4334
4417
|
case "bitbucket":
|
|
4335
4418
|
case "atlassian":
|
|
4336
|
-
return bitbucketSyncAdapter;
|
|
4337
|
-
// Bitbucket Cloud and Server use same adapter
|
|
4419
|
+
return repoPath ? new BitbucketSyncAdapter(repoPath) : bitbucketSyncAdapter;
|
|
4338
4420
|
case "gitlab":
|
|
4339
|
-
return gitlabSyncAdapter;
|
|
4421
|
+
return repoPath ? new GitLabSyncAdapter(repoPath) : gitlabSyncAdapter;
|
|
4340
4422
|
case "jira":
|
|
4341
4423
|
return jiraSyncAdapter;
|
|
4342
4424
|
case "confluence":
|
|
@@ -4501,6 +4583,9 @@ import terminalLink from "terminal-link";
|
|
|
4501
4583
|
init_esm_shims();
|
|
4502
4584
|
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
4503
4585
|
function formatDate(dateString) {
|
|
4586
|
+
if (!dateString || typeof dateString === "string" && dateString.trim() === "") {
|
|
4587
|
+
return "No date";
|
|
4588
|
+
}
|
|
4504
4589
|
const date = typeof dateString === "string" ? new Date(dateString) : dateString;
|
|
4505
4590
|
if (isNaN(date.getTime())) {
|
|
4506
4591
|
return "Invalid date";
|
|
@@ -4638,6 +4723,35 @@ function formatErrorMessage(message, hint) {
|
|
|
4638
4723
|
|
|
4639
4724
|
${error}${hintText}`;
|
|
4640
4725
|
}
|
|
4726
|
+
function formatMultiRepoSummary(result) {
|
|
4727
|
+
const emoji = "\u{1F389}";
|
|
4728
|
+
const title = colors.successBold(
|
|
4729
|
+
`${emoji} Successfully synced ${result.results.length} repositor${result.results.length > 1 ? "ies" : "y"}!`
|
|
4730
|
+
);
|
|
4731
|
+
let repoSummaries = "";
|
|
4732
|
+
for (const repoResult of result.results) {
|
|
4733
|
+
const repoHeader = `
|
|
4734
|
+
|
|
4735
|
+
${colors.highlight(repoResult.repoName)} (${colors.primary(repoResult.service)}): ${theme.count(repoResult.created)} brag${repoResult.created !== 1 ? "s" : ""} created`;
|
|
4736
|
+
let bragsList = "";
|
|
4737
|
+
if (repoResult.createdBrags && repoResult.createdBrags.length > 0) {
|
|
4738
|
+
for (const brag of repoResult.createdBrags) {
|
|
4739
|
+
const dateFormatted = formatDate(brag.date);
|
|
4740
|
+
bragsList += `
|
|
4741
|
+
${colors.dim("\u2022")} ${colors.white(brag.title)} ${colors.dim(`(${dateFormatted})`)}`;
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
repoSummaries += repoHeader + bragsList;
|
|
4745
|
+
}
|
|
4746
|
+
const total = `
|
|
4747
|
+
|
|
4748
|
+
${theme.secondary("Total: ")}${theme.count(result.totalCreated)} brag${result.totalCreated !== 1 ? "s" : ""} created across ${result.results.length} repositor${result.results.length > 1 ? "ies" : "y"}`;
|
|
4749
|
+
const hint = theme.secondary("\n\nRun ") + theme.command("bragduck list") + theme.secondary(" to see all your brags");
|
|
4750
|
+
const url = "https://bragduck.com/app/brags";
|
|
4751
|
+
const clickableUrl = terminalLink(url, url, { fallback: () => colors.primary(url) });
|
|
4752
|
+
const webUrl = theme.secondary("\n\nOr, check ") + clickableUrl + theme.secondary(" to see all your brags");
|
|
4753
|
+
return `${title}${repoSummaries}${total}${hint}${webUrl}`;
|
|
4754
|
+
}
|
|
4641
4755
|
|
|
4642
4756
|
// src/ui/prompts.ts
|
|
4643
4757
|
async function promptSelectCommits(commits) {
|
|
@@ -4693,6 +4807,25 @@ async function promptDaysToScan(defaultDays = 30) {
|
|
|
4693
4807
|
}
|
|
4694
4808
|
return parseInt(selected, 10);
|
|
4695
4809
|
}
|
|
4810
|
+
async function promptScanMode() {
|
|
4811
|
+
const choices = [
|
|
4812
|
+
{
|
|
4813
|
+
name: "Pull Requests / Merge Requests (Recommended)",
|
|
4814
|
+
value: "prs",
|
|
4815
|
+
description: "Scan merged PRs/MRs"
|
|
4816
|
+
},
|
|
4817
|
+
{
|
|
4818
|
+
name: "Direct Commits",
|
|
4819
|
+
value: "commits",
|
|
4820
|
+
description: "Scan git commits directly"
|
|
4821
|
+
}
|
|
4822
|
+
];
|
|
4823
|
+
return await select2({
|
|
4824
|
+
message: "What would you like to scan?",
|
|
4825
|
+
choices,
|
|
4826
|
+
default: "prs"
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4696
4829
|
async function promptSortOption() {
|
|
4697
4830
|
const choices = [
|
|
4698
4831
|
{
|
|
@@ -4925,8 +5058,445 @@ function failStepSpinner(spinner, currentStep, totalSteps, text) {
|
|
|
4925
5058
|
spinner.fail(`${stepIndicator} ${colors.error(text)}`);
|
|
4926
5059
|
}
|
|
4927
5060
|
|
|
5061
|
+
// src/utils/repo-scanner.ts
|
|
5062
|
+
init_esm_shims();
|
|
5063
|
+
import { promises as fs2 } from "fs";
|
|
5064
|
+
import path3 from "path";
|
|
5065
|
+
import { exec as exec6 } from "child_process";
|
|
5066
|
+
import { promisify as promisify6 } from "util";
|
|
5067
|
+
init_logger();
|
|
5068
|
+
var execAsync6 = promisify6(exec6);
|
|
5069
|
+
async function isGitRepository(dirPath) {
|
|
5070
|
+
try {
|
|
5071
|
+
const gitPath = path3.join(dirPath, ".git");
|
|
5072
|
+
await fs2.access(gitPath);
|
|
5073
|
+
return true;
|
|
5074
|
+
} catch {
|
|
5075
|
+
return false;
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
async function scanSubdirectories(basePath) {
|
|
5079
|
+
try {
|
|
5080
|
+
const entries = await fs2.readdir(basePath, { withFileTypes: true });
|
|
5081
|
+
return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => path3.join(basePath, entry.name));
|
|
5082
|
+
} catch (error) {
|
|
5083
|
+
logger.debug(`Error scanning subdirectories: ${error}`);
|
|
5084
|
+
return [];
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
async function getRemoteUrl(repoPath) {
|
|
5088
|
+
try {
|
|
5089
|
+
const { stdout } = await execAsync6("command git remote -v", { cwd: repoPath });
|
|
5090
|
+
const remotes = stdout.split("\n").filter(Boolean);
|
|
5091
|
+
if (remotes.length === 0) {
|
|
5092
|
+
return null;
|
|
5093
|
+
}
|
|
5094
|
+
const match = remotes[0]?.match(/^\S+\s+(\S+)\s+\(fetch\)$/);
|
|
5095
|
+
return match && match[1] ? match[1] : null;
|
|
5096
|
+
} catch {
|
|
5097
|
+
return null;
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
async function discoverRepositories() {
|
|
5101
|
+
const repositories = [];
|
|
5102
|
+
const currentDir = process.cwd();
|
|
5103
|
+
logger.debug(`Scanning for repositories in: ${currentDir}`);
|
|
5104
|
+
if (await isGitRepository(currentDir)) {
|
|
5105
|
+
logger.debug(`Current directory is a git repository`);
|
|
5106
|
+
try {
|
|
5107
|
+
const result = await sourceDetector.detectSources({}, currentDir);
|
|
5108
|
+
const remoteUrl = await getRemoteUrl(currentDir);
|
|
5109
|
+
if (result.detected.length > 0 && result.recommended && remoteUrl) {
|
|
5110
|
+
const source = result.detected.find((s) => s.type === result.recommended);
|
|
5111
|
+
if (source) {
|
|
5112
|
+
repositories.push({
|
|
5113
|
+
path: currentDir,
|
|
5114
|
+
name: path3.basename(currentDir),
|
|
5115
|
+
service: source.type,
|
|
5116
|
+
remoteUrl
|
|
5117
|
+
});
|
|
5118
|
+
logger.debug(`Added repository: ${path3.basename(currentDir)} (${source.type})`);
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
} catch (error) {
|
|
5122
|
+
logger.debug(`Error detecting sources for current directory: ${error}`);
|
|
5123
|
+
}
|
|
5124
|
+
} else {
|
|
5125
|
+
logger.info("No repository found in current directory.");
|
|
5126
|
+
logger.log("");
|
|
5127
|
+
const subdirs = await scanSubdirectories(currentDir);
|
|
5128
|
+
logger.info(
|
|
5129
|
+
`Scanning ${subdirs.length} director${subdirs.length === 1 ? "y" : "ies"} for repositories...`
|
|
5130
|
+
);
|
|
5131
|
+
logger.log("");
|
|
5132
|
+
logger.debug(`Subdirectories to scan: ${subdirs.map((d) => path3.basename(d)).join(", ")}`);
|
|
5133
|
+
for (const subdir of subdirs) {
|
|
5134
|
+
if (await isGitRepository(subdir)) {
|
|
5135
|
+
try {
|
|
5136
|
+
const result = await sourceDetector.detectSources({ allowInteractive: false }, subdir);
|
|
5137
|
+
const remoteUrl = await getRemoteUrl(subdir);
|
|
5138
|
+
if (result.detected.length > 0 && result.recommended && remoteUrl) {
|
|
5139
|
+
const source = result.detected.find((s) => s.type === result.recommended);
|
|
5140
|
+
if (source) {
|
|
5141
|
+
repositories.push({
|
|
5142
|
+
path: subdir,
|
|
5143
|
+
name: path3.basename(subdir),
|
|
5144
|
+
service: source.type,
|
|
5145
|
+
remoteUrl
|
|
5146
|
+
});
|
|
5147
|
+
logger.debug(`Added repository: ${path3.basename(subdir)} (${source.type})`);
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
} catch (error) {
|
|
5151
|
+
logger.debug(`Error detecting sources for ${subdir}: ${error}`);
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
logger.debug(`Found ${repositories.length} repositories`);
|
|
5157
|
+
return repositories;
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5160
|
+
// src/sync/multi-repo-sync.ts
|
|
5161
|
+
init_esm_shims();
|
|
5162
|
+
init_api_service();
|
|
5163
|
+
init_auth_service();
|
|
5164
|
+
init_storage_service();
|
|
5165
|
+
init_logger();
|
|
5166
|
+
async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, options) {
|
|
5167
|
+
const TOTAL_STEPS = 5;
|
|
5168
|
+
logger.debug(`Syncing repository: ${repo.name} at ${repo.path}`);
|
|
5169
|
+
const adapter = AdapterFactory.getAdapter(repo.service, repo.path);
|
|
5170
|
+
const repoSpinner = createStepSpinner(1, TOTAL_STEPS, "Validating repository");
|
|
5171
|
+
repoSpinner.start();
|
|
5172
|
+
const VALIDATION_TIMEOUT = 3e4;
|
|
5173
|
+
let repoInfo;
|
|
5174
|
+
try {
|
|
5175
|
+
repoInfo = await Promise.race([
|
|
5176
|
+
(async () => {
|
|
5177
|
+
await adapter.validate();
|
|
5178
|
+
return await adapter.getRepositoryInfo();
|
|
5179
|
+
})(),
|
|
5180
|
+
new Promise((_, reject) => {
|
|
5181
|
+
setTimeout(
|
|
5182
|
+
() => reject(new Error("Validation timeout after 30 seconds")),
|
|
5183
|
+
VALIDATION_TIMEOUT
|
|
5184
|
+
);
|
|
5185
|
+
})
|
|
5186
|
+
]);
|
|
5187
|
+
succeedStepSpinner(
|
|
5188
|
+
repoSpinner,
|
|
5189
|
+
1,
|
|
5190
|
+
TOTAL_STEPS,
|
|
5191
|
+
`Repository: ${theme.value(repoInfo.fullName)}`
|
|
5192
|
+
);
|
|
5193
|
+
logger.log("");
|
|
5194
|
+
} catch (error) {
|
|
5195
|
+
failStepSpinner(repoSpinner, 1, TOTAL_STEPS, "Validation failed");
|
|
5196
|
+
throw error;
|
|
5197
|
+
}
|
|
5198
|
+
const fetchSpinner = createStepSpinner(
|
|
5199
|
+
2,
|
|
5200
|
+
TOTAL_STEPS,
|
|
5201
|
+
`Fetching work items from the last ${days} days`
|
|
5202
|
+
);
|
|
5203
|
+
fetchSpinner.start();
|
|
5204
|
+
const workItems = await adapter.fetchWorkItems({
|
|
5205
|
+
days,
|
|
5206
|
+
scanMode,
|
|
5207
|
+
author: await adapter.getCurrentUser() || void 0
|
|
5208
|
+
});
|
|
5209
|
+
if (workItems.length === 0) {
|
|
5210
|
+
failStepSpinner(fetchSpinner, 2, TOTAL_STEPS, `No work items found in the last ${days} days`);
|
|
5211
|
+
logger.log("");
|
|
5212
|
+
return { repoName: repo.name, service: repo.service, created: 0, skipped: 0 };
|
|
5213
|
+
}
|
|
5214
|
+
succeedStepSpinner(
|
|
5215
|
+
fetchSpinner,
|
|
5216
|
+
2,
|
|
5217
|
+
TOTAL_STEPS,
|
|
5218
|
+
`Found ${theme.count(workItems.length)} work item${workItems.length > 1 ? "s" : ""}`
|
|
5219
|
+
);
|
|
5220
|
+
logger.log("");
|
|
5221
|
+
logger.log(formatCommitStats(workItems));
|
|
5222
|
+
logger.log("");
|
|
5223
|
+
const existingBrags = await apiService.listBrags({ limit: 100 });
|
|
5224
|
+
logger.debug(`Fetched ${existingBrags.brags.length} existing brags`);
|
|
5225
|
+
const existingUrls = new Set(existingBrags.brags.flatMap((b) => b.attachments || []));
|
|
5226
|
+
logger.debug(`Existing URLs in attachments: ${existingUrls.size}`);
|
|
5227
|
+
const duplicates = workItems.filter((c) => c.url && existingUrls.has(c.url));
|
|
5228
|
+
const newWorkItems = workItems.filter((c) => !c.url || !existingUrls.has(c.url));
|
|
5229
|
+
logger.debug(`Found ${duplicates.length} duplicates, ${newWorkItems.length} new items`);
|
|
5230
|
+
if (duplicates.length > 0) {
|
|
5231
|
+
logger.log("");
|
|
5232
|
+
logger.info(
|
|
5233
|
+
colors.dim(
|
|
5234
|
+
`\u2139 ${duplicates.length} work item${duplicates.length > 1 ? "s" : ""} already exist in Bragduck (will be skipped)`
|
|
5235
|
+
)
|
|
5236
|
+
);
|
|
5237
|
+
logger.log("");
|
|
5238
|
+
}
|
|
5239
|
+
if (newWorkItems.length === 0) {
|
|
5240
|
+
logger.log("");
|
|
5241
|
+
logger.info(theme.secondary("All work items already exist in Bragduck. Nothing to sync."));
|
|
5242
|
+
logger.log("");
|
|
5243
|
+
return { repoName: repo.name, service: repo.service, created: 0, skipped: duplicates.length };
|
|
5244
|
+
}
|
|
5245
|
+
let sortedCommits = [...newWorkItems];
|
|
5246
|
+
if (sortOption === "date") {
|
|
5247
|
+
sortedCommits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
5248
|
+
} else if (sortOption === "size") {
|
|
5249
|
+
sortedCommits.sort((a, b) => {
|
|
5250
|
+
const sizeA = (a.diffStats?.insertions || 0) + (a.diffStats?.deletions || 0);
|
|
5251
|
+
const sizeB = (b.diffStats?.insertions || 0) + (b.diffStats?.deletions || 0);
|
|
5252
|
+
return sizeB - sizeA;
|
|
5253
|
+
});
|
|
5254
|
+
} else if (sortOption === "files") {
|
|
5255
|
+
sortedCommits.sort((a, b) => {
|
|
5256
|
+
const filesA = a.diffStats?.filesChanged || 0;
|
|
5257
|
+
const filesB = b.diffStats?.filesChanged || 0;
|
|
5258
|
+
return filesB - filesA;
|
|
5259
|
+
});
|
|
5260
|
+
}
|
|
5261
|
+
let selectedShas;
|
|
5262
|
+
if (options.turbo) {
|
|
5263
|
+
selectedShas = sortedCommits.map((c) => c.sha);
|
|
5264
|
+
logger.debug(`Turbo mode: auto-selected all ${selectedShas.length} new items`);
|
|
5265
|
+
} else {
|
|
5266
|
+
selectedShas = await promptSelectCommits(sortedCommits);
|
|
5267
|
+
if (selectedShas.length === 0) {
|
|
5268
|
+
logger.log("");
|
|
5269
|
+
logger.info(theme.secondary("No work items selected. Sync cancelled."));
|
|
5270
|
+
logger.log("");
|
|
5271
|
+
return { repoName: repo.name, service: repo.service, created: 0, skipped: duplicates.length };
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
const selectedCommits = sortedCommits.filter((c) => selectedShas.includes(c.sha));
|
|
5275
|
+
logger.log(formatSelectionSummary(selectedCommits.length, selectedCommits));
|
|
5276
|
+
logger.log("");
|
|
5277
|
+
const refineSpinner = createStepSpinner(
|
|
5278
|
+
3,
|
|
5279
|
+
TOTAL_STEPS,
|
|
5280
|
+
`Refining ${theme.count(selectedCommits.length)} work item${selectedCommits.length > 1 ? "s" : ""} with AI`
|
|
5281
|
+
);
|
|
5282
|
+
refineSpinner.start();
|
|
5283
|
+
const refineRequest = {
|
|
5284
|
+
brags: selectedCommits.map((c) => ({
|
|
5285
|
+
text: c.message,
|
|
5286
|
+
date: c.date,
|
|
5287
|
+
title: c.message.split("\n")[0]
|
|
5288
|
+
}))
|
|
5289
|
+
};
|
|
5290
|
+
const refineResponse = await apiService.refineBrags(refineRequest);
|
|
5291
|
+
let refinedBrags = refineResponse.refined_brags;
|
|
5292
|
+
succeedStepSpinner(refineSpinner, 3, TOTAL_STEPS, "Work items refined successfully");
|
|
5293
|
+
logger.log("");
|
|
5294
|
+
let acceptedBrags;
|
|
5295
|
+
if (options.turbo) {
|
|
5296
|
+
acceptedBrags = refinedBrags;
|
|
5297
|
+
logger.debug(`Turbo mode: auto-accepted all ${acceptedBrags.length} refined brags`);
|
|
5298
|
+
logger.log("");
|
|
5299
|
+
} else {
|
|
5300
|
+
logger.info("Preview of refined brags:");
|
|
5301
|
+
logger.log("");
|
|
5302
|
+
logger.log(formatRefinedCommitsTable(refinedBrags, selectedCommits));
|
|
5303
|
+
logger.log("");
|
|
5304
|
+
acceptedBrags = await promptReviewBrags(refinedBrags, selectedCommits);
|
|
5305
|
+
}
|
|
5306
|
+
if (acceptedBrags.length === 0) {
|
|
5307
|
+
logger.log("");
|
|
5308
|
+
logger.info(theme.secondary("No brags selected for creation. Sync cancelled."));
|
|
5309
|
+
logger.log("");
|
|
5310
|
+
return { repoName: repo.name, service: repo.service, created: 0, skipped: duplicates.length };
|
|
5311
|
+
}
|
|
5312
|
+
logger.log("");
|
|
5313
|
+
const createSpinner2 = createStepSpinner(
|
|
5314
|
+
4,
|
|
5315
|
+
TOTAL_STEPS,
|
|
5316
|
+
`Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
|
|
5317
|
+
);
|
|
5318
|
+
createSpinner2.start();
|
|
5319
|
+
const createRequest = {
|
|
5320
|
+
brags: acceptedBrags.map((refined, index) => {
|
|
5321
|
+
const originalCommit = selectedCommits[index];
|
|
5322
|
+
const repoTag = repo.name;
|
|
5323
|
+
const tagsWithRepo = refined.suggested_tags.includes(repoTag) ? refined.suggested_tags : [repoTag, ...refined.suggested_tags];
|
|
5324
|
+
return {
|
|
5325
|
+
commit_sha: originalCommit?.sha || `brag-${index}`,
|
|
5326
|
+
title: refined.refined_title,
|
|
5327
|
+
description: refined.refined_description,
|
|
5328
|
+
tags: tagsWithRepo,
|
|
5329
|
+
repository: repoInfo.url,
|
|
5330
|
+
date: originalCommit?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5331
|
+
commit_url: originalCommit?.url || "",
|
|
5332
|
+
impact_score: refined.suggested_impactLevel,
|
|
5333
|
+
impact_description: refined.impact_description,
|
|
5334
|
+
attachments: originalCommit?.url ? [originalCommit.url] : [],
|
|
5335
|
+
orgId: orgId || void 0,
|
|
5336
|
+
// External fields for non-git sources
|
|
5337
|
+
externalId: originalCommit?.externalId,
|
|
5338
|
+
externalType: originalCommit?.externalType,
|
|
5339
|
+
externalSource: originalCommit?.externalSource,
|
|
5340
|
+
externalUrl: originalCommit?.externalUrl
|
|
5341
|
+
};
|
|
5342
|
+
})
|
|
5343
|
+
};
|
|
5344
|
+
const CREATE_TIMEOUT = 6e4;
|
|
5345
|
+
logger.debug(`Sending ${acceptedBrags.length} brags to API for creation...`);
|
|
5346
|
+
let createResponse;
|
|
5347
|
+
try {
|
|
5348
|
+
createResponse = await Promise.race([
|
|
5349
|
+
apiService.createBrags(createRequest),
|
|
5350
|
+
new Promise((_, reject) => {
|
|
5351
|
+
setTimeout(
|
|
5352
|
+
() => reject(new Error("Create brags timeout after 60 seconds")),
|
|
5353
|
+
CREATE_TIMEOUT
|
|
5354
|
+
);
|
|
5355
|
+
})
|
|
5356
|
+
]);
|
|
5357
|
+
logger.debug(`API response: ${createResponse.created} brags created`);
|
|
5358
|
+
succeedStepSpinner(
|
|
5359
|
+
createSpinner2,
|
|
5360
|
+
4,
|
|
5361
|
+
TOTAL_STEPS,
|
|
5362
|
+
`Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
|
|
5363
|
+
);
|
|
5364
|
+
logger.log("");
|
|
5365
|
+
} catch (error) {
|
|
5366
|
+
failStepSpinner(createSpinner2, 4, TOTAL_STEPS, "Failed to create brags");
|
|
5367
|
+
logger.log("");
|
|
5368
|
+
throw error;
|
|
5369
|
+
}
|
|
5370
|
+
const createdBrags = createResponse.brags.map((brag, index) => ({
|
|
5371
|
+
title: brag.title,
|
|
5372
|
+
date: selectedCommits[index]?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5373
|
+
source: repo.service
|
|
5374
|
+
}));
|
|
5375
|
+
return {
|
|
5376
|
+
repoName: repo.name,
|
|
5377
|
+
service: repo.service,
|
|
5378
|
+
created: createResponse.created,
|
|
5379
|
+
skipped: duplicates.length,
|
|
5380
|
+
createdBrags
|
|
5381
|
+
};
|
|
5382
|
+
}
|
|
5383
|
+
async function syncMultipleRepositories(repos, options) {
|
|
5384
|
+
logger.debug(`Starting multi-repo sync for ${repos.length} repositories`);
|
|
5385
|
+
let days = options.days;
|
|
5386
|
+
if (!days && options.today) {
|
|
5387
|
+
days = 1;
|
|
5388
|
+
logger.debug("Using --today flag: scanning last 24 hours (1 day)");
|
|
5389
|
+
}
|
|
5390
|
+
if (!days && !options.turbo) {
|
|
5391
|
+
const defaultDays = storageService.getConfig("defaultCommitDays") || 30;
|
|
5392
|
+
days = await promptDaysToScan(defaultDays);
|
|
5393
|
+
logger.log("");
|
|
5394
|
+
}
|
|
5395
|
+
if (!days && options.turbo) {
|
|
5396
|
+
days = storageService.getConfig("defaultCommitDays") || 30;
|
|
5397
|
+
logger.debug(`Turbo mode: using default ${days} days`);
|
|
5398
|
+
}
|
|
5399
|
+
if (!days) {
|
|
5400
|
+
days = 30;
|
|
5401
|
+
}
|
|
5402
|
+
let scanMode = "prs";
|
|
5403
|
+
if (!options.turbo) {
|
|
5404
|
+
scanMode = await promptScanMode();
|
|
5405
|
+
logger.log("");
|
|
5406
|
+
} else {
|
|
5407
|
+
logger.debug("Turbo mode: using default scan mode (prs)");
|
|
5408
|
+
}
|
|
5409
|
+
let sortOption = "date";
|
|
5410
|
+
if (!options.turbo) {
|
|
5411
|
+
sortOption = await promptSortOption();
|
|
5412
|
+
logger.log("");
|
|
5413
|
+
} else {
|
|
5414
|
+
logger.debug("Turbo mode: using default sort option (date)");
|
|
5415
|
+
}
|
|
5416
|
+
const results = [];
|
|
5417
|
+
for (let i = 0; i < repos.length; i++) {
|
|
5418
|
+
const repo = repos[i];
|
|
5419
|
+
if (!repo) continue;
|
|
5420
|
+
const current = i + 1;
|
|
5421
|
+
const total = repos.length;
|
|
5422
|
+
logger.log("");
|
|
5423
|
+
logger.log(colors.highlight(`\u2501\u2501\u2501 Syncing ${current}/${total}: ${repo.name} \u2501\u2501\u2501`));
|
|
5424
|
+
logger.log("");
|
|
5425
|
+
try {
|
|
5426
|
+
let selectedOrgId = null;
|
|
5427
|
+
if (!options.turbo) {
|
|
5428
|
+
const userInfo = authService.getUserInfo();
|
|
5429
|
+
if (userInfo?.id) {
|
|
5430
|
+
try {
|
|
5431
|
+
const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
|
|
5432
|
+
if (orgsResponse.items.length > 0) {
|
|
5433
|
+
const defaultOrgId = storageService.getConfig("defaultCompany");
|
|
5434
|
+
selectedOrgId = await promptSelectOrganisationWithDefault(
|
|
5435
|
+
orgsResponse.items,
|
|
5436
|
+
defaultOrgId
|
|
5437
|
+
);
|
|
5438
|
+
logger.log("");
|
|
5439
|
+
}
|
|
5440
|
+
} catch {
|
|
5441
|
+
logger.debug("Failed to fetch organisations, skipping org selection");
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
const result = await syncSingleRepository(
|
|
5446
|
+
repo,
|
|
5447
|
+
days,
|
|
5448
|
+
sortOption,
|
|
5449
|
+
scanMode,
|
|
5450
|
+
selectedOrgId,
|
|
5451
|
+
options
|
|
5452
|
+
);
|
|
5453
|
+
results.push(result);
|
|
5454
|
+
logger.log("");
|
|
5455
|
+
logger.log(
|
|
5456
|
+
theme.success(
|
|
5457
|
+
`Completed ${repo.name}: ${result.created} created, ${result.skipped} skipped`
|
|
5458
|
+
)
|
|
5459
|
+
);
|
|
5460
|
+
} catch (error) {
|
|
5461
|
+
const err = error;
|
|
5462
|
+
logger.log("");
|
|
5463
|
+
logger.warning(`Failed to sync ${repo.name}: ${err.message}`);
|
|
5464
|
+
results.push({
|
|
5465
|
+
repoName: repo.name,
|
|
5466
|
+
service: repo.service,
|
|
5467
|
+
created: 0,
|
|
5468
|
+
skipped: 0,
|
|
5469
|
+
error: err.message
|
|
5470
|
+
});
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
const totalCreated = results.reduce((sum, r) => sum + r.created, 0);
|
|
5474
|
+
const totalSkipped = results.reduce((sum, r) => sum + r.skipped, 0);
|
|
5475
|
+
logger.log("");
|
|
5476
|
+
logger.log(colors.highlight("\u2501\u2501\u2501 Multi-Repository Sync Summary \u2501\u2501\u2501"));
|
|
5477
|
+
logger.log("");
|
|
5478
|
+
logger.log(`Total repositories: ${results.length}`);
|
|
5479
|
+
logger.log(`Total brags created: ${totalCreated}`);
|
|
5480
|
+
logger.log(`Total brags skipped: ${totalSkipped}`);
|
|
5481
|
+
const failed = results.filter((r) => r.error);
|
|
5482
|
+
if (failed.length > 0) {
|
|
5483
|
+
logger.log("");
|
|
5484
|
+
logger.log(colors.warning(`Failed repositories: ${failed.length}`));
|
|
5485
|
+
for (const result of failed) {
|
|
5486
|
+
logger.info(` \u2022 ${result.repoName}: ${result.error}`);
|
|
5487
|
+
}
|
|
5488
|
+
}
|
|
5489
|
+
return {
|
|
5490
|
+
totalCreated,
|
|
5491
|
+
totalSkipped,
|
|
5492
|
+
results
|
|
5493
|
+
};
|
|
5494
|
+
}
|
|
5495
|
+
|
|
4928
5496
|
// src/commands/sync.ts
|
|
4929
5497
|
async function promptSelectService() {
|
|
5498
|
+
const nonGitServices = ["atlassian", "jira", "confluence"];
|
|
5499
|
+
const authenticatedServices = await storageService.getAuthenticatedServices();
|
|
4930
5500
|
const allServices = [
|
|
4931
5501
|
"github",
|
|
4932
5502
|
"gitlab",
|
|
@@ -4935,22 +5505,26 @@ async function promptSelectService() {
|
|
|
4935
5505
|
"jira",
|
|
4936
5506
|
"confluence"
|
|
4937
5507
|
];
|
|
4938
|
-
const authenticatedServices = await storageService.getAuthenticatedServices();
|
|
4939
5508
|
const authenticatedSyncServices = authenticatedServices.filter(
|
|
4940
5509
|
(service) => service !== "bragduck" && allServices.includes(service)
|
|
4941
5510
|
);
|
|
4942
|
-
const serviceChoices =
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
5511
|
+
const serviceChoices = [
|
|
5512
|
+
{
|
|
5513
|
+
name: "Git Contributions (detect repositories)",
|
|
5514
|
+
value: "git",
|
|
5515
|
+
description: "Sync from your local repository (GitHub, GitLab, or Bitbucket)"
|
|
5516
|
+
}
|
|
5517
|
+
];
|
|
5518
|
+
for (const service of nonGitServices) {
|
|
5519
|
+
const isAuth = await storageService.isServiceAuthenticated(service);
|
|
5520
|
+
const indicator = isAuth ? "\u2713" : "\u2717";
|
|
5521
|
+
const serviceLabel = service.charAt(0).toUpperCase() + service.slice(1);
|
|
5522
|
+
serviceChoices.push({
|
|
5523
|
+
name: `${indicator} ${serviceLabel}`,
|
|
5524
|
+
value: service,
|
|
5525
|
+
description: isAuth ? "Authenticated" : "Not authenticated"
|
|
5526
|
+
});
|
|
5527
|
+
}
|
|
4954
5528
|
if (authenticatedSyncServices.length > 0) {
|
|
4955
5529
|
const serviceNames = authenticatedSyncServices.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(", ");
|
|
4956
5530
|
serviceChoices.push({
|
|
@@ -5151,13 +5725,19 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5151
5725
|
const createRequest = {
|
|
5152
5726
|
brags: acceptedBrags.map((refined, index) => {
|
|
5153
5727
|
const originalCommit = selectedCommits[index];
|
|
5728
|
+
const isGitSource = sourceType === "github" || sourceType === "gitlab" || sourceType === "bitbucket" || sourceType === "atlassian";
|
|
5729
|
+
let tagsWithRepo = refined.suggested_tags;
|
|
5730
|
+
if (isGitSource && repoInfo.name) {
|
|
5731
|
+
const repoTag = repoInfo.name;
|
|
5732
|
+
tagsWithRepo = refined.suggested_tags.includes(repoTag) ? refined.suggested_tags : [repoTag, ...refined.suggested_tags];
|
|
5733
|
+
}
|
|
5154
5734
|
return {
|
|
5155
5735
|
commit_sha: originalCommit?.sha || `brag-${index}`,
|
|
5156
5736
|
title: refined.refined_title,
|
|
5157
5737
|
description: refined.refined_description,
|
|
5158
|
-
tags:
|
|
5738
|
+
tags: tagsWithRepo,
|
|
5159
5739
|
repository: repoInfo.url,
|
|
5160
|
-
date: originalCommit?.date ||
|
|
5740
|
+
date: originalCommit?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5161
5741
|
commit_url: originalCommit?.url || "",
|
|
5162
5742
|
impact_score: refined.suggested_impactLevel,
|
|
5163
5743
|
impact_description: refined.impact_description,
|
|
@@ -5197,9 +5777,9 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5197
5777
|
logger.log("");
|
|
5198
5778
|
throw error;
|
|
5199
5779
|
}
|
|
5200
|
-
const createdBrags = createResponse.brags.map((brag) => ({
|
|
5780
|
+
const createdBrags = createResponse.brags.map((brag, index) => ({
|
|
5201
5781
|
title: brag.title,
|
|
5202
|
-
date:
|
|
5782
|
+
date: selectedCommits[index]?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5203
5783
|
source: sourceType
|
|
5204
5784
|
}));
|
|
5205
5785
|
return { created: createResponse.created, skipped: duplicates.length, createdBrags };
|
|
@@ -5419,7 +5999,37 @@ async function syncCommand(options = {}) {
|
|
|
5419
5999
|
} else {
|
|
5420
6000
|
const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Preparing sync");
|
|
5421
6001
|
detectionSpinner.start();
|
|
5422
|
-
|
|
6002
|
+
if (selectedSource === "git") {
|
|
6003
|
+
succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Discovering repositories");
|
|
6004
|
+
logger.log("");
|
|
6005
|
+
const repos = await discoverRepositories();
|
|
6006
|
+
if (repos.length === 0) {
|
|
6007
|
+
logger.log("");
|
|
6008
|
+
logger.error("\u26A0 No git repositories found");
|
|
6009
|
+
logger.log("");
|
|
6010
|
+
logger.info("Searched in:");
|
|
6011
|
+
logger.info(` \u2022 Current directory: ${process.cwd()}`);
|
|
6012
|
+
logger.info(` \u2022 Subdirectories (1 level): ${process.cwd()}/*`);
|
|
6013
|
+
logger.log("");
|
|
6014
|
+
logger.info("Run from a directory containing git repositories.");
|
|
6015
|
+
return;
|
|
6016
|
+
}
|
|
6017
|
+
logger.log("");
|
|
6018
|
+
logger.info(`Found ${repos.length} repositor${repos.length > 1 ? "ies" : "y"}:`);
|
|
6019
|
+
repos.forEach((r) => logger.info(` \u2022 ${r.name} (${r.service})`));
|
|
6020
|
+
logger.log("");
|
|
6021
|
+
const result2 = await syncMultipleRepositories(repos, options);
|
|
6022
|
+
if (result2.totalCreated > 0 || result2.totalSkipped > 0) {
|
|
6023
|
+
logger.log("");
|
|
6024
|
+
logger.log(boxen6(formatMultiRepoSummary(result2), boxStyles.success));
|
|
6025
|
+
} else {
|
|
6026
|
+
logger.log("");
|
|
6027
|
+
logger.info("No brags created.");
|
|
6028
|
+
}
|
|
6029
|
+
process.exit(0);
|
|
6030
|
+
} else {
|
|
6031
|
+
sourceType = selectedSource;
|
|
6032
|
+
}
|
|
5423
6033
|
if (sourceType === "jira" || sourceType === "confluence") {
|
|
5424
6034
|
const creds = await storageService.getServiceCredentials(sourceType);
|
|
5425
6035
|
const envInstance = loadEnvConfig()[`${sourceType}Instance`];
|
|
@@ -5787,11 +6397,13 @@ Please use ${chalk7.cyan("bragduck sync")} instead.
|
|
|
5787
6397
|
const createRequest = {
|
|
5788
6398
|
brags: acceptedBrags.map((refined, index) => {
|
|
5789
6399
|
const originalCommit = newCommits[index];
|
|
6400
|
+
const repoTag = repoInfo.name;
|
|
6401
|
+
const tagsWithRepo = refined.suggested_tags.includes(repoTag) ? refined.suggested_tags : [repoTag, ...refined.suggested_tags];
|
|
5790
6402
|
return {
|
|
5791
6403
|
commit_sha: originalCommit?.sha || `brag-${index}`,
|
|
5792
6404
|
title: refined.refined_title,
|
|
5793
6405
|
description: refined.refined_description,
|
|
5794
|
-
tags:
|
|
6406
|
+
tags: tagsWithRepo,
|
|
5795
6407
|
repository: repoInfo.url,
|
|
5796
6408
|
date: originalCommit?.date || "",
|
|
5797
6409
|
commit_url: originalCommit?.url || "",
|