@bragduck/cli 2.24.2 → 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 +673 -96
- 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":
|
|
@@ -4641,6 +4723,35 @@ function formatErrorMessage(message, hint) {
|
|
|
4641
4723
|
|
|
4642
4724
|
${error}${hintText}`;
|
|
4643
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
|
+
}
|
|
4644
4755
|
|
|
4645
4756
|
// src/ui/prompts.ts
|
|
4646
4757
|
async function promptSelectCommits(commits) {
|
|
@@ -4696,6 +4807,25 @@ async function promptDaysToScan(defaultDays = 30) {
|
|
|
4696
4807
|
}
|
|
4697
4808
|
return parseInt(selected, 10);
|
|
4698
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
|
+
}
|
|
4699
4829
|
async function promptSortOption() {
|
|
4700
4830
|
const choices = [
|
|
4701
4831
|
{
|
|
@@ -4928,6 +5058,441 @@ function failStepSpinner(spinner, currentStep, totalSteps, text) {
|
|
|
4928
5058
|
spinner.fail(`${stepIndicator} ${colors.error(text)}`);
|
|
4929
5059
|
}
|
|
4930
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
|
+
|
|
4931
5496
|
// src/commands/sync.ts
|
|
4932
5497
|
async function promptSelectService() {
|
|
4933
5498
|
const nonGitServices = ["atlassian", "jira", "confluence"];
|
|
@@ -4945,7 +5510,7 @@ async function promptSelectService() {
|
|
|
4945
5510
|
);
|
|
4946
5511
|
const serviceChoices = [
|
|
4947
5512
|
{
|
|
4948
|
-
name: "Git Contributions (
|
|
5513
|
+
name: "Git Contributions (detect repositories)",
|
|
4949
5514
|
value: "git",
|
|
4950
5515
|
description: "Sync from your local repository (GitHub, GitLab, or Bitbucket)"
|
|
4951
5516
|
}
|
|
@@ -5160,11 +5725,17 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5160
5725
|
const createRequest = {
|
|
5161
5726
|
brags: acceptedBrags.map((refined, index) => {
|
|
5162
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
|
+
}
|
|
5163
5734
|
return {
|
|
5164
5735
|
commit_sha: originalCommit?.sha || `brag-${index}`,
|
|
5165
5736
|
title: refined.refined_title,
|
|
5166
5737
|
description: refined.refined_description,
|
|
5167
|
-
tags:
|
|
5738
|
+
tags: tagsWithRepo,
|
|
5168
5739
|
repository: repoInfo.url,
|
|
5169
5740
|
date: originalCommit?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5170
5741
|
commit_url: originalCommit?.url || "",
|
|
@@ -5206,9 +5777,9 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5206
5777
|
logger.log("");
|
|
5207
5778
|
throw error;
|
|
5208
5779
|
}
|
|
5209
|
-
const createdBrags = createResponse.brags.map((brag) => ({
|
|
5780
|
+
const createdBrags = createResponse.brags.map((brag, index) => ({
|
|
5210
5781
|
title: brag.title,
|
|
5211
|
-
date:
|
|
5782
|
+
date: selectedCommits[index]?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5212
5783
|
source: sourceType
|
|
5213
5784
|
}));
|
|
5214
5785
|
return { created: createResponse.created, skipped: duplicates.length, createdBrags };
|
|
@@ -5429,29 +6000,33 @@ async function syncCommand(options = {}) {
|
|
|
5429
6000
|
const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Preparing sync");
|
|
5430
6001
|
detectionSpinner.start();
|
|
5431
6002
|
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");
|
|
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");
|
|
5450
6009
|
logger.log("");
|
|
5451
|
-
logger.
|
|
6010
|
+
logger.info("Searched in:");
|
|
6011
|
+
logger.info(` \u2022 Current directory: ${process.cwd()}`);
|
|
6012
|
+
logger.info(` \u2022 Subdirectories (1 level): ${process.cwd()}/*`);
|
|
5452
6013
|
logger.log("");
|
|
6014
|
+
logger.info("Run from a directory containing git repositories.");
|
|
5453
6015
|
return;
|
|
5454
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);
|
|
5455
6030
|
} else {
|
|
5456
6031
|
sourceType = selectedSource;
|
|
5457
6032
|
}
|
|
@@ -5822,11 +6397,13 @@ Please use ${chalk7.cyan("bragduck sync")} instead.
|
|
|
5822
6397
|
const createRequest = {
|
|
5823
6398
|
brags: acceptedBrags.map((refined, index) => {
|
|
5824
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];
|
|
5825
6402
|
return {
|
|
5826
6403
|
commit_sha: originalCommit?.sha || `brag-${index}`,
|
|
5827
6404
|
title: refined.refined_title,
|
|
5828
6405
|
description: refined.refined_description,
|
|
5829
|
-
tags:
|
|
6406
|
+
tags: tagsWithRepo,
|
|
5830
6407
|
repository: repoInfo.url,
|
|
5831
6408
|
date: originalCommit?.date || "",
|
|
5832
6409
|
commit_url: originalCommit?.url || "",
|