@bragduck/cli 2.28.0 → 2.29.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.
@@ -22,7 +22,7 @@ var init_esm_shims = __esm({
22
22
  import { config } from "dotenv";
23
23
  import { fileURLToPath as fileURLToPath2 } from "url";
24
24
  import { dirname, join } from "path";
25
- var __filename, __dirname, APP_NAME, CONFIG_KEYS, DEFAULT_CONFIG, OAUTH_CONFIG, API_ENDPOINTS, ENCRYPTION_CONFIG, STORAGE_PATHS, HTTP_STATUS, CONFIG_FILES;
25
+ var __filename, __dirname, APP_NAME, CONFIG_KEYS, DEFAULT_CONFIG, OAUTH_CONFIG, ATLASSIAN_OAUTH_CONFIG, API_ENDPOINTS, ENCRYPTION_CONFIG, STORAGE_PATHS, HTTP_STATUS, CONFIG_FILES;
26
26
  var init_constants = __esm({
27
27
  "src/constants.ts"() {
28
28
  "use strict";
@@ -57,11 +57,23 @@ var init_constants = __esm({
57
57
  MIN_PORT: 8e3,
58
58
  MAX_PORT: 9e3
59
59
  };
60
+ ATLASSIAN_OAUTH_CONFIG = {
61
+ CLIENT_ID: "kJlsd66DLTnENE8p7Ru2JwqZg1Sie4yZ",
62
+ AUTH_URL: "https://auth.atlassian.com/authorize",
63
+ AUDIENCE: "api.atlassian.com",
64
+ SCOPES: "read:jira-work read:jira-user read:page:confluence read:confluence-user offline_access read:me",
65
+ ACCESSIBLE_RESOURCES_URL: "https://api.atlassian.com/oauth/token/accessible-resources",
66
+ API_GATEWAY_URL: "https://api.atlassian.com"
67
+ };
60
68
  API_ENDPOINTS = {
61
69
  AUTH: {
62
70
  INITIATE: "/v1/auth/cli/initiate",
63
71
  TOKEN: "/v1/auth/cli/token"
64
72
  },
73
+ ATLASSIAN: {
74
+ TOKEN: "/v1/auth/atlassian/token",
75
+ REFRESH: "/v1/auth/atlassian/refresh"
76
+ },
65
77
  BRAGS: {
66
78
  CREATE: "/v1/brags",
67
79
  LIST: "/v1/brags",
@@ -157,7 +169,7 @@ var init_env_loader = __esm({
157
169
  });
158
170
 
159
171
  // src/utils/errors.ts
160
- var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError;
172
+ var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError, AtlassianError;
161
173
  var init_errors = __esm({
162
174
  "src/utils/errors.ts"() {
163
175
  "use strict";
@@ -248,6 +260,12 @@ var init_errors = __esm({
248
260
  this.name = "ConfluenceError";
249
261
  }
250
262
  };
263
+ AtlassianError = class extends BragduckError {
264
+ constructor(message, details) {
265
+ super(message, "ATLASSIAN_ERROR", details);
266
+ this.name = "AtlassianError";
267
+ }
268
+ };
251
269
  }
252
270
  });
253
271
 
@@ -1241,15 +1259,15 @@ __export(version_exports, {
1241
1259
  getCurrentVersion: () => getCurrentVersion,
1242
1260
  version: () => version
1243
1261
  });
1244
- import { readFileSync as readFileSync3 } from "fs";
1245
- import { fileURLToPath as fileURLToPath4 } from "url";
1246
- import { dirname as dirname3, join as join5 } from "path";
1262
+ import { readFileSync as readFileSync4 } from "fs";
1263
+ import { fileURLToPath as fileURLToPath5 } from "url";
1264
+ import { dirname as dirname4, join as join6 } from "path";
1247
1265
  import chalk4 from "chalk";
1248
1266
  import boxen2 from "boxen";
1249
1267
  function getCurrentVersion() {
1250
1268
  try {
1251
- const packageJsonPath2 = join5(__dirname4, "../../package.json");
1252
- const packageJson2 = JSON.parse(readFileSync3(packageJsonPath2, "utf-8"));
1269
+ const packageJsonPath2 = join6(__dirname5, "../../package.json");
1270
+ const packageJson2 = JSON.parse(readFileSync4(packageJsonPath2, "utf-8"));
1253
1271
  return packageJson2.version;
1254
1272
  } catch {
1255
1273
  logger.debug("Failed to read package.json version");
@@ -1331,7 +1349,7 @@ function formatVersion(includePrefix = true) {
1331
1349
  const prefix = includePrefix ? "v" : "";
1332
1350
  return `${prefix}${version}`;
1333
1351
  }
1334
- var __filename4, __dirname4, version;
1352
+ var __filename5, __dirname5, version;
1335
1353
  var init_version = __esm({
1336
1354
  "src/utils/version.ts"() {
1337
1355
  "use strict";
@@ -1339,22 +1357,22 @@ var init_version = __esm({
1339
1357
  init_api_service();
1340
1358
  init_storage_service();
1341
1359
  init_logger();
1342
- __filename4 = fileURLToPath4(import.meta.url);
1343
- __dirname4 = dirname3(__filename4);
1360
+ __filename5 = fileURLToPath5(import.meta.url);
1361
+ __dirname5 = dirname4(__filename5);
1344
1362
  version = getCurrentVersion();
1345
1363
  }
1346
1364
  });
1347
1365
 
1348
1366
  // src/services/api.service.ts
1349
- import { ofetch as ofetch2 } from "ofetch";
1350
- import { readFileSync as readFileSync4 } from "fs";
1351
- import { fileURLToPath as fileURLToPath5 } from "url";
1352
- import { URLSearchParams as URLSearchParams2 } from "url";
1353
- import { dirname as dirname4, join as join6 } from "path";
1367
+ import { ofetch as ofetch3 } from "ofetch";
1368
+ import { readFileSync as readFileSync5 } from "fs";
1369
+ import { fileURLToPath as fileURLToPath6 } from "url";
1370
+ import { URLSearchParams as URLSearchParams3 } from "url";
1371
+ import { dirname as dirname5, join as join7 } from "path";
1354
1372
  function getCliVersion() {
1355
1373
  try {
1356
- const packageJsonPath2 = join6(__dirname5, "../../package.json");
1357
- const packageJson2 = JSON.parse(readFileSync4(packageJsonPath2, "utf-8"));
1374
+ const packageJsonPath2 = join7(__dirname6, "../../package.json");
1375
+ const packageJson2 = JSON.parse(readFileSync5(packageJsonPath2, "utf-8"));
1358
1376
  return packageJson2.version;
1359
1377
  } catch {
1360
1378
  logger.debug("Failed to read package.json version");
@@ -1366,7 +1384,7 @@ function getPlatformInfo() {
1366
1384
  const arch = process.arch;
1367
1385
  return `${platform}-${arch}`;
1368
1386
  }
1369
- var __filename5, __dirname5, ApiService, apiService;
1387
+ var __filename6, __dirname6, ApiService, apiService;
1370
1388
  var init_api_service = __esm({
1371
1389
  "src/services/api.service.ts"() {
1372
1390
  "use strict";
@@ -1375,14 +1393,14 @@ var init_api_service = __esm({
1375
1393
  init_constants();
1376
1394
  init_errors();
1377
1395
  init_logger();
1378
- __filename5 = fileURLToPath5(import.meta.url);
1379
- __dirname5 = dirname4(__filename5);
1396
+ __filename6 = fileURLToPath6(import.meta.url);
1397
+ __dirname6 = dirname5(__filename6);
1380
1398
  ApiService = class {
1381
1399
  baseURL;
1382
1400
  client;
1383
1401
  constructor() {
1384
1402
  this.baseURL = process.env.API_BASE_URL || "https://api.bragduck.com";
1385
- this.client = ofetch2.create({
1403
+ this.client = ofetch3.create({
1386
1404
  baseURL: this.baseURL,
1387
1405
  // Request interceptor
1388
1406
  onRequest: async ({ request, options }) => {
@@ -1546,7 +1564,7 @@ var init_api_service = __esm({
1546
1564
  const { limit = 50, offset = 0, tags, search } = params;
1547
1565
  logger.debug(`Listing brags: limit=${limit}, offset=${offset}`);
1548
1566
  try {
1549
- const queryParams = new URLSearchParams2({
1567
+ const queryParams = new URLSearchParams3({
1550
1568
  limit: limit.toString(),
1551
1569
  offset: offset.toString()
1552
1570
  });
@@ -1643,7 +1661,7 @@ var init_api_service = __esm({
1643
1661
  */
1644
1662
  setBaseURL(url) {
1645
1663
  this.baseURL = url;
1646
- this.client = ofetch2.create({
1664
+ this.client = ofetch3.create({
1647
1665
  baseURL: url
1648
1666
  });
1649
1667
  }
@@ -1661,18 +1679,267 @@ var init_api_service = __esm({
1661
1679
  // src/cli.ts
1662
1680
  init_esm_shims();
1663
1681
  import { Command } from "commander";
1664
- import { readFileSync as readFileSync5 } from "fs";
1665
- import { fileURLToPath as fileURLToPath6 } from "url";
1666
- import { dirname as dirname5, join as join7 } from "path";
1682
+ import { readFileSync as readFileSync6 } from "fs";
1683
+ import { fileURLToPath as fileURLToPath7 } from "url";
1684
+ import { dirname as dirname6, join as join8 } from "path";
1667
1685
 
1668
1686
  // src/commands/auth.ts
1669
1687
  init_esm_shims();
1670
1688
  init_auth_service();
1671
- init_storage_service();
1672
1689
  import boxen from "boxen";
1673
1690
  import chalk3 from "chalk";
1674
1691
  import { input } from "@inquirer/prompts";
1675
1692
 
1693
+ // src/services/atlassian-auth.service.ts
1694
+ init_esm_shims();
1695
+ init_constants();
1696
+ init_storage_service();
1697
+ init_oauth_server();
1698
+ init_browser();
1699
+ init_errors();
1700
+ init_logger();
1701
+ import { randomBytes as randomBytes3 } from "crypto";
1702
+ import { readFileSync as readFileSync3 } from "fs";
1703
+ import { fileURLToPath as fileURLToPath4, URLSearchParams as URLSearchParams2 } from "url";
1704
+ import { dirname as dirname3, join as join4 } from "path";
1705
+ import { ofetch as ofetch2 } from "ofetch";
1706
+ import { select } from "@inquirer/prompts";
1707
+ var __filename4 = fileURLToPath4(import.meta.url);
1708
+ var __dirname4 = dirname3(__filename4);
1709
+ function getUserAgent2() {
1710
+ try {
1711
+ const packageJsonPath2 = join4(__dirname4, "../../package.json");
1712
+ const packageJson2 = JSON.parse(readFileSync3(packageJsonPath2, "utf-8"));
1713
+ const version2 = packageJson2.version;
1714
+ const platform = process.platform;
1715
+ const arch = process.arch;
1716
+ return `BragDuck-CLI/${version2} (${platform}-${arch})`;
1717
+ } catch {
1718
+ logger.debug("Failed to read package.json version");
1719
+ return "BragDuck-CLI/2.0.0";
1720
+ }
1721
+ }
1722
+ var AtlassianAuthService = class {
1723
+ apiBaseUrl;
1724
+ refreshPromise = null;
1725
+ constructor() {
1726
+ this.apiBaseUrl = process.env.API_BASE_URL || "https://api.bragduck.com";
1727
+ }
1728
+ /**
1729
+ * Generate a random state string for CSRF protection
1730
+ */
1731
+ generateState() {
1732
+ return randomBytes3(32).toString("hex");
1733
+ }
1734
+ /**
1735
+ * Build the Atlassian OAuth authorization URL
1736
+ */
1737
+ buildAuthUrl(state, callbackUrl) {
1738
+ const params = new URLSearchParams2({
1739
+ audience: ATLASSIAN_OAUTH_CONFIG.AUDIENCE,
1740
+ client_id: ATLASSIAN_OAUTH_CONFIG.CLIENT_ID,
1741
+ scope: ATLASSIAN_OAUTH_CONFIG.SCOPES,
1742
+ redirect_uri: callbackUrl,
1743
+ state,
1744
+ response_type: "code",
1745
+ prompt: "consent"
1746
+ });
1747
+ return `${ATLASSIAN_OAUTH_CONFIG.AUTH_URL}?${params.toString()}`;
1748
+ }
1749
+ /**
1750
+ * Exchange authorization code for tokens via BragDuck backend
1751
+ */
1752
+ async exchangeCodeForToken(code, redirectUri) {
1753
+ try {
1754
+ logger.debug("Exchanging Atlassian authorization code for token via backend");
1755
+ const response = await ofetch2(
1756
+ `${this.apiBaseUrl}${API_ENDPOINTS.ATLASSIAN.TOKEN}`,
1757
+ {
1758
+ method: "POST",
1759
+ body: {
1760
+ code,
1761
+ redirect_uri: redirectUri
1762
+ },
1763
+ headers: {
1764
+ "Content-Type": "application/json",
1765
+ "User-Agent": getUserAgent2()
1766
+ }
1767
+ }
1768
+ );
1769
+ logger.debug("Atlassian token exchange successful");
1770
+ return response;
1771
+ } catch (error) {
1772
+ logger.debug(`Atlassian token exchange failed: ${error.message}`);
1773
+ throw new AtlassianError("Failed to exchange Atlassian authorization code", {
1774
+ originalError: error.message
1775
+ });
1776
+ }
1777
+ }
1778
+ /**
1779
+ * Fetch accessible Atlassian Cloud resources (sites) using the access token
1780
+ */
1781
+ async fetchAccessibleResources(accessToken) {
1782
+ try {
1783
+ logger.debug("Fetching accessible Atlassian resources");
1784
+ const resources = await ofetch2(
1785
+ ATLASSIAN_OAUTH_CONFIG.ACCESSIBLE_RESOURCES_URL,
1786
+ {
1787
+ headers: {
1788
+ Authorization: `Bearer ${accessToken}`,
1789
+ Accept: "application/json"
1790
+ }
1791
+ }
1792
+ );
1793
+ logger.debug(`Found ${resources.length} accessible resource(s)`);
1794
+ return resources;
1795
+ } catch (error) {
1796
+ logger.debug(`Failed to fetch accessible resources: ${error.message}`);
1797
+ throw new AtlassianError("Failed to fetch Atlassian Cloud sites", {
1798
+ originalError: error.message
1799
+ });
1800
+ }
1801
+ }
1802
+ /**
1803
+ * Let user pick a site if multiple accessible resources exist
1804
+ */
1805
+ async selectSite(resources) {
1806
+ if (resources.length === 0) {
1807
+ throw new AtlassianError(
1808
+ "No accessible Atlassian Cloud sites found. Make sure your account has access to at least one site."
1809
+ );
1810
+ }
1811
+ if (resources.length === 1) {
1812
+ const site = resources[0];
1813
+ logger.debug(`Auto-selecting single site: ${site.name}`);
1814
+ return site;
1815
+ }
1816
+ const selectedId = await select({
1817
+ message: "Select your Atlassian Cloud site:",
1818
+ choices: resources.map((r) => ({
1819
+ name: `${r.name} (${r.url})`,
1820
+ value: r.id
1821
+ }))
1822
+ });
1823
+ return resources.find((r) => r.id === selectedId);
1824
+ }
1825
+ /**
1826
+ * Full OAuth login flow:
1827
+ * 1. Open browser to Atlassian consent screen
1828
+ * 2. Wait for callback with authorization code
1829
+ * 3. Exchange code for tokens via BragDuck backend
1830
+ * 4. Fetch accessible resources and pick site
1831
+ * 5. Store credentials for jira, confluence, and atlassian services
1832
+ */
1833
+ async login() {
1834
+ logger.debug("Starting Atlassian OAuth login flow");
1835
+ const state = this.generateState();
1836
+ const callbackUrl = await getCallbackUrl();
1837
+ storageService.setOAuthState({
1838
+ state,
1839
+ createdAt: Date.now()
1840
+ });
1841
+ logger.debug(`OAuth state: ${state}`);
1842
+ logger.debug(`Callback URL: ${callbackUrl}`);
1843
+ const authUrl = this.buildAuthUrl(state, callbackUrl);
1844
+ logger.debug(`Authorization URL: ${authUrl}`);
1845
+ const serverPromise = startOAuthCallbackServer(state);
1846
+ try {
1847
+ await openBrowser(authUrl);
1848
+ } catch {
1849
+ logger.warning("Could not open browser automatically");
1850
+ logger.info("Please open this URL in your browser:");
1851
+ logger.log(authUrl);
1852
+ }
1853
+ let callbackResult;
1854
+ try {
1855
+ callbackResult = await serverPromise;
1856
+ } catch (error) {
1857
+ storageService.deleteOAuthState();
1858
+ throw error;
1859
+ }
1860
+ storageService.deleteOAuthState();
1861
+ const tokenResponse = await this.exchangeCodeForToken(callbackResult.code, callbackUrl);
1862
+ const resources = await this.fetchAccessibleResources(tokenResponse.access_token);
1863
+ const site = await this.selectSite(resources);
1864
+ const expiresAt = tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : void 0;
1865
+ const credentials = {
1866
+ accessToken: tokenResponse.access_token,
1867
+ refreshToken: tokenResponse.refresh_token,
1868
+ expiresAt,
1869
+ instanceUrl: site.url,
1870
+ // Real Cloud URL for browse links
1871
+ cloudId: site.id,
1872
+ authMethod: "oauth"
1873
+ };
1874
+ await storageService.setServiceCredentials("jira", credentials);
1875
+ await storageService.setServiceCredentials("confluence", credentials);
1876
+ await storageService.setServiceCredentials("atlassian", credentials);
1877
+ logger.debug("Atlassian OAuth login successful");
1878
+ return { siteName: site.name, siteUrl: site.url };
1879
+ }
1880
+ /**
1881
+ * Refresh Atlassian OAuth tokens via BragDuck backend.
1882
+ * Uses a singleton promise to prevent concurrent refresh race conditions
1883
+ * (Atlassian uses rotating refresh tokens - each refresh invalidates the previous one).
1884
+ */
1885
+ async refreshToken() {
1886
+ if (this.refreshPromise) {
1887
+ return this.refreshPromise;
1888
+ }
1889
+ this.refreshPromise = this.doRefreshToken();
1890
+ try {
1891
+ await this.refreshPromise;
1892
+ } finally {
1893
+ this.refreshPromise = null;
1894
+ }
1895
+ }
1896
+ async doRefreshToken() {
1897
+ logger.debug("Refreshing Atlassian OAuth token");
1898
+ const creds = await storageService.getServiceCredentials("jira");
1899
+ if (!creds?.refreshToken) {
1900
+ throw new AtlassianError("No Atlassian refresh token available", {
1901
+ hint: "Run: bragduck auth atlassian"
1902
+ });
1903
+ }
1904
+ try {
1905
+ const response = await ofetch2(
1906
+ `${this.apiBaseUrl}${API_ENDPOINTS.ATLASSIAN.REFRESH}`,
1907
+ {
1908
+ method: "POST",
1909
+ body: {
1910
+ refresh_token: creds.refreshToken
1911
+ },
1912
+ headers: {
1913
+ "Content-Type": "application/json",
1914
+ "User-Agent": getUserAgent2()
1915
+ }
1916
+ }
1917
+ );
1918
+ const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1e3 : void 0;
1919
+ const updatedCreds = {
1920
+ ...creds,
1921
+ accessToken: response.access_token,
1922
+ refreshToken: response.refresh_token,
1923
+ expiresAt
1924
+ };
1925
+ await storageService.setServiceCredentials("jira", updatedCreds);
1926
+ await storageService.setServiceCredentials("confluence", updatedCreds);
1927
+ await storageService.setServiceCredentials("atlassian", updatedCreds);
1928
+ logger.debug("Atlassian token refresh successful");
1929
+ } catch (error) {
1930
+ logger.debug(`Atlassian token refresh failed: ${error.message}`);
1931
+ throw new AtlassianError(
1932
+ 'Atlassian token refresh failed. Please run "bragduck auth atlassian" to re-authenticate.',
1933
+ { originalError: error.message }
1934
+ );
1935
+ }
1936
+ }
1937
+ };
1938
+ var atlassianAuthService = new AtlassianAuthService();
1939
+
1940
+ // src/commands/auth.ts
1941
+ init_storage_service();
1942
+
1676
1943
  // src/services/github.service.ts
1677
1944
  init_esm_shims();
1678
1945
  init_errors();
@@ -1688,9 +1955,9 @@ import simpleGit from "simple-git";
1688
1955
  init_esm_shims();
1689
1956
  init_errors();
1690
1957
  import { existsSync as existsSync2 } from "fs";
1691
- import { join as join4 } from "path";
1958
+ import { join as join5 } from "path";
1692
1959
  function validateGitRepository(path4) {
1693
- const gitDir = join4(path4, ".git");
1960
+ const gitDir = join5(path4, ".git");
1694
1961
  if (!existsSync2(gitDir)) {
1695
1962
  throw new GitError(
1696
1963
  "Not a git repository. Please run this command from within a git repository.",
@@ -2404,7 +2671,14 @@ async function authStatus() {
2404
2671
  }
2405
2672
  for (const service of services) {
2406
2673
  if (service !== "bragduck") {
2407
- logger.info(`${colors.success("\u2713")} ${service}: Authenticated`);
2674
+ const creds = await storageService.getServiceCredentials(service);
2675
+ let methodLabel = "";
2676
+ if (creds?.authMethod === "oauth") {
2677
+ methodLabel = " (Cloud - OAuth)";
2678
+ } else if (creds?.authMethod === "basic") {
2679
+ methodLabel = " (Server - API Token)";
2680
+ }
2681
+ logger.info(`${colors.success("\u2713")} ${service}: Authenticated${methodLabel}`);
2408
2682
  }
2409
2683
  }
2410
2684
  logger.log("");
@@ -2556,68 +2830,21 @@ User: ${user.name} (@${user.username})`,
2556
2830
  }
2557
2831
  async function authAtlassian() {
2558
2832
  logger.log("");
2559
- logger.log(
2560
- boxen(
2561
- theme.info("Atlassian API Token Authentication") + "\n\nThis token works for Jira, Confluence, and Bitbucket\n\nCreate an API Token at:\n" + colors.highlight("https://id.atlassian.com/manage-profile/security/api-tokens") + "\n\nRequired access:\n \u2022 Jira: Read issues\n \u2022 Confluence: Read pages\n \u2022 Bitbucket: Read repositories",
2562
- boxStyles.info
2563
- )
2564
- );
2833
+ logger.info("Opening browser for Atlassian Cloud authentication...");
2565
2834
  logger.log("");
2566
2835
  try {
2567
- const instanceUrl = await input({
2568
- message: "Atlassian instance URL (e.g., company.atlassian.net):",
2569
- validate: (value) => value.length > 0 ? true : "Instance URL cannot be empty"
2570
- });
2571
- const email = await input({
2572
- message: "Atlassian account email:",
2573
- validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
2574
- });
2575
- const apiToken = await input({
2576
- message: "API Token:",
2577
- validate: (value) => value.length > 0 ? true : "API token cannot be empty"
2578
- });
2579
- const testInstanceUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
2580
- const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
2581
- const response = await fetch(`${testInstanceUrl}/rest/api/2/myself`, {
2582
- headers: { Authorization: `Basic ${auth}` }
2583
- });
2584
- if (!response.ok) {
2585
- logger.log("");
2586
- logger.log(
2587
- boxen(
2588
- theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL, email, or API token\nMake sure the instance URL is correct (e.g., company.atlassian.net)",
2589
- boxStyles.error
2590
- )
2591
- );
2592
- logger.log("");
2593
- process.exit(1);
2594
- }
2595
- const user = await response.json();
2596
- const credentials = {
2597
- accessToken: apiToken,
2598
- username: email,
2599
- instanceUrl: instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`
2600
- };
2601
- await storageService.setServiceCredentials("jira", credentials);
2602
- await storageService.setServiceCredentials("confluence", credentials);
2603
- await storageService.setServiceCredentials("bitbucket", {
2604
- ...credentials,
2605
- instanceUrl: "https://api.bitbucket.org"
2606
- // Bitbucket uses different API base
2607
- });
2836
+ const result = await atlassianAuthService.login();
2608
2837
  logger.log("");
2609
2838
  logger.log(
2610
2839
  boxen(
2611
- theme.success("\u2713 Successfully authenticated with Atlassian") + `
2840
+ theme.success("\u2713 Successfully authenticated with Atlassian Cloud") + `
2612
2841
 
2613
- Instance: ${instanceUrl}
2614
- User: ${user.displayName}
2615
- Email: ${user.emailAddress}
2842
+ Site: ${result.siteName}
2843
+ URL: ${result.siteUrl}
2616
2844
 
2617
2845
  Services configured:
2618
- \u2022 Jira
2619
- \u2022 Confluence
2620
- \u2022 Bitbucket`,
2846
+ - Jira (OAuth)
2847
+ - Confluence (OAuth)`,
2621
2848
  boxStyles.success
2622
2849
  )
2623
2850
  );
@@ -2650,7 +2877,7 @@ var CancelPromptError = class extends Error {
2650
2877
  init_api_service();
2651
2878
  init_storage_service();
2652
2879
  init_auth_service();
2653
- import { select as select3 } from "@inquirer/prompts";
2880
+ import { select as select4 } from "@inquirer/prompts";
2654
2881
  import boxen6 from "boxen";
2655
2882
 
2656
2883
  // src/utils/source-detector.ts
@@ -2659,7 +2886,7 @@ init_errors();
2659
2886
  init_storage_service();
2660
2887
  import { exec as exec3 } from "child_process";
2661
2888
  import { promisify as promisify3 } from "util";
2662
- import { select } from "@inquirer/prompts";
2889
+ import { select as select2 } from "@inquirer/prompts";
2663
2890
  var execAsync3 = promisify3(exec3);
2664
2891
  var SourceDetector = class {
2665
2892
  /**
@@ -2715,7 +2942,7 @@ var SourceDetector = class {
2715
2942
  description: source.remoteUrl
2716
2943
  };
2717
2944
  });
2718
- return await select({
2945
+ return await select2({
2719
2946
  message: "Multiple sources detected. Which do you want to sync?",
2720
2947
  choices
2721
2948
  });
@@ -3231,7 +3458,7 @@ init_logger();
3231
3458
  init_storage_service();
3232
3459
  import { exec as exec5 } from "child_process";
3233
3460
  import { promisify as promisify5 } from "util";
3234
- import { URLSearchParams as URLSearchParams3 } from "url";
3461
+ import { URLSearchParams as URLSearchParams4 } from "url";
3235
3462
  var execAsync5 = promisify5(exec5);
3236
3463
  var GitLabService = class {
3237
3464
  DEFAULT_INSTANCE = "https://gitlab.com";
@@ -3377,7 +3604,7 @@ var GitLabService = class {
3377
3604
  async getMergedMRs(options = {}) {
3378
3605
  const { projectPath } = await this.getProjectFromGit();
3379
3606
  const encodedPath = encodeURIComponent(projectPath);
3380
- const params = new URLSearchParams3({
3607
+ const params = new URLSearchParams4({
3381
3608
  state: "merged",
3382
3609
  order_by: "updated_at",
3383
3610
  sort: "desc",
@@ -3543,41 +3770,76 @@ init_esm_shims();
3543
3770
  init_errors();
3544
3771
  init_logger();
3545
3772
  init_storage_service();
3773
+ init_constants();
3546
3774
  var JiraService = class {
3547
3775
  MAX_DESCRIPTION_LENGTH = 5e3;
3548
3776
  /**
3549
- * Get stored Jira credentials
3777
+ * Get stored Jira credentials, auto-refreshing OAuth tokens if expired
3550
3778
  */
3551
3779
  async getCredentials() {
3552
3780
  const creds = await storageService.getServiceCredentials("jira");
3553
- if (!creds || !creds.username || !creds.accessToken || !creds.instanceUrl) {
3781
+ if (!creds || !creds.accessToken) {
3554
3782
  throw new JiraError("Not authenticated with Jira", {
3555
3783
  hint: "Run: bragduck auth atlassian"
3556
3784
  });
3557
3785
  }
3558
- if (creds.expiresAt && creds.expiresAt < Date.now()) {
3786
+ if (creds.authMethod !== "oauth" && (!creds.username || !creds.instanceUrl)) {
3787
+ throw new JiraError("Not authenticated with Jira", {
3788
+ hint: "Run: bragduck auth atlassian"
3789
+ });
3790
+ }
3791
+ if (creds.authMethod === "oauth" && !creds.cloudId) {
3792
+ throw new JiraError("Missing Atlassian Cloud ID", {
3793
+ hint: "Run: bragduck auth atlassian"
3794
+ });
3795
+ }
3796
+ if (creds.authMethod === "oauth" && creds.expiresAt && creds.expiresAt < Date.now()) {
3797
+ if (creds.refreshToken) {
3798
+ logger.debug("Jira OAuth token expired, refreshing...");
3799
+ await atlassianAuthService.refreshToken();
3800
+ const refreshed = await storageService.getServiceCredentials("jira");
3801
+ if (!refreshed?.accessToken) {
3802
+ throw new JiraError("Token refresh failed", {
3803
+ hint: "Run: bragduck auth atlassian"
3804
+ });
3805
+ }
3806
+ return refreshed;
3807
+ }
3808
+ throw new JiraError("OAuth token has expired and no refresh token available", {
3809
+ hint: "Run: bragduck auth atlassian"
3810
+ });
3811
+ }
3812
+ if (creds.authMethod !== "oauth" && creds.expiresAt && creds.expiresAt < Date.now()) {
3559
3813
  throw new JiraError("API token has expired", {
3560
3814
  hint: "Run: bragduck auth atlassian"
3561
3815
  });
3562
3816
  }
3563
- return {
3564
- email: creds.username,
3565
- apiToken: creds.accessToken,
3566
- instanceUrl: creds.instanceUrl
3567
- };
3817
+ return creds;
3568
3818
  }
3569
3819
  /**
3570
- * Make authenticated request to Jira API
3820
+ * Make authenticated request to Jira API.
3821
+ * Uses OAuth Bearer + API gateway for Cloud, Basic Auth for Server/DC.
3822
+ * Retries once on 401 for OAuth (auto-refresh).
3571
3823
  */
3572
3824
  async request(endpoint, method = "GET", body) {
3573
- const { email, apiToken, instanceUrl } = await this.getCredentials();
3574
- const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
3575
- const baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
3825
+ const creds = await this.getCredentials();
3826
+ const isOAuth = creds.authMethod === "oauth";
3827
+ let baseUrl;
3828
+ let authHeader;
3829
+ if (isOAuth) {
3830
+ baseUrl = `${ATLASSIAN_OAUTH_CONFIG.API_GATEWAY_URL}/ex/jira/${creds.cloudId}`;
3831
+ authHeader = `Bearer ${creds.accessToken}`;
3832
+ } else {
3833
+ const instanceUrl = creds.instanceUrl;
3834
+ baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
3835
+ const auth = Buffer.from(`${creds.username}:${creds.accessToken}`).toString("base64");
3836
+ authHeader = `Basic ${auth}`;
3837
+ }
3576
3838
  logger.debug(`Jira API: ${method} ${endpoint}`);
3577
3839
  const options = {
3578
3840
  method,
3579
3841
  headers: {
3580
- Authorization: `Basic ${auth}`,
3842
+ Authorization: authHeader,
3581
3843
  "Content-Type": "application/json",
3582
3844
  Accept: "application/json"
3583
3845
  }
@@ -3585,7 +3847,17 @@ var JiraService = class {
3585
3847
  if (body) {
3586
3848
  options.body = JSON.stringify(body);
3587
3849
  }
3588
- const response = await fetch(`${baseUrl}${endpoint}`, options);
3850
+ let response = await fetch(`${baseUrl}${endpoint}`, options);
3851
+ if (response.status === 401 && isOAuth && creds.refreshToken) {
3852
+ logger.debug("Jira OAuth 401, attempting token refresh and retry");
3853
+ await atlassianAuthService.refreshToken();
3854
+ const refreshedCreds = await storageService.getServiceCredentials("jira");
3855
+ if (refreshedCreds?.accessToken) {
3856
+ options.headers.Authorization = `Bearer ${refreshedCreds.accessToken}`;
3857
+ const newBaseUrl = `${ATLASSIAN_OAUTH_CONFIG.API_GATEWAY_URL}/ex/jira/${refreshedCreds.cloudId}`;
3858
+ response = await fetch(`${newBaseUrl}${endpoint}`, options);
3859
+ }
3860
+ }
3589
3861
  if (!response.ok) {
3590
3862
  const statusText = response.statusText;
3591
3863
  const status = response.status;
@@ -3653,33 +3925,6 @@ var JiraService = class {
3653
3925
  return null;
3654
3926
  }
3655
3927
  }
3656
- /**
3657
- * Check if a user object matches the given identifier (accountId, email, or username)
3658
- */
3659
- isMatchingUser(candidate, userIdentifier) {
3660
- if (!candidate) return false;
3661
- return candidate.email === userIdentifier || candidate.emailAddress === userIdentifier || candidate.accountId === userIdentifier || candidate.username === userIdentifier || candidate.name === userIdentifier;
3662
- }
3663
- /**
3664
- * Filter issues to only those where the user made contributions within the date range.
3665
- * Excludes issues where the user's only involvement is a static role (creator/assignee)
3666
- * with a date outside the scan period.
3667
- */
3668
- filterIssuesByUserContribution(issues, userIdentifier, sinceDate) {
3669
- const results = [];
3670
- for (const issue of issues) {
3671
- const userChanges = (issue.changelog?.histories || []).filter(
3672
- (h) => this.isMatchingUser(h.author, userIdentifier) && new Date(h.created) >= sinceDate
3673
- );
3674
- const isCreatorInRange = this.isMatchingUser(issue.fields.creator, userIdentifier) && new Date(issue.fields.created) >= sinceDate;
3675
- if (userChanges.length > 0 || isCreatorInRange) {
3676
- results.push({ issue, userChanges });
3677
- } else {
3678
- logger.debug(`Excluding issue ${issue.key} - no user contributions in date range`);
3679
- }
3680
- }
3681
- return results;
3682
- }
3683
3928
  /**
3684
3929
  * Build JQL query from options
3685
3930
  */
@@ -3737,7 +3982,7 @@ var JiraService = class {
3737
3982
  }
3738
3983
  const isAssigned = issue.fields.assignee?.emailAddress === userEmail;
3739
3984
  const isResolved = issue.fields.resolutiondate !== null && issue.fields.resolutiondate !== void 0;
3740
- const userEdits = issue.changelog?.histories?.filter((history) => history.author?.emailAddress === userEmail) || [];
3985
+ const userEdits = issue.changelog?.histories?.filter((history) => history.author.emailAddress === userEmail) || [];
3741
3986
  const hasEdits = userEdits.length > 0;
3742
3987
  if (isAssigned && isResolved) {
3743
3988
  return {
@@ -3772,77 +4017,6 @@ var JiraService = class {
3772
4017
  };
3773
4018
  return Math.ceil(baseComplexity * multipliers[contributionType]);
3774
4019
  }
3775
- /**
3776
- * Summarize the user's specific changes from changelog entries into human-readable lines.
3777
- * This enriches the brag message so the AI refinement can generate a more specific brag.
3778
- */
3779
- summarizeUserChanges(userChanges) {
3780
- const MAX_LINES = 10;
3781
- const allItems = [];
3782
- for (const entry of userChanges) {
3783
- for (const item of entry.items) {
3784
- allItems.push(item);
3785
- }
3786
- }
3787
- if (allItems.length === 0) return "";
3788
- const latestByField = /* @__PURE__ */ new Map();
3789
- for (const item of allItems) {
3790
- latestByField.set(item.field, { fromString: item.fromString, toString: item.toString });
3791
- }
3792
- const lines = [];
3793
- for (const [field, change] of latestByField) {
3794
- if (lines.length >= MAX_LINES) break;
3795
- const from = change.fromString || "";
3796
- const to = change.toString || "";
3797
- switch (field.toLowerCase()) {
3798
- case "status":
3799
- lines.push(from ? `Moved status from '${from}' to '${to}'` : `Set status to '${to}'`);
3800
- break;
3801
- case "resolution":
3802
- lines.push(to ? `Resolved as '${to}'` : "Reopened issue");
3803
- break;
3804
- case "assignee":
3805
- lines.push(to ? `Assigned to ${to}` : "Unassigned");
3806
- break;
3807
- case "priority":
3808
- lines.push(
3809
- from ? `Changed priority from '${from}' to '${to}'` : `Set priority to '${to}'`
3810
- );
3811
- break;
3812
- case "summary":
3813
- lines.push("Updated issue title");
3814
- break;
3815
- case "description":
3816
- lines.push("Updated description");
3817
- break;
3818
- case "comment":
3819
- lines.push("Added comment");
3820
- break;
3821
- case "labels":
3822
- lines.push(to ? `Updated labels: ${to}` : "Removed labels");
3823
- break;
3824
- case "fix version":
3825
- case "fixversions":
3826
- lines.push(to ? `Set fix version: ${to}` : "Removed fix version");
3827
- break;
3828
- case "sprint":
3829
- lines.push(to ? `Moved to sprint: ${to}` : "Removed from sprint");
3830
- break;
3831
- case "story points":
3832
- case "story point estimate":
3833
- lines.push(`Set story points to ${to}`);
3834
- break;
3835
- default:
3836
- if (to) {
3837
- lines.push(`Updated ${field}`);
3838
- }
3839
- break;
3840
- }
3841
- }
3842
- if (lines.length === 0) return "";
3843
- return `User changes:
3844
- ${lines.map((l) => `- ${l}`).join("\n")}`;
3845
- }
3846
4020
  /**
3847
4021
  * Fetch issues with optional filtering
3848
4022
  */
@@ -3880,7 +4054,7 @@ ${lines.map((l) => `- ${l}`).join("\n")}`;
3880
4054
  );
3881
4055
  break;
3882
4056
  }
3883
- const endpoint = `/rest/api/3/search/jql?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=${fields.join(",")}&expand=changelog`;
4057
+ const endpoint = `/rest/api/3/search/jql?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=${fields.join(",")}`;
3884
4058
  try {
3885
4059
  const response = await this.request(endpoint);
3886
4060
  if (response.issues.length === 0) {
@@ -3906,7 +4080,14 @@ ${lines.map((l) => `- ${l}`).join("\n")}`;
3906
4080
  break;
3907
4081
  }
3908
4082
  if (options.limit && allIssues.length >= options.limit) {
3909
- break;
4083
+ const email2 = await this.getCurrentUser();
4084
+ const limitedIssues = allIssues.slice(0, options.limit);
4085
+ const commits2 = [];
4086
+ for (const issue of limitedIssues) {
4087
+ const commit = await this.transformIssueToCommit(issue, void 0, email2 || void 0);
4088
+ commits2.push(commit);
4089
+ }
4090
+ return commits2;
3910
4091
  }
3911
4092
  startAt += maxResults;
3912
4093
  } catch (error) {
@@ -3930,23 +4111,9 @@ ${lines.map((l) => `- ${l}`).join("\n")}`;
3930
4111
  throw error;
3931
4112
  }
3932
4113
  }
3933
- const issuesToProcess = options.limit ? allIssues.slice(0, options.limit) : allIssues;
3934
4114
  const email = await this.getCurrentUser();
3935
- const sinceDate = options.days ? new Date(Date.now() - options.days * 24 * 60 * 60 * 1e3) : void 0;
3936
- if (sinceDate && email) {
3937
- const filtered = this.filterIssuesByUserContribution(issuesToProcess, email, sinceDate);
3938
- logger.debug(
3939
- `Date-scoped filtering: ${issuesToProcess.length} issues -> ${filtered.length} with user contributions in range`
3940
- );
3941
- const commits2 = [];
3942
- for (const { issue, userChanges } of filtered) {
3943
- const commit = await this.transformIssueToCommit(issue, void 0, email, userChanges);
3944
- commits2.push(commit);
3945
- }
3946
- return commits2;
3947
- }
3948
4115
  const commits = [];
3949
- for (const issue of issuesToProcess) {
4116
+ for (const issue of allIssues) {
3950
4117
  const commit = await this.transformIssueToCommit(issue, void 0, email || void 0);
3951
4118
  commits.push(commit);
3952
4119
  }
@@ -3968,7 +4135,7 @@ ${lines.map((l) => `- ${l}`).join("\n")}`;
3968
4135
  /**
3969
4136
  * Transform Jira issue to GitCommit format with contribution-specific data
3970
4137
  */
3971
- async transformIssueToCommit(issue, instanceUrl, userEmail, userChanges) {
4138
+ async transformIssueToCommit(issue, instanceUrl, userEmail) {
3972
4139
  let contribution = null;
3973
4140
  if (userEmail) {
3974
4141
  contribution = await this.determineJiraContributionType(issue, userEmail);
@@ -4005,21 +4172,11 @@ ${contribution.details}`;
4005
4172
  ${truncatedDesc}`;
4006
4173
  }
4007
4174
  }
4008
- if (userChanges && userChanges.length > 0) {
4009
- const changeSummary = this.summarizeUserChanges(userChanges);
4010
- if (changeSummary) {
4011
- message += `
4012
-
4013
- ${changeSummary}`;
4014
- }
4015
- }
4016
4175
  let date;
4017
4176
  if (contribution?.type === "created" || contribution?.type === "reported") {
4018
4177
  date = issue.fields.created;
4019
4178
  } else if (contribution?.type === "assigned-resolved" && issue.fields.resolutiondate) {
4020
4179
  date = issue.fields.resolutiondate;
4021
- } else if (userChanges && userChanges.length > 0) {
4022
- date = userChanges[userChanges.length - 1].created;
4023
4180
  } else {
4024
4181
  date = issue.fields.updated;
4025
4182
  }
@@ -4106,40 +4263,75 @@ init_esm_shims();
4106
4263
  init_errors();
4107
4264
  init_logger();
4108
4265
  init_storage_service();
4266
+ init_constants();
4109
4267
  var ConfluenceService = class {
4110
4268
  /**
4111
- * Get stored Confluence credentials
4269
+ * Get stored Confluence credentials, auto-refreshing OAuth tokens if expired
4112
4270
  */
4113
4271
  async getCredentials() {
4114
4272
  const creds = await storageService.getServiceCredentials("confluence");
4115
- if (!creds || !creds.username || !creds.accessToken || !creds.instanceUrl) {
4273
+ if (!creds || !creds.accessToken) {
4116
4274
  throw new ConfluenceError("Not authenticated with Confluence", {
4117
4275
  hint: "Run: bragduck auth atlassian"
4118
4276
  });
4119
4277
  }
4120
- if (creds.expiresAt && creds.expiresAt < Date.now()) {
4278
+ if (creds.authMethod !== "oauth" && (!creds.username || !creds.instanceUrl)) {
4279
+ throw new ConfluenceError("Not authenticated with Confluence", {
4280
+ hint: "Run: bragduck auth atlassian"
4281
+ });
4282
+ }
4283
+ if (creds.authMethod === "oauth" && !creds.cloudId) {
4284
+ throw new ConfluenceError("Missing Atlassian Cloud ID", {
4285
+ hint: "Run: bragduck auth atlassian"
4286
+ });
4287
+ }
4288
+ if (creds.authMethod === "oauth" && creds.expiresAt && creds.expiresAt < Date.now()) {
4289
+ if (creds.refreshToken) {
4290
+ logger.debug("Confluence OAuth token expired, refreshing...");
4291
+ await atlassianAuthService.refreshToken();
4292
+ const refreshed = await storageService.getServiceCredentials("confluence");
4293
+ if (!refreshed?.accessToken) {
4294
+ throw new ConfluenceError("Token refresh failed", {
4295
+ hint: "Run: bragduck auth atlassian"
4296
+ });
4297
+ }
4298
+ return refreshed;
4299
+ }
4300
+ throw new ConfluenceError("OAuth token has expired and no refresh token available", {
4301
+ hint: "Run: bragduck auth atlassian"
4302
+ });
4303
+ }
4304
+ if (creds.authMethod !== "oauth" && creds.expiresAt && creds.expiresAt < Date.now()) {
4121
4305
  throw new ConfluenceError("API token has expired", {
4122
4306
  hint: "Run: bragduck auth atlassian"
4123
4307
  });
4124
4308
  }
4125
- return {
4126
- email: creds.username,
4127
- apiToken: creds.accessToken,
4128
- instanceUrl: creds.instanceUrl
4129
- };
4309
+ return creds;
4130
4310
  }
4131
4311
  /**
4132
- * Make authenticated request to Confluence API
4312
+ * Make authenticated request to Confluence API.
4313
+ * Uses OAuth Bearer + API gateway for Cloud, Basic Auth for Server/DC.
4314
+ * Retries once on 401 for OAuth (auto-refresh).
4133
4315
  */
4134
4316
  async request(endpoint, method = "GET", body) {
4135
- const { email, apiToken, instanceUrl } = await this.getCredentials();
4136
- const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
4137
- const baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
4317
+ const creds = await this.getCredentials();
4318
+ const isOAuth = creds.authMethod === "oauth";
4319
+ let baseUrl;
4320
+ let authHeader;
4321
+ if (isOAuth) {
4322
+ baseUrl = `${ATLASSIAN_OAUTH_CONFIG.API_GATEWAY_URL}/ex/confluence/${creds.cloudId}`;
4323
+ authHeader = `Bearer ${creds.accessToken}`;
4324
+ } else {
4325
+ const instanceUrl = creds.instanceUrl;
4326
+ baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
4327
+ const auth = Buffer.from(`${creds.username}:${creds.accessToken}`).toString("base64");
4328
+ authHeader = `Basic ${auth}`;
4329
+ }
4138
4330
  logger.debug(`Confluence API: ${method} ${endpoint}`);
4139
4331
  const options = {
4140
4332
  method,
4141
4333
  headers: {
4142
- Authorization: `Basic ${auth}`,
4334
+ Authorization: authHeader,
4143
4335
  "Content-Type": "application/json",
4144
4336
  Accept: "application/json"
4145
4337
  }
@@ -4147,7 +4339,17 @@ var ConfluenceService = class {
4147
4339
  if (body) {
4148
4340
  options.body = JSON.stringify(body);
4149
4341
  }
4150
- const response = await fetch(`${baseUrl}${endpoint}`, options);
4342
+ let response = await fetch(`${baseUrl}${endpoint}`, options);
4343
+ if (response.status === 401 && isOAuth && creds.refreshToken) {
4344
+ logger.debug("Confluence OAuth 401, attempting token refresh and retry");
4345
+ await atlassianAuthService.refreshToken();
4346
+ const refreshedCreds = await storageService.getServiceCredentials("confluence");
4347
+ if (refreshedCreds?.accessToken) {
4348
+ options.headers.Authorization = `Bearer ${refreshedCreds.accessToken}`;
4349
+ const newBaseUrl = `${ATLASSIAN_OAUTH_CONFIG.API_GATEWAY_URL}/ex/confluence/${refreshedCreds.cloudId}`;
4350
+ response = await fetch(`${newBaseUrl}${endpoint}`, options);
4351
+ }
4352
+ }
4151
4353
  if (!response.ok) {
4152
4354
  const statusText = response.statusText;
4153
4355
  const status = response.status;
@@ -4209,60 +4411,6 @@ var ConfluenceService = class {
4209
4411
  return null;
4210
4412
  }
4211
4413
  }
4212
- /**
4213
- * Check if a user object matches the given identifier (accountId, email, or username)
4214
- */
4215
- isMatchingUser(candidate, userIdentifier) {
4216
- if (!candidate) return false;
4217
- return candidate.email === userIdentifier || candidate.emailAddress === userIdentifier || candidate.accountId === userIdentifier || candidate.username === userIdentifier || candidate.name === userIdentifier;
4218
- }
4219
- /**
4220
- * Fetch full version history for a page
4221
- */
4222
- async getPageVersionHistory(pageId) {
4223
- const allVersions = [];
4224
- let start = 0;
4225
- const limit = 50;
4226
- while (true) {
4227
- const response = await this.request(
4228
- `/wiki/rest/api/content/${pageId}/version?start=${start}&limit=${limit}`
4229
- );
4230
- allVersions.push(...response.results);
4231
- if (response.size < limit) break;
4232
- start += limit;
4233
- }
4234
- return allVersions;
4235
- }
4236
- /**
4237
- * Filter pages to only those where the user made contributions within the date range.
4238
- * Fetches version history per page and checks if the user has versions in range.
4239
- */
4240
- async filterPagesByUserContribution(pages, userIdentifier, sinceDate) {
4241
- const results = [];
4242
- for (let i = 0; i < pages.length; i++) {
4243
- const page = pages[i];
4244
- if (i > 0) {
4245
- await new Promise((resolve) => globalThis.setTimeout(resolve, 100));
4246
- }
4247
- try {
4248
- const versions = await this.getPageVersionHistory(page.id);
4249
- const userVersions = versions.filter(
4250
- (v) => this.isMatchingUser(v.by, userIdentifier) && new Date(v.when) >= sinceDate
4251
- );
4252
- const userCommentsInRange = page.children?.comment?.results?.filter(
4253
- (comment) => this.isMatchingUser(comment.version?.by || {}, userIdentifier) && new Date(comment.version?.when || 0) >= sinceDate
4254
- ) || [];
4255
- if (userVersions.length > 0 || userCommentsInRange.length > 0) {
4256
- results.push({ page, userVersions });
4257
- } else {
4258
- logger.debug(`Excluding page "${page.title}" - no user contributions in date range`);
4259
- }
4260
- } catch (error) {
4261
- logger.debug(`Skipping version history for page ${page.id}: ${error}`);
4262
- }
4263
- }
4264
- return results;
4265
- }
4266
4414
  /**
4267
4415
  * Build CQL query from options
4268
4416
  * Returns empty string if no filters need CQL (will use simple endpoint instead)
@@ -4298,31 +4446,28 @@ var ConfluenceService = class {
4298
4446
  * Determine the type of contribution the current user made to a page
4299
4447
  * Returns: 'created' | 'edited' | 'commented'
4300
4448
  */
4301
- async determineContributionType(page, userEmail, userVersions) {
4302
- const createdByUser = page.history?.createdBy ? this.isMatchingUser(page.history.createdBy, userEmail) : false;
4303
- if (createdByUser) {
4449
+ async determineContributionType(page, userEmail) {
4450
+ if (page.history?.createdBy?.email === userEmail) {
4304
4451
  return {
4305
4452
  type: "created",
4306
4453
  details: `Created page with ${page.version?.number || 1} version${(page.version?.number || 1) > 1 ? "s" : ""}`
4307
4454
  };
4308
4455
  }
4309
- 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;
4456
+ const hasEdits = page.version?.by?.email === userEmail && (page.version?.number || 0) > 1;
4310
4457
  const userComments = page.children?.comment?.results?.filter(
4311
- (comment) => comment.version?.by ? this.isMatchingUser(comment.version.by, userEmail) : false
4458
+ (comment) => comment.version?.by?.email === userEmail
4312
4459
  ) || [];
4313
4460
  const hasComments = userComments.length > 0;
4314
4461
  if (hasEdits && hasComments) {
4315
- const editCount = userVersions?.length || 1;
4316
4462
  return {
4317
4463
  type: "edited",
4318
- details: `Edited page (${editCount} edit${editCount > 1 ? "s" : ""}) and added ${userComments.length} comment${userComments.length > 1 ? "s" : ""}`
4464
+ details: `Edited page (v${page.version?.number || 1}) and added ${userComments.length} comment${userComments.length > 1 ? "s" : ""}`
4319
4465
  };
4320
4466
  }
4321
4467
  if (hasEdits) {
4322
- const editCount = userVersions?.length || 1;
4323
4468
  return {
4324
4469
  type: "edited",
4325
- details: `Edited page (${editCount} edit${editCount > 1 ? "s" : ""})`
4470
+ details: `Edited page to version ${page.version?.number || 1}`
4326
4471
  };
4327
4472
  }
4328
4473
  if (hasComments) {
@@ -4350,31 +4495,6 @@ var ConfluenceService = class {
4350
4495
  };
4351
4496
  return Math.ceil(baseSize * multipliers[contributionType]);
4352
4497
  }
4353
- /**
4354
- * Summarize the user's version edits into human-readable lines.
4355
- * This enriches the brag message so the AI refinement can generate a more specific brag.
4356
- */
4357
- summarizeUserVersions(userVersions) {
4358
- const MAX_ENTRIES = 5;
4359
- const versionsWithMessages = userVersions.filter((v) => v.message && v.message.trim());
4360
- if (versionsWithMessages.length > 0) {
4361
- const lines = versionsWithMessages.slice(0, MAX_ENTRIES).map((v) => {
4362
- const suffix = v.minorEdit ? " (minor edit)" : "";
4363
- return `- v${v.number}: ${v.message.trim()}${suffix}`;
4364
- });
4365
- return `Edit notes:
4366
- ${lines.join("\n")}`;
4367
- }
4368
- if (userVersions.length > 0) {
4369
- const major = userVersions.filter((v) => !v.minorEdit).length;
4370
- const minor = userVersions.filter((v) => v.minorEdit).length;
4371
- const parts = [];
4372
- if (major > 0) parts.push(`${major} major`);
4373
- if (minor > 0) parts.push(`${minor} minor`);
4374
- return `Made ${userVersions.length} edit${userVersions.length > 1 ? "s" : ""} to this page (${parts.join(", ")})`;
4375
- }
4376
- return "";
4377
- }
4378
4498
  /**
4379
4499
  * Fetch pages with optional filtering
4380
4500
  */
@@ -4445,7 +4565,14 @@ ${lines.join("\n")}`;
4445
4565
  break;
4446
4566
  }
4447
4567
  if (options.limit && allPages.length >= options.limit) {
4448
- break;
4568
+ const email2 = await this.getCurrentUser();
4569
+ const limitedPages = allPages.slice(0, options.limit);
4570
+ const commits2 = [];
4571
+ for (const page of limitedPages) {
4572
+ const commit = await this.transformPageToCommit(page, void 0, email2 || void 0);
4573
+ commits2.push(commit);
4574
+ }
4575
+ return commits2;
4449
4576
  }
4450
4577
  start += limit;
4451
4578
  } catch (error) {
@@ -4469,23 +4596,9 @@ ${lines.join("\n")}`;
4469
4596
  throw error;
4470
4597
  }
4471
4598
  }
4472
- const pagesToProcess = options.limit ? allPages.slice(0, options.limit) : allPages;
4473
4599
  const email = await this.getCurrentUser();
4474
- const sinceDate = options.days ? new Date(Date.now() - options.days * 24 * 60 * 60 * 1e3) : void 0;
4475
- if (sinceDate && email) {
4476
- const filtered = await this.filterPagesByUserContribution(pagesToProcess, email, sinceDate);
4477
- logger.debug(
4478
- `Date-scoped filtering: ${pagesToProcess.length} pages -> ${filtered.length} with user contributions in range`
4479
- );
4480
- const commits2 = [];
4481
- for (const { page, userVersions } of filtered) {
4482
- const commit = await this.transformPageToCommit(page, void 0, email, userVersions);
4483
- commits2.push(commit);
4484
- }
4485
- return commits2;
4486
- }
4487
4600
  const commits = [];
4488
- for (const page of pagesToProcess) {
4601
+ for (const page of allPages) {
4489
4602
  const commit = await this.transformPageToCommit(page, void 0, email || void 0);
4490
4603
  commits.push(commit);
4491
4604
  }
@@ -4507,10 +4620,10 @@ ${lines.join("\n")}`;
4507
4620
  /**
4508
4621
  * Transform Confluence page to GitCommit format with contribution-specific data
4509
4622
  */
4510
- async transformPageToCommit(page, instanceUrl, userEmail, userVersions) {
4623
+ async transformPageToCommit(page, instanceUrl, userEmail) {
4511
4624
  let contribution = null;
4512
4625
  if (userEmail) {
4513
- contribution = await this.determineContributionType(page, userEmail, userVersions);
4626
+ contribution = await this.determineContributionType(page, userEmail);
4514
4627
  }
4515
4628
  let message;
4516
4629
  let contributionPrefix = "";
@@ -4531,14 +4644,6 @@ ${contribution.details}
4531
4644
 
4532
4645
  [Confluence Page v${page.version?.number || 1}]`;
4533
4646
  }
4534
- if (userVersions && userVersions.length > 0) {
4535
- const versionSummary = this.summarizeUserVersions(userVersions);
4536
- if (versionSummary) {
4537
- message += `
4538
-
4539
- ${versionSummary}`;
4540
- }
4541
- }
4542
4647
  let baseUrl = "https://confluence.atlassian.net";
4543
4648
  if (instanceUrl) {
4544
4649
  baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
@@ -4556,14 +4661,7 @@ ${versionSummary}`;
4556
4661
  const impactScore = contribution ? this.calculateContributionImpact(contribution.type, baseSize) : baseSize;
4557
4662
  const author = page.history?.createdBy?.displayName || page.version?.by?.displayName || "Unknown Author";
4558
4663
  const authorEmail = page.history?.createdBy?.email || page.version?.by?.email || "unknown@example.com";
4559
- let date;
4560
- if (contribution?.type === "created") {
4561
- date = page.history?.createdDate || page.version?.when;
4562
- } else if (userVersions && userVersions.length > 0) {
4563
- date = userVersions[userVersions.length - 1].when;
4564
- } else {
4565
- date = page.version?.when || (/* @__PURE__ */ new Date()).toISOString();
4566
- }
4664
+ const date = contribution?.type === "created" ? page.history?.createdDate || page.version?.when : page.version?.when || (/* @__PURE__ */ new Date()).toISOString();
4567
4665
  return {
4568
4666
  sha: page.id,
4569
4667
  message,
@@ -4790,7 +4888,7 @@ init_logger();
4790
4888
 
4791
4889
  // src/ui/prompts.ts
4792
4890
  init_esm_shims();
4793
- import { checkbox, confirm, input as input2, select as select2, editor } from "@inquirer/prompts";
4891
+ import { checkbox, confirm, input as input2, select as select3, editor } from "@inquirer/prompts";
4794
4892
  import boxen4 from "boxen";
4795
4893
 
4796
4894
  // src/ui/formatters.ts
@@ -5005,7 +5103,7 @@ async function promptDaysToScan(defaultDays = 30) {
5005
5103
  { name: "90 days", value: "90", description: "Last 3 months" },
5006
5104
  { name: "Custom", value: "custom", description: "Enter custom number of days" }
5007
5105
  ];
5008
- const selected = await select2({
5106
+ const selected = await select3({
5009
5107
  message: "How many days back should we scan for PRs?",
5010
5108
  choices,
5011
5109
  default: "30"
@@ -5039,7 +5137,7 @@ async function promptScanMode() {
5039
5137
  description: "Scan git commits directly"
5040
5138
  }
5041
5139
  ];
5042
- return await select2({
5140
+ return await select3({
5043
5141
  message: "What would you like to scan?",
5044
5142
  choices,
5045
5143
  default: "prs"
@@ -5056,7 +5154,7 @@ async function promptSortOption() {
5056
5154
  { name: "By files (most files)", value: "files", description: "Most files changed" },
5057
5155
  { name: "No sorting", value: "none", description: "Keep original order" }
5058
5156
  ];
5059
- return await select2({
5157
+ return await select3({
5060
5158
  message: "How would you like to sort the PRs?",
5061
5159
  choices,
5062
5160
  default: "date"
@@ -5070,7 +5168,7 @@ async function promptSelectOrganisation(organisations) {
5070
5168
  value: org.id
5071
5169
  }))
5072
5170
  ];
5073
- const selected = await select2({
5171
+ const selected = await select3({
5074
5172
  message: "Attach brags to which company?",
5075
5173
  choices,
5076
5174
  default: "none"
@@ -5135,7 +5233,7 @@ ${theme.label("PR Link")} ${colors.link(prUrl)}`;
5135
5233
  }
5136
5234
  console.log(boxen4(bragDetails, boxStyles.info));
5137
5235
  console.log("");
5138
- const action = await select2({
5236
+ const action = await select3({
5139
5237
  message: `What would you like to do with this brag?`,
5140
5238
  choices: [
5141
5239
  { name: "\u2713 Accept", value: "accept", description: "Add this brag as-is" },
@@ -5752,7 +5850,7 @@ async function promptSelectService() {
5752
5850
  description: `Sync all ${authenticatedSyncServices.length} authenticated service${authenticatedSyncServices.length > 1 ? "s" : ""}`
5753
5851
  });
5754
5852
  }
5755
- const selected = await select3({
5853
+ const selected = await select4({
5756
5854
  message: "Select a service to sync:",
5757
5855
  choices: serviceChoices
5758
5856
  });
@@ -7053,10 +7151,10 @@ function getConfigHint(error) {
7053
7151
  // src/cli.ts
7054
7152
  init_version();
7055
7153
  init_logger();
7056
- var __filename6 = fileURLToPath6(import.meta.url);
7057
- var __dirname6 = dirname5(__filename6);
7058
- var packageJsonPath = join7(__dirname6, "../../package.json");
7059
- var packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
7154
+ var __filename7 = fileURLToPath7(import.meta.url);
7155
+ var __dirname7 = dirname6(__filename7);
7156
+ var packageJsonPath = join8(__dirname7, "../../package.json");
7157
+ var packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
7060
7158
  var program = new Command();
7061
7159
  program.name("bragduck").description("CLI tool for managing developer achievements and brags\nAliases: bd, duck, brag").version(packageJson.version, "-v, --version", "Display version number").helpOption("-h, --help", "Display help information").option("--skip-version-check", "Skip automatic version check on startup").option("--debug", "Enable debug mode (shows detailed logs)");
7062
7160
  program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status, bitbucket, gitlab)").action(async (subcommand) => {