@bragduck/cli 2.27.0 → 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.
@@ -3653,6 +3653,32 @@ var JiraService = class {
3653
3653
  return null;
3654
3654
  }
3655
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
+ }
3656
3682
  /**
3657
3683
  * Build JQL query from options
3658
3684
  */
@@ -3782,7 +3808,7 @@ var JiraService = class {
3782
3808
  );
3783
3809
  break;
3784
3810
  }
3785
- 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`;
3786
3812
  try {
3787
3813
  const response = await this.request(endpoint);
3788
3814
  if (response.issues.length === 0) {
@@ -3808,14 +3834,7 @@ var JiraService = class {
3808
3834
  break;
3809
3835
  }
3810
3836
  if (options.limit && allIssues.length >= options.limit) {
3811
- const email2 = await this.getCurrentUser();
3812
- const limitedIssues = allIssues.slice(0, options.limit);
3813
- const commits2 = [];
3814
- for (const issue of limitedIssues) {
3815
- const commit = await this.transformIssueToCommit(issue, void 0, email2 || void 0);
3816
- commits2.push(commit);
3817
- }
3818
- return commits2;
3837
+ break;
3819
3838
  }
3820
3839
  startAt += maxResults;
3821
3840
  } catch (error) {
@@ -3839,9 +3858,23 @@ var JiraService = class {
3839
3858
  throw error;
3840
3859
  }
3841
3860
  }
3861
+ const issuesToProcess = options.limit ? allIssues.slice(0, options.limit) : allIssues;
3842
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
+ }
3843
3876
  const commits = [];
3844
- for (const issue of allIssues) {
3877
+ for (const issue of issuesToProcess) {
3845
3878
  const commit = await this.transformIssueToCommit(issue, void 0, email || void 0);
3846
3879
  commits.push(commit);
3847
3880
  }
@@ -3863,7 +3896,7 @@ var JiraService = class {
3863
3896
  /**
3864
3897
  * Transform Jira issue to GitCommit format with contribution-specific data
3865
3898
  */
3866
- async transformIssueToCommit(issue, instanceUrl, userEmail) {
3899
+ async transformIssueToCommit(issue, instanceUrl, userEmail, userChanges) {
3867
3900
  let contribution = null;
3868
3901
  if (userEmail) {
3869
3902
  contribution = await this.determineJiraContributionType(issue, userEmail);
@@ -3905,6 +3938,8 @@ ${truncatedDesc}`;
3905
3938
  date = issue.fields.created;
3906
3939
  } else if (contribution?.type === "assigned-resolved" && issue.fields.resolutiondate) {
3907
3940
  date = issue.fields.resolutiondate;
3941
+ } else if (userChanges && userChanges.length > 0) {
3942
+ date = userChanges[userChanges.length - 1].created;
3908
3943
  } else {
3909
3944
  date = issue.fields.updated;
3910
3945
  }
@@ -4094,6 +4129,59 @@ var ConfluenceService = class {
4094
4129
  return null;
4095
4130
  }
4096
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
+ }
4097
4185
  /**
4098
4186
  * Build CQL query from options
4099
4187
  * Returns empty string if no filters need CQL (will use simple endpoint instead)
@@ -4129,28 +4217,31 @@ var ConfluenceService = class {
4129
4217
  * Determine the type of contribution the current user made to a page
4130
4218
  * Returns: 'created' | 'edited' | 'commented'
4131
4219
  */
4132
- async determineContributionType(page, userEmail) {
4133
- 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) {
4134
4223
  return {
4135
4224
  type: "created",
4136
4225
  details: `Created page with ${page.version?.number || 1} version${(page.version?.number || 1) > 1 ? "s" : ""}`
4137
4226
  };
4138
4227
  }
4139
- 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;
4140
4229
  const userComments = page.children?.comment?.results?.filter(
4141
- (comment) => comment.version?.by?.email === userEmail
4230
+ (comment) => comment.version?.by ? this.isMatchingUser(comment.version.by, userEmail) : false
4142
4231
  ) || [];
