@bragduck/cli 2.23.0 → 2.27.0

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