@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.
@@ -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(path3) {
1693
- const gitDir = join4(path3, ".git");
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: path3,
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
- await gitService.validateRepository();
1985
- const { stdout } = await this.execGhCommand("command gh repo view --json url");
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
- ...options,
2094
- author: currentUser
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 { stdout } = await execAsync3("git remote -v");
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(output) {
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
- let prs;
2852
- if (options.author) {
2853
- prs = await githubService.getMergedPRs({
2854
- days: options.days,
2855
- limit: options.limit,
2856
- author: options.author
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 = await githubService.getPRsByCurrentUser({
2860
- days: options.days,
2861
- limit: options.limit
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
- let prs;
3134
- if (options.author) {
3135
- prs = await bitbucketService.getMergedPRs({
3136
- days: options.days,
3137
- limit: options.limit,
3138
- author: options.author
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 = await bitbucketService.getPRsByCurrentUser({
3142
- days: options.days,
3143
- limit: options.limit
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
- let mrs;
3427
- if (options.author) {
3428
- mrs = await gitlabService.getMergedMRs({
3429
- days: options.days,
3430
- limit: options.limit,
3431
- author: options.author
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 = await gitlabService.getMRsByCurrentUser({
3435
- days: options.days,
3436
- limit: options.limit
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
- const email2 = await this.getCurrentUser();
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 allIssues) {
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
- if (page.history?.createdBy?.email === userEmail) {
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?.email === userEmail && (page.version?.number || 0) > 1;
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?.email === userEmail
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 (v${page.version?.number || 1}) and added ${userComments.length} comment${userComments.length > 1 ? "s" : ""}`
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 to version ${page.version?.number || 1}`
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
- const email2 = await this.getCurrentUser();
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 allPages) {
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
- const date = contribution?.type === "created" ? page.history?.createdDate || page.version?.when : page.version?.when || (/* @__PURE__ */ new Date()).toISOString();
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 (current dir)",
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: refined.suggested_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: brag.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
- logger.info(
5350
- ` \u2022 ${serviceLabel}: ${result.created} brag${result.created !== 1 ? "s" : ""} created${result.skipped > 0 ? `, ${result.skipped} skipped` : ""}`
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
- try {
5433
- const { detected } = await sourceDetector.detectSources();
5434
- const gitSource = detected.find(
5435
- (source) => ["github", "gitlab", "bitbucket", "atlassian"].includes(source.type)
5436
- );
5437
- if (!gitSource) {
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.error("Could not detect git repository");
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: refined.suggested_tags,
6519
+ tags: tagsWithRepo,
5830
6520
  repository: repoInfo.url,
5831
6521
  date: originalCommit?.date || "",
5832
6522
  commit_url: originalCommit?.url || "",