4143
4232
  const hasComments = userComments.length > 0;
4144
4233
  if (hasEdits && hasComments) {
4234
+ const editCount = userVersions?.length || 1;
4145
4235
  return {
4146
4236
  type: "edited",
4147
- 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" : ""}`
4148
4238
  };
4149
4239
  }
4150
4240
  if (hasEdits) {
4241
+ const editCount = userVersions?.length || 1;
4151
4242
  return {
4152
4243
  type: "edited",
4153
- details: `Edited page to version ${page.version?.number || 1}`
4244
+ details: `Edited page (${editCount} edit${editCount > 1 ? "s" : ""})`
4154
4245
  };
4155
4246
  }
4156
4247
  if (hasComments) {
@@ -4248,14 +4339,7 @@ var ConfluenceService = class {
4248
4339
  break;
4249
4340
  }
4250
4341
  if (options.limit && allPages.length >= options.limit) {
4251
- const email2 = await this.getCurrentUser();
4252
- const limitedPages = allPages.slice(0, options.limit);
4253
- const commits2 = [];
4254
- for (const page of limitedPages) {
4255
- const commit = await this.transformPageToCommit(page, void 0, email2 || void 0);
4256
- commits2.push(commit);
4257
- }
4258
- return commits2;
4342
+ break;
4259
4343
  }
4260
4344
  start += limit;
4261
4345
  } catch (error) {
@@ -4279,9 +4363,23 @@ var ConfluenceService = class {
4279
4363
  throw error;
4280
4364
  }
4281
4365
  }
4366
+ const pagesToProcess = options.limit ? allPages.slice(0, options.limit) : allPages;
4282
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
+ }
4283
4381
  const commits = [];
4284
- for (const page of allPages) {
4382
+ for (const page of pagesToProcess) {
4285
4383
  const commit = await this.transformPageToCommit(page, void 0, email || void 0);
4286
4384
  commits.push(commit);
4287
4385
  }
@@ -4303,10 +4401,10 @@ var ConfluenceService = class {
4303
4401
  /**
4304
4402
  * Transform Confluence page to GitCommit format with contribution-specific data
4305
4403
  */
4306
- async transformPageToCommit(page, instanceUrl, userEmail) {
4404
+ async transformPageToCommit(page, instanceUrl, userEmail, userVersions) {
4307
4405
  let contribution = null;
4308
4406
  if (userEmail) {
4309
- contribution = await this.determineContributionType(page, userEmail);
4407
+ contribution = await this.determineContributionType(page, userEmail, userVersions);
4310
4408
  }
4311
4409
  let message;
4312
4410
  let contributionPrefix = "";
@@ -4344,7 +4442,14 @@ ${contribution.details}
4344
4442
  const impactScore = contribution ? this.calculateContributionImpact(contribution.type, baseSize) : baseSize;
4345
4443
  const author = page.history?.createdBy?.displayName || page.version?.by?.displayName || "Unknown Author";
4346
4444
  const authorEmail = page.history?.createdBy?.email || page.version?.by?.email || "unknown@example.com";
4347
- 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
+ }
4348
4453
  return {
4349
4454
  sha: page.id,
4350
4455
  message,
@@ -5917,9 +6022,17 @@ async function syncAllAuthenticatedServices(options) {
5917
6022
  );
5918
6023
  for (const result of successful) {
5919
6024
  const serviceLabel = result.service.charAt(0).toUpperCase() + result.service.slice(1);
5920
- logger.info(
5921
- ` \u2022 ${serviceLabel}: ${result.created} brag${result.created !== 1 ? "s" : ""} created${result.skipped > 0 ? `, ${result.skipped} skipped` : ""}`
5922
- );
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
+ }
5923
6036
  }
5924
6037
  logger.log("");
5925
6038
  }