@bragduck/cli 2.8.1 → 2.9.1

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.
@@ -1150,7 +1150,7 @@ var init_auth_service = __esm({
1150
1150
  refreshToken: tokenResponse.refresh_token,
1151
1151
  expiresAt
1152
1152
  };
1153
- await storageService.setCredentials(credentials);
1153
+ await storageService.setServiceCredentials("bragduck", credentials);
1154
1154
  if (tokenResponse.user) {
1155
1155
  storageService.setUserInfo(tokenResponse.user);
1156
1156
  }
@@ -1177,7 +1177,7 @@ var init_auth_service = __esm({
1177
1177
  * Get current access token
1178
1178
  */
1179
1179
  async getAccessToken() {
1180
- const credentials = await storageService.getCredentials();
1180
+ const credentials = await storageService.getServiceCredentials("bragduck");
1181
1181
  return credentials?.accessToken || null;
1182
1182
  }
1183
1183
  /**
@@ -1191,7 +1191,7 @@ var init_auth_service = __esm({
1191
1191
  */
1192
1192
  async refreshToken() {
1193
1193
  logger.debug("Refreshing access token");
1194
- const credentials = await storageService.getCredentials();
1194
+ const credentials = await storageService.getServiceCredentials("bragduck");
1195
1195
  if (!credentials?.refreshToken) {
1196
1196
  throw new AuthenticationError("No refresh token available");
1197
1197
  }
@@ -1217,12 +1217,14 @@ var init_auth_service = __esm({
1217
1217
  refreshToken: response.refresh_token || credentials.refreshToken,
1218
1218
  expiresAt
1219
1219
  };
1220
- await storageService.setCredentials(newCredentials);
1220
+ await storageService.setServiceCredentials("bragduck", newCredentials);
1221
1221
  logger.debug("Token refresh successful");
1222
1222
  } catch (error) {
1223
1223
  logger.debug(`Token refresh failed: ${error.message}`);
1224
1224
  await this.logout();
1225
- throw new AuthenticationError("Token refresh failed. Please log in again.");
1225
+ throw new AuthenticationError(
1226
+ 'Bragduck platform token refresh failed. Please run "bragduck auth login" to re-authenticate.'
1227
+ );
1226
1228
  }
1227
1229
  }
1228
1230
  };
@@ -1241,12 +1243,12 @@ __export(version_exports, {
1241
1243
  });
1242
1244
  import { readFileSync as readFileSync3 } from "fs";
1243
1245
  import { fileURLToPath as fileURLToPath4 } from "url";
1244
- import { dirname as dirname3, join as join4 } from "path";
1246
+ import { dirname as dirname3, join as join5 } from "path";
1245
1247
  import chalk4 from "chalk";
1246
1248
  import boxen2 from "boxen";
1247
1249
  function getCurrentVersion() {
1248
1250
  try {
1249
- const packageJsonPath2 = join4(__dirname4, "../../package.json");
1251
+ const packageJsonPath2 = join5(__dirname4, "../../package.json");
1250
1252
  const packageJson2 = JSON.parse(readFileSync3(packageJsonPath2, "utf-8"));
1251
1253
  return packageJson2.version;
1252
1254
  } catch {
@@ -1348,10 +1350,10 @@ import { ofetch as ofetch2 } from "ofetch";
1348
1350
  import { readFileSync as readFileSync4 } from "fs";
1349
1351
  import { fileURLToPath as fileURLToPath5 } from "url";
1350
1352
  import { URLSearchParams as URLSearchParams2 } from "url";
1351
- import { dirname as dirname4, join as join5 } from "path";
1353
+ import { dirname as dirname4, join as join6 } from "path";
1352
1354
  function getCliVersion() {
1353
1355
  try {
1354
- const packageJsonPath2 = join5(__dirname5, "../../package.json");
1356
+ const packageJsonPath2 = join6(__dirname5, "../../package.json");
1355
1357
  const packageJson2 = JSON.parse(readFileSync4(packageJsonPath2, "utf-8"));
1356
1358
  return packageJson2.version;
1357
1359
  } catch {
@@ -1428,7 +1430,7 @@ var init_api_service = __esm({
1428
1430
  throw error;
1429
1431
  }
1430
1432
  throw new TokenExpiredError(
1431
- 'Your session has expired. Please run "bragduck init" to login again.'
1433
+ 'Your Bragduck platform session has expired. Please run "bragduck auth login" to re-authenticate.'
1432
1434
  );
1433
1435
  }
1434
1436
  }
@@ -1667,1133 +1669,976 @@ import { dirname as dirname5, join as join7 } from "path";
1667
1669
  init_esm_shims();
1668
1670
  init_auth_service();
1669
1671
  init_storage_service();
1670
- init_logger();
1671
1672
  import boxen from "boxen";
1672
1673
  import chalk3 from "chalk";
1673
1674
  import { input } from "@inquirer/prompts";
1674
1675
 
1675
- // src/ui/theme.ts
1676
+ // src/services/github.service.ts
1676
1677
  init_esm_shims();
1677
- import chalk2 from "chalk";
1678
- var colors = {
1679
- // Primary colors for main actions and interactive elements
1680
- primary: chalk2.cyan,
1681
- // Success states
1682
- success: chalk2.green,
1683
- successBold: chalk2.green.bold,
1684
- // Warning states
1685
- warning: chalk2.yellow,
1686
- warningBold: chalk2.yellow.bold,
1687
- // Error states
1688
- error: chalk2.red,
1689
- errorBold: chalk2.red.bold,
1690
- // Info and metadata
1691
- info: chalk2.gray,
1692
- infoDim: chalk2.dim,
1693
- // Highlighted/important data
1694
- highlight: chalk2.yellow.bold,
1695
- highlightCyan: chalk2.cyan.bold,
1696
- // Text emphasis
1697
- bold: chalk2.bold,
1698
- dim: chalk2.dim,
1699
- // Special purpose
1700
- white: chalk2.white,
1701
- gray: chalk2.gray,
1702
- // Links and URLs
1703
- link: chalk2.blue.underline
1704
- };
1705
- var theme = {
1706
- /**
1707
- * Format command names and CLI actions
1708
- */
1709
- command: (text) => colors.primary(text),
1710
- /**
1711
- * Format success messages
1712
- */
1713
- success: (text) => colors.success(`\u2713 ${text}`),
1714
- successBold: (text) => colors.successBold(`\u2713 ${text}`),
1715
- /**
1716
- * Format error messages
1717
- */
1718
- error: (text) => colors.error(`\u2717 ${text}`),
1719
- errorBold: (text) => colors.errorBold(`\u2717 ${text}`),
1678
+ init_errors();
1679
+ init_logger();
1680
+ import { exec as exec2 } from "child_process";
1681
+ import { promisify as promisify2 } from "util";
1682
+
1683
+ // src/services/git.service.ts
1684
+ init_esm_shims();
1685
+ import simpleGit from "simple-git";
1686
+
1687
+ // src/utils/validators.ts
1688
+ init_esm_shims();
1689
+ init_errors();
1690
+ import { existsSync as existsSync2 } from "fs";
1691
+ import { join as join4 } from "path";
1692
+ function validateGitRepository(path3) {
1693
+ const gitDir = join4(path3, ".git");
1694
+ if (!existsSync2(gitDir)) {
1695
+ throw new GitError(
1696
+ "Not a git repository. Please run this command from within a git repository.",
1697
+ {
1698
+ path: path3,
1699
+ hint: 'Run "git init" to initialize a git repository, or navigate to an existing one'
1700
+ }
1701
+ );
1702
+ }
1703
+ }
1704
+
1705
+ // src/services/git.service.ts
1706
+ init_errors();
1707
+ init_logger();
1708
+ var GitService = class {
1709
+ git;
1710
+ repoPath;
1711
+ constructor(repoPath = process.cwd()) {
1712
+ this.repoPath = repoPath;
1713
+ this.git = simpleGit(repoPath);
1714
+ }
1720
1715
  /**
1721
- * Format warning messages
1716
+ * Validate that the current directory is a git repository
1722
1717
  */
1723
- warning: (text) => colors.warning(`\u26A0 ${text}`),
1718
+ async validateRepository() {
1719
+ try {
1720
+ validateGitRepository(this.repoPath);
1721
+ const isRepo = await this.git.checkIsRepo();
1722
+ if (!isRepo) {
1723
+ throw new GitError("Not a valid git repository", {
1724
+ path: this.repoPath
1725
+ });
1726
+ }
1727
+ } catch (error) {
1728
+ if (error instanceof GitError) {
1729
+ throw error;
1730
+ }
1731
+ throw new GitError("Failed to validate git repository", {
1732
+ originalError: error.message,
1733
+ path: this.repoPath
1734
+ });
1735
+ }
1736
+ }
1724
1737
  /**
1725
- * Format info messages
1738
+ * Get repository information
1726
1739
  */
1727
- info: (text) => colors.info(text),
1740
+ async getRepositoryInfo() {
1741
+ try {
1742
+ await this.validateRepository();
1743
+ const status = await this.git.status();
1744
+ const remotes = await this.git.getRemotes(true);
1745
+ const primaryRemote = remotes.find((r) => r.name === "origin");
1746
+ return {
1747
+ path: this.repoPath,
1748
+ currentBranch: status.current || "unknown",
1749
+ remoteUrl: primaryRemote?.refs?.fetch || primaryRemote?.refs?.push,
1750
+ isClean: status.isClean()
1751
+ };
1752
+ } catch (error) {
1753
+ if (error instanceof GitError) {
1754
+ throw error;
1755
+ }
1756
+ throw new GitError("Failed to get repository information", {
1757
+ originalError: error.message
1758
+ });
1759
+ }
1760
+ }
1728
1761
  /**
1729
- * Format labels (e.g., "User:", "Email:")
1762
+ * Fetch recent commits
1730
1763
  */
1731
- label: (text) => colors.gray(`${text}:`),
1764
+ async getRecentCommits(options = {}) {
1765
+ const { days = 30, limit, author } = options;
1766
+ try {
1767
+ await this.validateRepository();
1768
+ logger.debug(`Fetching commits from last ${days} days`);
1769
+ const since = /* @__PURE__ */ new Date();
1770
+ since.setDate(since.getDate() - days);
1771
+ const logOptions = {
1772
+ "--since": since.toISOString(),
1773
+ "--no-merges": null
1774
+ };
1775
+ if (limit) {
1776
+ logOptions.maxCount = limit;
1777
+ }
1778
+ if (author) {
1779
+ logOptions["--author"] = author;
1780
+ }
1781
+ const log = await this.git.log(logOptions);
1782
+ logger.debug(`Found ${log.all.length} commits`);
1783
+ const commits = log.all.map((commit) => ({
1784
+ sha: commit.hash,
1785
+ message: commit.message,
1786
+ author: commit.author_name,
1787
+ authorEmail: commit.author_email,
1788
+ date: commit.date
1789
+ }));
1790
+ return commits;
1791
+ } catch (error) {
1792
+ if (error instanceof GitError) {
1793
+ throw error;
1794
+ }
1795
+ throw new GitError("Failed to fetch commits", {
1796
+ originalError: error.message,
1797
+ days
1798
+ });
1799
+ }
1800
+ }
1732
1801
  /**
1733
- * Format values
1802
+ * Get diff statistics for a specific commit
1734
1803
  */
1735
- value: (text) => colors.highlight(text),
1804
+ async getCommitStats(sha) {
1805
+ try {
1806
+ logger.debug(`Getting stats for commit ${sha}`);
1807
+ const diffSummary = await this.git.diffSummary([`${sha}^`, sha]);
1808
+ const stats = {
1809
+ filesChanged: diffSummary.files.length,
1810
+ insertions: diffSummary.insertions,
1811
+ deletions: diffSummary.deletions
1812
+ };
1813
+ logger.debug(
1814
+ `Commit ${sha}: ${stats.filesChanged} files, +${stats.insertions} -${stats.deletions}`
1815
+ );
1816
+ return stats;
1817
+ } catch (error) {
1818
+ if (error.message && error.message.includes("unknown revision")) {
1819
+ logger.debug(`Commit ${sha} has no parent (first commit)`);
1820
+ try {
1821
+ const diffSummary = await this.git.diffSummary([sha]);
1822
+ return {
1823
+ filesChanged: diffSummary.files.length,
1824
+ insertions: diffSummary.insertions,
1825
+ deletions: diffSummary.deletions
1826
+ };
1827
+ } catch {
1828
+ return {
1829
+ filesChanged: 0,
1830
+ insertions: 0,
1831
+ deletions: 0
1832
+ };
1833
+ }
1834
+ }
1835
+ throw new GitError("Failed to get commit statistics", {
1836
+ originalError: error.message,
1837
+ sha
1838
+ });
1839
+ }
1840
+ }
1736
1841
  /**
1737
- * Format counts and numbers
1842
+ * Get commits with their diff statistics
1738
1843
  */
1739
- count: (num) => colors.highlightCyan(num.toString()),
1844
+ async getCommitsWithStats(options = {}) {
1845
+ const commits = await this.getRecentCommits(options);
1846
+ logger.debug(`Fetching diff stats for ${commits.length} commits`);
1847
+ const commitsWithStats = await Promise.all(
1848
+ commits.map(async (commit) => {
1849
+ try {
1850
+ const stats = await this.getCommitStats(commit.sha);
1851
+ return {
1852
+ ...commit,
1853
+ diffStats: stats
1854
+ };
1855
+ } catch {
1856
+ logger.debug(`Failed to get stats for commit ${commit.sha}, continuing without stats`);
1857
+ return commit;
1858
+ }
1859
+ })
1860
+ );
1861
+ return commitsWithStats;
1862
+ }
1740
1863
  /**
1741
- * Format file paths
1864
+ * Get the current git user email
1742
1865
  */
1743
- path: (text) => colors.info(text),
1866
+ async getCurrentUserEmail() {
1867
+ try {
1868
+ const email = await this.git.raw(["config", "user.email"]);
1869
+ return email.trim() || null;
1870
+ } catch {
1871
+ logger.debug("Failed to get git user email");
1872
+ return null;
1873
+ }
1874
+ }
1744
1875
  /**
1745
- * Format step indicators (e.g., "Step 1/5:")
1876
+ * Get the current git user name
1746
1877
  */
1747
- step: (current, total) => colors.primary(`[${current}/${total}]`),
1878
+ async getCurrentUserName() {
1879
+ try {
1880
+ const name = await this.git.raw(["config", "user.name"]);
1881
+ return name.trim() || null;
1882
+ } catch {
1883
+ logger.debug("Failed to get git user name");
1884
+ return null;
1885
+ }
1886
+ }
1748
1887
  /**
1749
- * Format progress text
1888
+ * Filter commits by current user
1750
1889
  */
1751
- progress: (text) => colors.infoDim(text),
1890
+ async getCommitsByCurrentUser(options = {}) {
1891
+ const userEmail = await this.getCurrentUserEmail();
1892
+ if (!userEmail) {
1893
+ logger.warning("Could not determine git user email, returning all commits");
1894
+ return this.getCommitsWithStats(options);
1895
+ }
1896
+ logger.debug(`Filtering commits by user: ${userEmail}`);
1897
+ return this.getCommitsWithStats({
1898
+ ...options,
1899
+ author: userEmail
1900
+ });
1901
+ }
1902
+ };
1903
+ var gitService = new GitService();
1904
+
1905
+ // src/services/github.service.ts
1906
+ var execAsync2 = promisify2(exec2);
1907
+ var GitHubService = class {
1908
+ MAX_BODY_LENGTH = 5e3;
1909
+ PR_SEARCH_FIELDS = "number,title,body,author,mergedAt,additions,deletions,changedFiles,url,labels";
1752
1910
  /**
1753
- * Format emphasized text
1911
+ * Check if GitHub CLI is installed and available
1754
1912
  */
1755
- emphasis: (text) => colors.bold(text),
1913
+ async checkGitHubCLI() {
1914
+ try {
1915
+ await execAsync2("command gh --version");
1916
+ return true;
1917
+ } catch {
1918
+ return false;
1919
+ }
1920
+ }
1756
1921
  /**
1757
- * Format de-emphasized/secondary text
1922
+ * Validate that GitHub CLI is installed
1758
1923
  */
1759
- secondary: (text) => colors.dim(text),
1924
+ async ensureGitHubCLI() {
1925
+ const isInstalled = await this.checkGitHubCLI();
1926
+ if (!isInstalled) {
1927
+ throw new GitHubError("GitHub CLI (gh) is not installed", {
1928
+ hint: "Install from https://cli.github.com/"
1929
+ });
1930
+ }
1931
+ }
1760
1932
  /**
1761
- * Format interactive elements (prompts, selections)
1933
+ * Check if user is authenticated with GitHub CLI
1762
1934
  */
1763
- interactive: (text) => colors.primary(text),
1935
+ async checkAuthentication() {
1936
+ try {
1937
+ await execAsync2("command gh auth status");
1938
+ return true;
1939
+ } catch {
1940
+ return false;
1941
+ }
1942
+ }
1764
1943
  /**
1765
- * Format keys in key-value pairs
1944
+ * Ensure user is authenticated with GitHub CLI
1766
1945
  */
1767
- key: (text) => colors.white(text)
1768
- };
1769
- var boxStyles = {
1770
- success: {
1771
- borderColor: "green",
1772
- borderStyle: "round",
1773
- padding: 1,
1774
- margin: 1
1775
- },
1776
- error: {
1777
- borderColor: "red",
1778
- borderStyle: "round",
1779
- padding: 1,
1780
- margin: 1
1781
- },
1782
- warning: {
1783
- borderColor: "yellow",
1784
- borderStyle: "round",
1785
- padding: 1,
1786
- margin: 1
1787
- },
1788
- info: {
1789
- borderColor: "cyan",
1790
- borderStyle: "round",
1791
- padding: 1,
1792
- margin: 1
1793
- },
1794
- plain: {
1795
- borderColor: "gray",
1796
- borderStyle: "round",
1797
- padding: 1,
1798
- margin: 1
1799
- }
1800
- };
1801
- var tableStyles = {
1802
- default: {
1803
- style: {
1804
- head: [],
1805
- border: ["gray"]
1806
- }
1807
- },
1808
- compact: {
1809
- style: {
1810
- head: [],
1811
- border: ["dim"],
1812
- compact: true
1946
+ async ensureAuthentication() {
1947
+ const isAuthenticated = await this.checkAuthentication();
1948
+ if (!isAuthenticated) {
1949
+ throw new GitHubError("Not authenticated with GitHub", {
1950
+ hint: 'Run "gh auth login" to authenticate'
1951
+ });
1813
1952
  }
1814
1953
  }
1815
- };
1816
- var sizeIndicators = {
1817
- small: colors.success("\u25CF"),
1818
- medium: colors.warning("\u25CF"),
1819
- large: colors.error("\u25CF")
1820
- };
1821
- function getSizeIndicator(insertions, deletions) {
1822
- const total = insertions + deletions;
1823
- if (total < 50) {
1824
- return `${sizeIndicators.small} Small`;
1825
- } else if (total < 200) {
1826
- return `${sizeIndicators.medium} Medium`;
1827
- } else {
1828
- return `${sizeIndicators.large} Large`;
1954
+ /**
1955
+ * Check GitHub CLI authentication status
1956
+ * Returns object with installation and authentication status
1957
+ */
1958
+ async getAuthStatus() {
1959
+ const installed = await this.checkGitHubCLI();
1960
+ if (!installed) {
1961
+ return { installed: false, authenticated: false };
1962
+ }
1963
+ const authenticated = await this.checkAuthentication();
1964
+ return { installed, authenticated };
1829
1965
  }
1830
- }
1831
- function formatDiffStats(insertions, deletions) {
1832
- return `${colors.success(`+${insertions}`)} ${colors.error(`-${deletions}`)}`;
1833
- }
1834
-
1835
- // src/commands/auth.ts
1836
- async function authCommand(subcommand) {
1837
- if (!subcommand || subcommand === "login") {
1838
- await authLogin();
1839
- } else if (subcommand === "status") {
1840
- await authStatus();
1841
- } else if (subcommand === "bitbucket") {
1842
- await authBitbucket();
1843
- } else if (subcommand === "gitlab") {
1844
- await authGitLab();
1845
- } else if (subcommand === "atlassian") {
1846
- await authAtlassian();
1847
- } else {
1848
- logger.error(`Unknown auth subcommand: ${subcommand}`);
1849
- logger.info("Available subcommands: login, status, bitbucket, gitlab, atlassian");
1850
- process.exit(1);
1966
+ /**
1967
+ * Validate that the current repository is hosted on GitHub
1968
+ */
1969
+ async validateGitHubRepository() {
1970
+ try {
1971
+ await this.ensureGitHubCLI();
1972
+ await this.ensureAuthentication();
1973
+ await gitService.validateRepository();
1974
+ const { stdout } = await execAsync2("command gh repo view --json url");
1975
+ const data = JSON.parse(stdout);
1976
+ if (!data.url) {
1977
+ throw new GitHubError("This repository is not hosted on GitHub", {
1978
+ hint: "Only GitHub repositories are currently supported for PR scanning"
1979
+ });
1980
+ }
1981
+ } catch (error) {
1982
+ if (error instanceof GitHubError || error instanceof GitError) {
1983
+ throw error;
1984
+ }
1985
+ if (error.message?.includes("not a git repository")) {
1986
+ throw new GitHubError("Not a git repository", {
1987
+ hint: "Navigate to a git repository directory"
1988
+ });
1989
+ }
1990
+ if (error.message?.includes("Could not resolve to a Repository")) {
1991
+ throw new GitHubError("This repository is not hosted on GitHub", {
1992
+ hint: "Only GitHub repositories are currently supported for PR scanning"
1993
+ });
1994
+ }
1995
+ throw new GitHubError("Failed to validate GitHub repository", {
1996
+ originalError: error.message
1997
+ });
1998
+ }
1851
1999
  }
1852
- }
1853
- async function authLogin() {
1854
- logger.log("");
1855
- logger.info("Authenticating with Bragduck...");
1856
- logger.log("");
1857
- const isAuthenticated = await storageService.isServiceAuthenticated("bragduck");
1858
- if (isAuthenticated) {
1859
- const userInfo = authService.getUserInfo();
1860
- if (userInfo) {
1861
- logger.log(
1862
- boxen(
1863
- `${chalk3.yellow("Already authenticated!")}
1864
-
1865
- ${chalk3.gray("User:")} ${userInfo.name}
1866
- ${chalk3.gray("Email:")} ${userInfo.email}
1867
-
1868
- ${chalk3.dim("Run")} ${chalk3.cyan("bragduck logout")} ${chalk3.dim("to sign out")}`,
1869
- {
1870
- padding: 1,
1871
- margin: 1,
1872
- borderStyle: "round",
1873
- borderColor: "yellow"
1874
- }
1875
- )
2000
+ /**
2001
+ * Get GitHub repository information
2002
+ */
2003
+ async getRepositoryInfo() {
2004
+ try {
2005
+ await this.ensureGitHubCLI();
2006
+ const { stdout } = await execAsync2(
2007
+ "command gh repo view --json owner,name,url,nameWithOwner"
1876
2008
  );
1877
- logger.log("");
1878
- return;
1879
- }
1880
- }
1881
- try {
1882
- const userInfo = await authService.login();
1883
- logger.log("");
1884
- logger.log(
1885
- boxen(
1886
- `${chalk3.green.bold("\u2713 Successfully authenticated!")}
1887
-
1888
- ${chalk3.gray("Welcome,")} ${chalk3.cyan(userInfo.name)}
1889
- ${chalk3.gray("Email:")} ${userInfo.email}
1890
-
1891
- ${chalk3.dim("Use")} ${chalk3.cyan("bragduck sync")} ${chalk3.dim("to create brags")}`,
1892
- {
1893
- padding: 1,
1894
- margin: 1,
1895
- borderStyle: "round",
1896
- borderColor: "green"
1897
- }
1898
- )
1899
- );
1900
- logger.log("");
1901
- } catch (error) {
1902
- const err = error;
1903
- logger.log("");
1904
- logger.log(
1905
- boxen(`${chalk3.red.bold("\u2717 Authentication Failed")}
1906
-
1907
- ${err.message}`, {
1908
- padding: 1,
1909
- margin: 1,
1910
- borderStyle: "round",
1911
- borderColor: "red"
1912
- })
1913
- );
1914
- process.exit(1);
1915
- }
1916
- }
1917
- async function authStatus() {
1918
- logger.log("");
1919
- logger.info("Authentication Status:");
1920
- logger.log("");
1921
- const services = await storageService.getAuthenticatedServices();
1922
- if (services.length === 0) {
1923
- logger.info(theme.secondary("Not authenticated with any services"));
1924
- logger.log("");
1925
- logger.info(`Run ${theme.command("bragduck auth login")} to authenticate`);
1926
- logger.log("");
1927
- return;
1928
- }
1929
- const bragduckAuth = await storageService.isServiceAuthenticated("bragduck");
1930
- if (bragduckAuth) {
1931
- const userInfo = authService.getUserInfo();
1932
- logger.info(`${theme.success("\u2713")} Bragduck: ${userInfo?.name || "Authenticated"}`);
1933
- } else {
1934
- logger.info(`${theme.error("\u2717")} Bragduck: Not authenticated`);
1935
- }
1936
- for (const service of services) {
1937
- if (service !== "bragduck") {
1938
- logger.info(`${theme.success("\u2713")} ${service}: Authenticated`);
1939
- }
1940
- }
1941
- logger.log("");
1942
- }
1943
- async function authBitbucket() {
1944
- logger.log("");
1945
- logger.log(
1946
- boxen(
1947
- theme.info("Bitbucket API Token Authentication") + "\n\nCreate an API Token at:\n" + colors.highlight("https://bitbucket.org/account/settings/api-token/new") + "\n\nRequired scopes:\n \u2022 pullrequest:read\n \u2022 repository:read\n \u2022 account:read\n\n" + theme.warning("Note: API tokens expire (max 1 year)"),
1948
- boxStyles.info
1949
- )
1950
- );
1951
- logger.log("");
1952
- try {
1953
- const email = await input({
1954
- message: "Atlassian account email:",
1955
- validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
1956
- });
1957
- const apiToken = await input({
1958
- message: "API Token:",
1959
- validate: (value) => value.length > 0 ? true : "API token cannot be empty"
1960
- });
1961
- const tokenExpiry = await input({
1962
- message: "Token expiry date (YYYY-MM-DD, optional):",
1963
- default: "",
1964
- validate: (value) => {
1965
- if (!value) return true;
1966
- const date = new Date(value);
1967
- return !isNaN(date.getTime()) ? true : "Please enter a valid date (YYYY-MM-DD)";
1968
- }
1969
- });
1970
- const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
1971
- const response = await fetch("https://api.bitbucket.org/2.0/user", {
1972
- headers: { Authorization: `Basic ${auth}` }
1973
- });
1974
- if (!response.ok) {
1975
- logger.log("");
1976
- logger.log(
1977
- boxen(
1978
- theme.error("\u2717 Authentication Failed") + "\n\nInvalid email or API token",
1979
- boxStyles.error
1980
- )
1981
- );
1982
- logger.log("");
1983
- process.exit(1);
1984
- }
1985
- const user = await response.json();
1986
- const credentials = {
1987
- accessToken: apiToken,
1988
- username: email
1989
- // Store email in username field
1990
- };
1991
- if (tokenExpiry) {
1992
- const expiryDate = new Date(tokenExpiry);
1993
- credentials.expiresAt = expiryDate.getTime();
1994
- }
1995
- await storageService.setServiceCredentials("bitbucket", credentials);
1996
- logger.log("");
1997
- logger.log(
1998
- boxen(
1999
- theme.success("\u2713 Successfully authenticated with Bitbucket") + `
2000
-
2001
- Email: ${email}
2002
- User: ${user.display_name}
2003
- ` + (tokenExpiry ? `Expires: ${tokenExpiry}` : ""),
2004
- boxStyles.success
2005
- )
2006
- );
2007
- logger.log("");
2008
- } catch (error) {
2009
- const err = error;
2010
- logger.log("");
2011
- logger.log(
2012
- boxen(
2013
- theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2014
- boxStyles.error
2015
- )
2016
- );
2017
- logger.log("");
2018
- process.exit(1);
2019
- }
2020
- }
2021
- async function authGitLab() {
2022
- logger.log("");
2023
- logger.log(
2024
- boxen(
2025
- theme.info("GitLab Personal Access Token Authentication") + "\n\nCreate a Personal Access Token at:\n" + colors.highlight("https://gitlab.com/-/profile/personal_access_tokens") + "\n\nRequired scopes:\n \u2022 api (full API access)\n \u2022 read_api (read API)\n \u2022 read_user (read user info)\n\n" + theme.warning("For self-hosted GitLab, check your instance docs"),
2026
- boxStyles.info
2027
- )
2028
- );
2029
- logger.log("");
2030
- try {
2031
- const instanceUrl = await input({
2032
- message: "GitLab instance URL (press Enter for gitlab.com):",
2033
- default: "https://gitlab.com"
2034
- });
2035
- const accessToken = await input({
2036
- message: "Personal Access Token:",
2037
- validate: (value) => value.length > 0 ? true : "Token cannot be empty"
2038
- });
2039
- const testUrl = `${instanceUrl.replace(/\/$/, "")}/api/v4/user`;
2040
- const response = await fetch(testUrl, {
2041
- headers: { "PRIVATE-TOKEN": accessToken }
2042
- });
2043
- if (!response.ok) {
2044
- logger.log("");
2045
- logger.log(
2046
- boxen(
2047
- theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL or access token",
2048
- boxStyles.error
2049
- )
2050
- );
2051
- logger.log("");
2052
- process.exit(1);
2053
- }
2054
- const user = await response.json();
2055
- const credentials = {
2056
- accessToken,
2057
- instanceUrl: instanceUrl === "https://gitlab.com" ? void 0 : instanceUrl
2058
- };
2059
- await storageService.setServiceCredentials("gitlab", credentials);
2060
- logger.log("");
2061
- logger.log(
2062
- boxen(
2063
- theme.success("\u2713 Successfully authenticated with GitLab") + `
2064
-
2065
- Instance: ${instanceUrl}
2066
- User: ${user.name} (@${user.username})`,
2067
- boxStyles.success
2068
- )
2069
- );
2070
- logger.log("");
2071
- } catch (error) {
2072
- const err = error;
2073
- logger.log("");
2074
- logger.log(
2075
- boxen(
2076
- theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2077
- boxStyles.error
2078
- )
2079
- );
2080
- logger.log("");
2081
- process.exit(1);
2082
- }
2083
- }
2084
- async function authAtlassian() {
2085
- logger.log("");
2086
- logger.log(
2087
- boxen(
2088
- 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",
2089
- boxStyles.info
2090
- )
2091
- );
2092
- logger.log("");
2093
- try {
2094
- const instanceUrl = await input({
2095
- message: "Atlassian instance URL (e.g., company.atlassian.net):",
2096
- validate: (value) => value.length > 0 ? true : "Instance URL cannot be empty"
2097
- });
2098
- const email = await input({
2099
- message: "Atlassian account email:",
2100
- validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
2101
- });
2102
- const apiToken = await input({
2103
- message: "API Token:",
2104
- validate: (value) => value.length > 0 ? true : "API token cannot be empty"
2105
- });
2106
- const testInstanceUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
2107
- const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
2108
- const response = await fetch(`${testInstanceUrl}/rest/api/2/myself`, {
2109
- headers: { Authorization: `Basic ${auth}` }
2110
- });
2111
- if (!response.ok) {
2112
- logger.log("");
2113
- logger.log(
2114
- boxen(
2115
- 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)",
2116
- boxStyles.error
2117
- )
2118
- );
2119
- logger.log("");
2120
- process.exit(1);
2121
- }
2122
- const user = await response.json();
2123
- const credentials = {
2124
- accessToken: apiToken,
2125
- username: email,
2126
- instanceUrl: instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`
2127
- };
2128
- await storageService.setServiceCredentials("jira", credentials);
2129
- await storageService.setServiceCredentials("confluence", credentials);
2130
- await storageService.setServiceCredentials("bitbucket", {
2131
- ...credentials,
2132
- instanceUrl: "https://api.bitbucket.org"
2133
- // Bitbucket uses different API base
2134
- });
2135
- logger.log("");
2136
- logger.log(
2137
- boxen(
2138
- theme.success("\u2713 Successfully authenticated with Atlassian") + `
2139
-
2140
- Instance: ${instanceUrl}
2141
- User: ${user.displayName}
2142
- Email: ${user.emailAddress}
2143
-
2144
- Services configured:
2145
- \u2022 Jira
2146
- \u2022 Confluence
2147
- \u2022 Bitbucket`,
2148
- boxStyles.success
2149
- )
2150
- );
2151
- logger.log("");
2152
- } catch (error) {
2153
- const err = error;
2154
- logger.log("");
2155
- logger.log(
2156
- boxen(
2157
- theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2158
- boxStyles.error
2159
- )
2160
- );
2161
- logger.log("");
2162
- process.exit(1);
2163
- }
2164
- }
2165
-
2166
- // src/commands/sync.ts
2167
- init_esm_shims();
2168
-
2169
- // node_modules/@inquirer/core/dist/esm/lib/errors.mjs
2170
- init_esm_shims();
2171
- var CancelPromptError = class extends Error {
2172
- name = "CancelPromptError";
2173
- message = "Prompt was canceled";
2174
- };
2175
-
2176
- // src/commands/sync.ts
2177
- init_api_service();
2178
- init_storage_service();
2179
- init_auth_service();
2180
- import boxen6 from "boxen";
2181
-
2182
- // src/utils/source-detector.ts
2183
- init_esm_shims();
2184
- init_errors();
2185
- init_storage_service();
2186
- import { exec as exec2 } from "child_process";
2187
- import { promisify as promisify2 } from "util";
2188
- import { select } from "@inquirer/prompts";
2189
- var execAsync2 = promisify2(exec2);
2190
- var SourceDetector = class {
2191
- /**
2192
- * Detect all possible sources from git remotes
2193
- */
2194
- async detectSources(options = {}) {
2195
- const detected = [];
2196
- try {
2197
- const { stdout } = await execAsync2("git remote -v");
2198
- const remotes = this.parseRemotes(stdout);
2199
- for (const remote of remotes) {
2200
- const source = this.parseRemoteUrl(remote.url);
2201
- if (source) {
2202
- const isAuthenticated = await this.checkAuthentication(source.type);
2203
- detected.push({
2204
- ...source,
2205
- remoteUrl: remote.url,
2206
- isAuthenticated
2207
- });
2208
- }
2209
- }
2210
- } catch {
2211
- throw new GitError("Not a git repository");
2212
- }
2213
- let recommended;
2214
- if (detected.length > 1 && options.allowInteractive && process.stdout.isTTY) {
2215
- try {
2216
- recommended = await this.promptSourceSelection(detected, options.showAuthStatus);
2217
- } catch {
2218
- }
2219
- }
2220
- if (!recommended && options.respectPriority) {
2221
- const priority = await storageService.getConfigWithHierarchy("sourcePriority");
2222
- if (priority) {
2223
- recommended = this.applyPriority(detected, priority);
2224
- }
2225
- }
2226
- if (!recommended) {
2227
- recommended = this.selectRecommendedSource(detected);
2228
- }
2229
- return { detected, recommended };
2230
- }
2231
- /**
2232
- * Prompt user to select a source interactively
2233
- */
2234
- async promptSourceSelection(sources, showAuthStatus = true) {
2235
- const choices = sources.map((source) => {
2236
- const authStatus2 = showAuthStatus ? source.isAuthenticated ? "\u2713 authenticated" : "\u2717 not authenticated" : "";
2237
- const repo = source.owner && source.repo ? `${source.owner}/${source.repo}` : source.host;
2238
- const name = `${source.type}${authStatus2 ? ` (${authStatus2})` : ""} - ${repo}`;
2239
- return {
2240
- name,
2241
- value: source.type,
2242
- description: source.remoteUrl
2243
- };
2244
- });
2245
- return await select({
2246
- message: "Multiple sources detected. Which do you want to sync?",
2247
- choices
2248
- });
2249
- }
2250
- /**
2251
- * Apply configured priority to select source
2252
- */
2253
- applyPriority(sources, priority) {
2254
- for (const sourceType of priority) {
2255
- const found = sources.find((s) => s.type === sourceType);
2256
- if (found) return found.type;
2257
- }
2258
- return void 0;
2259
- }
2260
- /**
2261
- * Parse git remote -v output
2262
- */
2263
- parseRemotes(output) {
2264
- const lines = output.split("\n").filter(Boolean);
2265
- const remotes = /* @__PURE__ */ new Map();
2266
- for (const line of lines) {
2267
- const match = line.match(/^(\S+)\s+(\S+)\s+\(fetch\)$/);
2268
- if (match && match[1] && match[2]) {
2269
- remotes.set(match[1], match[2]);
2270
- }
2271
- }
2272
- return Array.from(remotes.entries()).map(([name, url]) => ({ name, url }));
2273
- }
2274
- /**
2275
- * Parse remote URL to detect source type
2276
- */
2277
- parseRemoteUrl(url) {
2278
- if (url.includes("github.com")) {
2279
- const match = url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
2280
- if (match && match[1] && match[2]) {
2281
- return {
2282
- type: "github",
2283
- host: "github.com",
2284
- owner: match[1],
2285
- repo: match[2]
2286
- };
2287
- }
2288
- }
2289
- if (url.includes("gitlab.com")) {
2290
- const match = url.match(/gitlab\.com[:/]([^/]+)\/([^/.]+)/);
2291
- if (match && match[1] && match[2]) {
2292
- return {
2293
- type: "gitlab",
2294
- host: "gitlab.com",
2295
- owner: match[1],
2296
- repo: match[2]
2297
- };
2298
- }
2299
- }
2300
- if (url.includes("bitbucket.org")) {
2301
- const match = url.match(/bitbucket\.org[:/]([^/]+)\/([^/.]+)/);
2302
- if (match && match[1] && match[2]) {
2303
- return {
2304
- type: "bitbucket",
2305
- host: "bitbucket.org",
2306
- owner: match[1],
2307
- repo: match[2]
2308
- };
2309
- }
2310
- }
2311
- if (url.match(/\/scm\/|bitbucket\./)) {
2312
- const match = url.match(/([^/:]+)[:/]scm\/([^/]+)\/([^/.]+)/);
2313
- if (match && match[1] && match[2] && match[3]) {
2314
- return {
2315
- type: "atlassian",
2316
- host: match[1],
2317
- owner: match[2],
2318
- repo: match[3]
2319
- };
2320
- }
2321
- }
2322
- return null;
2323
- }
2324
- /**
2325
- * Check if user is authenticated with a specific source
2326
- */
2327
- async checkAuthentication(type) {
2328
- try {
2329
- if (type === "github") {
2330
- await execAsync2("command gh auth status");
2331
- return true;
2332
- } else if (type === "bitbucket" || type === "atlassian") {
2333
- return await storageService.isServiceAuthenticated("bitbucket");
2334
- } else if (type === "gitlab") {
2335
- return await storageService.isServiceAuthenticated("gitlab");
2336
- } else if (type === "jira") {
2337
- return await storageService.isServiceAuthenticated("jira");
2338
- } else if (type === "confluence") {
2339
- return await storageService.isServiceAuthenticated("confluence");
2340
- }
2341
- return false;
2342
- } catch {
2343
- return false;
2344
- }
2345
- }
2346
- /**
2347
- * Select recommended source (prefer authenticated GitHub)
2348
- */
2349
- selectRecommendedSource(sources) {
2350
- const authenticated = sources.find((s) => s.isAuthenticated);
2351
- if (authenticated) return authenticated.type;
2352
- const github = sources.find((s) => s.type === "github");
2353
- if (github) return "github";
2354
- return sources[0]?.type;
2355
- }
2356
- };
2357
- var sourceDetector = new SourceDetector();
2358
-
2359
- // src/commands/sync.ts
2360
- init_env_loader();
2361
- init_config_loader();
2362
-
2363
- // src/sync/adapter-factory.ts
2364
- init_esm_shims();
2365
-
2366
- // src/sync/github-adapter.ts
2367
- init_esm_shims();
2368
-
2369
- // src/services/github.service.ts
2370
- init_esm_shims();
2371
- init_errors();
2372
- init_logger();
2373
- import { exec as exec3 } from "child_process";
2374
- import { promisify as promisify3 } from "util";
2375
-
2376
- // src/services/git.service.ts
2377
- init_esm_shims();
2378
- import simpleGit from "simple-git";
2379
-
2380
- // src/utils/validators.ts
2381
- init_esm_shims();
2382
- init_errors();
2383
- import { existsSync as existsSync2 } from "fs";
2384
- import { join as join6 } from "path";
2385
- function validateGitRepository(path3) {
2386
- const gitDir = join6(path3, ".git");
2387
- if (!existsSync2(gitDir)) {
2388
- throw new GitError(
2389
- "Not a git repository. Please run this command from within a git repository.",
2390
- {
2391
- path: path3,
2392
- hint: 'Run "git init" to initialize a git repository, or navigate to an existing one'
2393
- }
2394
- );
2395
- }
2396
- }
2397
-
2398
- // src/services/git.service.ts
2399
- init_errors();
2400
- init_logger();
2401
- var GitService = class {
2402
- git;
2403
- repoPath;
2404
- constructor(repoPath = process.cwd()) {
2405
- this.repoPath = repoPath;
2406
- this.git = simpleGit(repoPath);
2407
- }
2408
- /**
2409
- * Validate that the current directory is a git repository
2410
- */
2411
- async validateRepository() {
2412
- try {
2413
- validateGitRepository(this.repoPath);
2414
- const isRepo = await this.git.checkIsRepo();
2415
- if (!isRepo) {
2416
- throw new GitError("Not a valid git repository", {
2417
- path: this.repoPath
2418
- });
2419
- }
2420
- } catch (error) {
2421
- if (error instanceof GitError) {
2422
- throw error;
2423
- }
2424
- throw new GitError("Failed to validate git repository", {
2425
- originalError: error.message,
2426
- path: this.repoPath
2427
- });
2009
+ const data = JSON.parse(stdout);
2010
+ return {
2011
+ owner: data.owner.login,
2012
+ name: data.name,
2013
+ fullName: data.nameWithOwner,
2014
+ url: data.url,
2015
+ isGitHub: true
2016
+ };
2017
+ } catch (error) {
2018
+ if (error instanceof GitHubError || error instanceof GitError) {
2019
+ throw error;
2020
+ }
2021
+ throw new GitHubError("Failed to get GitHub repository information", {
2022
+ originalError: error.message
2023
+ });
2428
2024
  }
2429
2025
  }
2430
2026
  /**
2431
- * Get repository information
2027
+ * Get the current authenticated GitHub user
2432
2028
  */
2433
- async getRepositoryInfo() {
2029
+ async getCurrentGitHubUser() {
2434
2030
  try {
2435
- await this.validateRepository();
2436
- const status = await this.git.status();
2437
- const remotes = await this.git.getRemotes(true);
2438
- const primaryRemote = remotes.find((r) => r.name === "origin");
2439
- return {
2440
- path: this.repoPath,
2441
- currentBranch: status.current || "unknown",
2442
- remoteUrl: primaryRemote?.refs?.fetch || primaryRemote?.refs?.push,
2443
- isClean: status.isClean()
2444
- };
2445
- } catch (error) {
2446
- if (error instanceof GitError) {
2447
- throw error;
2448
- }
2449
- throw new GitError("Failed to get repository information", {
2450
- originalError: error.message
2451
- });
2031
+ const { stdout } = await execAsync2("command gh api user --jq .login");
2032
+ return stdout.trim() || null;
2033
+ } catch {
2034
+ logger.debug("Failed to get GitHub user");
2035
+ return null;
2452
2036
  }
2453
2037
  }
2454
2038
  /**
2455
- * Fetch recent commits
2039
+ * Fetch merged PRs from GitHub
2456
2040
  */
2457
- async getRecentCommits(options = {}) {
2041
+ async getMergedPRs(options = {}) {
2458
2042
  const { days = 30, limit, author } = options;
2459
2043
  try {
2460
- await this.validateRepository();
2461
- logger.debug(`Fetching commits from last ${days} days`);
2044
+ await this.ensureGitHubCLI();
2045
+ await this.ensureAuthentication();
2046
+ logger.debug(`Fetching merged PRs from the last ${days} days`);
2462
2047
  const since = /* @__PURE__ */ new Date();
2463
2048
  since.setDate(since.getDate() - days);
2464
- const logOptions = {
2465
- "--since": since.toISOString(),
2466
- "--no-merges": null
2467
- };
2468
- if (limit) {
2469
- logOptions.maxCount = limit;
2470
- }
2049
+ const sinceDate = since.toISOString().split("T")[0];
2050
+ let searchQuery = `merged:>=${sinceDate}`;
2471
2051
  if (author) {
2472
- logOptions["--author"] = author;
2052
+ searchQuery += ` author:${author}`;
2473
2053
  }
2474
- const log = await this.git.log(logOptions);
2475
- logger.debug(`Found ${log.all.length} commits`);
2476
- const commits = log.all.map((commit) => ({
2477
- sha: commit.hash,
2478
- message: commit.message,
2479
- author: commit.author_name,
2480
- authorEmail: commit.author_email,
2481
- date: commit.date
2482
- }));
2483
- return commits;
2054
+ const limitArg = limit ? `--limit ${limit}` : "";
2055
+ const command = `command gh pr list --state merged --json ${this.PR_SEARCH_FIELDS} --search "${searchQuery}" ${limitArg}`;
2056
+ logger.debug(`Running: ${command}`);
2057
+ const { stdout } = await execAsync2(command);
2058
+ const prs = JSON.parse(stdout);
2059
+ logger.debug(`Found ${prs.length} merged PRs`);
2060
+ return prs;
2484
2061
  } catch (error) {
2485
- if (error instanceof GitError) {
2062
+ if (error instanceof GitHubError || error instanceof GitError) {
2486
2063
  throw error;
2487
2064
  }
2488
- throw new GitError("Failed to fetch commits", {
2065
+ throw new GitHubError("Failed to fetch PRs from GitHub", {
2489
2066
  originalError: error.message,
2490
2067
  days
2491
2068
  });
2492
2069
  }
2493
2070
  }
2494
2071
  /**
2495
- * Get diff statistics for a specific commit
2072
+ * Get merged PRs by current user (PRs authored by the current user)
2496
2073
  */
2497
- async getCommitStats(sha) {
2498
- try {
2499
- logger.debug(`Getting stats for commit ${sha}`);
2500
- const diffSummary = await this.git.diffSummary([`${sha}^`, sha]);
2501
- const stats = {
2502
- filesChanged: diffSummary.files.length,
2503
- insertions: diffSummary.insertions,
2504
- deletions: diffSummary.deletions
2505
- };
2506
- logger.debug(
2507
- `Commit ${sha}: ${stats.filesChanged} files, +${stats.insertions} -${stats.deletions}`
2508
- );
2509
- return stats;
2510
- } catch (error) {
2511
- if (error.message && error.message.includes("unknown revision")) {
2512
- logger.debug(`Commit ${sha} has no parent (first commit)`);
2513
- try {
2514
- const diffSummary = await this.git.diffSummary([sha]);
2515
- return {
2516
- filesChanged: diffSummary.files.length,
2517
- insertions: diffSummary.insertions,
2518
- deletions: diffSummary.deletions
2519
- };
2520
- } catch {
2521
- return {
2522
- filesChanged: 0,
2523
- insertions: 0,
2524
- deletions: 0
2525
- };
2526
- }
2527
- }
2528
- throw new GitError("Failed to get commit statistics", {
2529
- originalError: error.message,
2530
- sha
2531
- });
2074
+ async getPRsByCurrentUser(options = {}) {
2075
+ const currentUser = await this.getCurrentGitHubUser();
2076
+ if (!currentUser) {
2077
+ logger.warning("Could not determine GitHub user, returning all PRs");
2078
+ return this.getMergedPRs(options);
2532
2079
  }
2080
+ logger.debug(`Filtering PRs by author: ${currentUser}`);
2081
+ return this.getMergedPRs({
2082
+ ...options,
2083
+ author: currentUser
2084
+ });
2533
2085
  }
2534
2086
  /**
2535
- * Get commits with their diff statistics
2087
+ * Transform a GitHub PR into a GitCommit structure
2088
+ * This allows PR data to work with existing commit-based UI and API
2536
2089
  */
2537
- async getCommitsWithStats(options = {}) {
2538
- const commits = await this.getRecentCommits(options);
2539
- logger.debug(`Fetching diff stats for ${commits.length} commits`);
2540
- const commitsWithStats = await Promise.all(
2541
- commits.map(async (commit) => {
2542
- try {
2543
- const stats = await this.getCommitStats(commit.sha);
2544
- return {
2545
- ...commit,
2546
- diffStats: stats
2547
- };
2548
- } catch {
2549
- logger.debug(`Failed to get stats for commit ${commit.sha}, continuing without stats`);
2550
- return commit;
2551
- }
2552
- })
2553
- );
2554
- return commitsWithStats;
2090
+ transformPRToCommit(pr) {
2091
+ const title = pr.title;
2092
+ const body = pr.body || "";
2093
+ const truncatedBody = body.length > this.MAX_BODY_LENGTH ? body.substring(0, this.MAX_BODY_LENGTH) + "...[truncated]" : body;
2094
+ const message = truncatedBody ? `${title}
2095
+
2096
+ ${truncatedBody}` : title;
2097
+ return {
2098
+ sha: `pr-${pr.number}`,
2099
+ message,
2100
+ author: pr.author.login,
2101
+ authorEmail: "",
2102
+ // Not available from gh PR API
2103
+ date: pr.mergedAt,
2104
+ url: pr.url,
2105
+ diffStats: {
2106
+ filesChanged: pr.changedFiles,
2107
+ insertions: pr.additions,
2108
+ deletions: pr.deletions
2109
+ }
2110
+ };
2555
2111
  }
2112
+ };
2113
+ var githubService = new GitHubService();
2114
+
2115
+ // src/commands/auth.ts
2116
+ init_logger();
2117
+
2118
+ // src/ui/theme.ts
2119
+ init_esm_shims();
2120
+ import chalk2 from "chalk";
2121
+ var colors = {
2122
+ // Primary colors for main actions and interactive elements
2123
+ primary: chalk2.cyan,
2124
+ // Success states
2125
+ success: chalk2.green,
2126
+ successBold: chalk2.green.bold,
2127
+ // Warning states
2128
+ warning: chalk2.yellow,
2129
+ warningBold: chalk2.yellow.bold,
2130
+ // Error states
2131
+ error: chalk2.red,
2132
+ errorBold: chalk2.red.bold,
2133
+ // Info and metadata
2134
+ info: chalk2.gray,
2135
+ infoDim: chalk2.dim,
2136
+ // Highlighted/important data
2137
+ highlight: chalk2.yellow.bold,
2138
+ highlightCyan: chalk2.cyan.bold,
2139
+ // Text emphasis
2140
+ bold: chalk2.bold,
2141
+ dim: chalk2.dim,
2142
+ // Special purpose
2143
+ white: chalk2.white,
2144
+ gray: chalk2.gray,
2145
+ // Links and URLs
2146
+ link: chalk2.blue.underline
2147
+ };
2148
+ var theme = {
2149
+ /**
2150
+ * Format command names and CLI actions
2151
+ */
2152
+ command: (text) => colors.primary(text),
2153
+ /**
2154
+ * Format success messages
2155
+ */
2156
+ success: (text) => colors.success(`\u2713 ${text}`),
2157
+ successBold: (text) => colors.successBold(`\u2713 ${text}`),
2158
+ /**
2159
+ * Format error messages
2160
+ */
2161
+ error: (text) => colors.error(`\u2717 ${text}`),
2162
+ errorBold: (text) => colors.errorBold(`\u2717 ${text}`),
2163
+ /**
2164
+ * Format warning messages
2165
+ */
2166
+ warning: (text) => colors.warning(`\u26A0 ${text}`),
2167
+ /**
2168
+ * Format info messages
2169
+ */
2170
+ info: (text) => colors.info(text),
2171
+ /**
2172
+ * Format labels (e.g., "User:", "Email:")
2173
+ */
2174
+ label: (text) => colors.gray(`${text}:`),
2175
+ /**
2176
+ * Format values
2177
+ */
2178
+ value: (text) => colors.highlight(text),
2556
2179
  /**
2557
- * Get the current git user email
2180
+ * Format counts and numbers
2558
2181
  */
2559
- async getCurrentUserEmail() {
2560
- try {
2561
- const email = await this.git.raw(["config", "user.email"]);
2562
- return email.trim() || null;
2563
- } catch {
2564
- logger.debug("Failed to get git user email");
2565
- return null;
2566
- }
2567
- }
2182
+ count: (num) => colors.highlightCyan(num.toString()),
2568
2183
  /**
2569
- * Get the current git user name
2184
+ * Format file paths
2570
2185
  */
2571
- async getCurrentUserName() {
2572
- try {
2573
- const name = await this.git.raw(["config", "user.name"]);
2574
- return name.trim() || null;
2575
- } catch {
2576
- logger.debug("Failed to get git user name");
2577
- return null;
2578
- }
2579
- }
2186
+ path: (text) => colors.info(text),
2580
2187
  /**
2581
- * Filter commits by current user
2188
+ * Format step indicators (e.g., "Step 1/5:")
2582
2189
  */
2583
- async getCommitsByCurrentUser(options = {}) {
2584
- const userEmail = await this.getCurrentUserEmail();
2585
- if (!userEmail) {
2586
- logger.warning("Could not determine git user email, returning all commits");
2587
- return this.getCommitsWithStats(options);
2588
- }
2589
- logger.debug(`Filtering commits by user: ${userEmail}`);
2590
- return this.getCommitsWithStats({
2591
- ...options,
2592
- author: userEmail
2593
- });
2594
- }
2595
- };
2596
- var gitService = new GitService();
2597
-
2598
- // src/services/github.service.ts
2599
- var execAsync3 = promisify3(exec3);
2600
- var GitHubService = class {
2601
- MAX_BODY_LENGTH = 5e3;
2602
- PR_SEARCH_FIELDS = "number,title,body,author,mergedAt,additions,deletions,changedFiles,url,labels";
2190
+ step: (current, total) => colors.primary(`[${current}/${total}]`),
2603
2191
  /**
2604
- * Check if GitHub CLI is installed and available
2192
+ * Format progress text
2605
2193
  */
2606
- async checkGitHubCLI() {
2607
- try {
2608
- await execAsync3("command gh --version");
2609
- return true;
2610
- } catch {
2611
- return false;
2612
- }
2613
- }
2194
+ progress: (text) => colors.infoDim(text),
2614
2195
  /**
2615
- * Validate that GitHub CLI is installed
2196
+ * Format emphasized text
2616
2197
  */
2617
- async ensureGitHubCLI() {
2618
- const isInstalled = await this.checkGitHubCLI();
2619
- if (!isInstalled) {
2620
- throw new GitHubError("GitHub CLI (gh) is not installed", {
2621
- hint: "Install from https://cli.github.com/"
2622
- });
2623
- }
2624
- }
2198
+ emphasis: (text) => colors.bold(text),
2625
2199
  /**
2626
- * Check if user is authenticated with GitHub CLI
2200
+ * Format de-emphasized/secondary text
2627
2201
  */
2628
- async checkAuthentication() {
2629
- try {
2630
- await execAsync3("command gh auth status");
2631
- return true;
2632
- } catch {
2633
- return false;
2634
- }
2635
- }
2202
+ secondary: (text) => colors.dim(text),
2636
2203
  /**
2637
- * Ensure user is authenticated with GitHub CLI
2204
+ * Format interactive elements (prompts, selections)
2638
2205
  */
2639
- async ensureAuthentication() {
2640
- const isAuthenticated = await this.checkAuthentication();
2641
- if (!isAuthenticated) {
2642
- throw new GitHubError("Not authenticated with GitHub", {
2643
- hint: 'Run "gh auth login" to authenticate'
2644
- });
2645
- }
2646
- }
2206
+ interactive: (text) => colors.primary(text),
2647
2207
  /**
2648
- * Validate that the current repository is hosted on GitHub
2208
+ * Format keys in key-value pairs
2649
2209
  */
2650
- async validateGitHubRepository() {
2651
- try {
2652
- await this.ensureGitHubCLI();
2653
- await this.ensureAuthentication();
2654
- await gitService.validateRepository();
2655
- const { stdout } = await execAsync3("command gh repo view --json url");
2656
- const data = JSON.parse(stdout);
2657
- if (!data.url) {
2658
- throw new GitHubError("This repository is not hosted on GitHub", {
2659
- hint: "Only GitHub repositories are currently supported for PR scanning"
2660
- });
2661
- }
2662
- } catch (error) {
2663
- if (error instanceof GitHubError || error instanceof GitError) {
2664
- throw error;
2665
- }
2666
- if (error.message?.includes("not a git repository")) {
2667
- throw new GitHubError("Not a git repository", {
2668
- hint: "Navigate to a git repository directory"
2669
- });
2670
- }
2671
- if (error.message?.includes("Could not resolve to a Repository")) {
2672
- throw new GitHubError("This repository is not hosted on GitHub", {
2673
- hint: "Only GitHub repositories are currently supported for PR scanning"
2674
- });
2675
- }
2676
- throw new GitHubError("Failed to validate GitHub repository", {
2677
- originalError: error.message
2678
- });
2210
+ key: (text) => colors.white(text)
2211
+ };
2212
+ var boxStyles = {
2213
+ success: {
2214
+ borderColor: "green",
2215
+ borderStyle: "round",
2216
+ padding: 1,
2217
+ margin: 1
2218
+ },
2219
+ error: {
2220
+ borderColor: "red",
2221
+ borderStyle: "round",
2222
+ padding: 1,
2223
+ margin: 1
2224
+ },
2225
+ warning: {
2226
+ borderColor: "yellow",
2227
+ borderStyle: "round",
2228
+ padding: 1,
2229
+ margin: 1
2230
+ },
2231
+ info: {
2232
+ borderColor: "cyan",
2233
+ borderStyle: "round",
2234
+ padding: 1,
2235
+ margin: 1
2236
+ },
2237
+ plain: {
2238
+ borderColor: "gray",
2239
+ borderStyle: "round",
2240
+ padding: 1,
2241
+ margin: 1
2242
+ }
2243
+ };
2244
+ var tableStyles = {
2245
+ default: {
2246
+ style: {
2247
+ head: [],
2248
+ border: ["gray"]
2249
+ }
2250
+ },
2251
+ compact: {
2252
+ style: {
2253
+ head: [],
2254
+ border: ["dim"],
2255
+ compact: true
2256
+ }
2257
+ }
2258
+ };
2259
+ var sizeIndicators = {
2260
+ small: colors.success("\u25CF"),
2261
+ medium: colors.warning("\u25CF"),
2262
+ large: colors.error("\u25CF")
2263
+ };
2264
+ function getSizeIndicator(insertions, deletions) {
2265
+ const total = insertions + deletions;
2266
+ if (total < 50) {
2267
+ return `${sizeIndicators.small} Small`;
2268
+ } else if (total < 200) {
2269
+ return `${sizeIndicators.medium} Medium`;
2270
+ } else {
2271
+ return `${sizeIndicators.large} Large`;
2272
+ }
2273
+ }
2274
+ function formatDiffStats(insertions, deletions) {
2275
+ return `${colors.success(`+${insertions}`)} ${colors.error(`-${deletions}`)}`;
2276
+ }
2277
+
2278
+ // src/commands/auth.ts
2279
+ async function authCommand(subcommand) {
2280
+ if (!subcommand || subcommand === "login") {
2281
+ await authLogin();
2282
+ } else if (subcommand === "status") {
2283
+ await authStatus();
2284
+ } else if (subcommand === "bitbucket") {
2285
+ await authBitbucket();
2286
+ } else if (subcommand === "gitlab") {
2287
+ await authGitLab();
2288
+ } else if (subcommand === "atlassian") {
2289
+ await authAtlassian();
2290
+ } else {
2291
+ logger.error(`Unknown auth subcommand: ${subcommand}`);
2292
+ logger.info("Available subcommands: login, status, bitbucket, gitlab, atlassian");
2293
+ process.exit(1);
2294
+ }
2295
+ }
2296
+ async function authLogin() {
2297
+ logger.log("");
2298
+ logger.info("Authenticating with Bragduck...");
2299
+ logger.log("");
2300
+ const isAuthenticated = await storageService.isServiceAuthenticated("bragduck");
2301
+ if (isAuthenticated) {
2302
+ const userInfo = authService.getUserInfo();
2303
+ if (userInfo) {
2304
+ logger.log(
2305
+ boxen(
2306
+ `${chalk3.yellow("Already authenticated!")}
2307
+
2308
+ ${chalk3.gray("User:")} ${userInfo.name}
2309
+ ${chalk3.gray("Email:")} ${userInfo.email}
2310
+
2311
+ ${chalk3.dim("Run")} ${chalk3.cyan("bragduck logout")} ${chalk3.dim("to sign out")}`,
2312
+ {
2313
+ padding: 1,
2314
+ margin: 1,
2315
+ borderStyle: "round",
2316
+ borderColor: "yellow"
2317
+ }
2318
+ )
2319
+ );
2320
+ logger.log("");
2321
+ return;
2679
2322
  }
2680
2323
  }
2681
- /**
2682
- * Get GitHub repository information
2683
- */
2684
- async getRepositoryInfo() {
2685
- try {
2686
- await this.ensureGitHubCLI();
2687
- const { stdout } = await execAsync3(
2688
- "command gh repo view --json owner,name,url,nameWithOwner"
2689
- );
2690
- const data = JSON.parse(stdout);
2691
- return {
2692
- owner: data.owner.login,
2693
- name: data.name,
2694
- fullName: data.nameWithOwner,
2695
- url: data.url,
2696
- isGitHub: true
2697
- };
2698
- } catch (error) {
2699
- if (error instanceof GitHubError || error instanceof GitError) {
2700
- throw error;
2701
- }
2702
- throw new GitHubError("Failed to get GitHub repository information", {
2703
- originalError: error.message
2704
- });
2324
+ try {
2325
+ const userInfo = await authService.login();
2326
+ logger.log("");
2327
+ logger.log(
2328
+ boxen(
2329
+ `${chalk3.green.bold("\u2713 Successfully authenticated!")}
2330
+
2331
+ ${chalk3.gray("Welcome,")} ${chalk3.cyan(userInfo.name)}
2332
+ ${chalk3.gray("Email:")} ${userInfo.email}
2333
+
2334
+ ${chalk3.dim("Use")} ${chalk3.cyan("bragduck sync")} ${chalk3.dim("to create brags")}`,
2335
+ {
2336
+ padding: 1,
2337
+ margin: 1,
2338
+ borderStyle: "round",
2339
+ borderColor: "green"
2340
+ }
2341
+ )
2342
+ );
2343
+ logger.log("");
2344
+ } catch (error) {
2345
+ const err = error;
2346
+ logger.log("");
2347
+ logger.log(
2348
+ boxen(`${chalk3.red.bold("\u2717 Authentication Failed")}
2349
+
2350
+ ${err.message}`, {
2351
+ padding: 1,
2352
+ margin: 1,
2353
+ borderStyle: "round",
2354
+ borderColor: "red"
2355
+ })
2356
+ );
2357
+ process.exit(1);
2358
+ }
2359
+ }
2360
+ async function authStatus() {
2361
+ logger.log("");
2362
+ logger.info("Authentication Status:");
2363
+ logger.log("");
2364
+ const services = await storageService.getAuthenticatedServices();
2365
+ const bragduckAuth = await storageService.isServiceAuthenticated("bragduck");
2366
+ if (bragduckAuth) {
2367
+ const userInfo = authService.getUserInfo();
2368
+ logger.info(`${colors.success("\u2713")} Bragduck: ${userInfo?.name || "Authenticated"}`);
2369
+ } else {
2370
+ logger.info(`${colors.error("\u2717")} Bragduck: Not authenticated`);
2371
+ }
2372
+ const ghStatus = await githubService.getAuthStatus();
2373
+ if (ghStatus.installed) {
2374
+ if (ghStatus.authenticated) {
2375
+ logger.info(`${colors.success("\u2713")} GitHub CLI (gh): Authenticated`);
2376
+ } else {
2377
+ logger.info(`${colors.error("\u2717")} GitHub CLI (gh): Not authenticated`);
2378
+ logger.info(theme.secondary(" Run: gh auth login"));
2705
2379
  }
2380
+ } else {
2381
+ logger.info(`${colors.error("\u2717")} GitHub CLI (gh): Not installed`);
2382
+ logger.info(theme.secondary(" Install from: https://cli.github.com"));
2706
2383
  }
2707
- /**
2708
- * Get the current authenticated GitHub user
2709
- */
2710
- async getCurrentGitHubUser() {
2711
- try {
2712
- const { stdout } = await execAsync3("command gh api user --jq .login");
2713
- return stdout.trim() || null;
2714
- } catch {
2715
- logger.debug("Failed to get GitHub user");
2716
- return null;
2384
+ for (const service of services) {
2385
+ if (service !== "bragduck") {
2386
+ logger.info(`${colors.success("\u2713")} ${service}: Authenticated`);
2717
2387
  }
2718
2388
  }
2719
- /**
2720
- * Fetch merged PRs from GitHub
2721
- */
2722
- async getMergedPRs(options = {}) {
2723
- const { days = 30, limit, author } = options;
2724
- try {
2725
- await this.ensureGitHubCLI();
2726
- await this.ensureAuthentication();
2727
- logger.debug(`Fetching merged PRs from the last ${days} days`);
2728
- const since = /* @__PURE__ */ new Date();
2729
- since.setDate(since.getDate() - days);
2730
- const sinceDate = since.toISOString().split("T")[0];
2731
- let searchQuery = `merged:>=${sinceDate}`;
2732
- if (author) {
2733
- searchQuery += ` author:${author}`;
2734
- }
2735
- const limitArg = limit ? `--limit ${limit}` : "";
2736
- const command = `command gh pr list --state merged --json ${this.PR_SEARCH_FIELDS} --search "${searchQuery}" ${limitArg}`;
2737
- logger.debug(`Running: ${command}`);
2738
- const { stdout } = await execAsync3(command);
2739
- const prs = JSON.parse(stdout);
2740
- logger.debug(`Found ${prs.length} merged PRs`);
2741
- return prs;
2742
- } catch (error) {
2743
- if (error instanceof GitHubError || error instanceof GitError) {
2744
- throw error;
2389
+ logger.log("");
2390
+ if (services.length === 0) {
2391
+ logger.info(`Run ${theme.command("bragduck auth login")} to authenticate with Bragduck`);
2392
+ logger.log("");
2393
+ }
2394
+ }
2395
+ async function authBitbucket() {
2396
+ logger.log("");
2397
+ logger.log(
2398
+ boxen(
2399
+ theme.info("Bitbucket API Token Authentication") + "\n\nCreate an API Token at:\n" + colors.highlight("https://bitbucket.org/account/settings/api-token/new") + "\n\nRequired scopes:\n \u2022 pullrequest:read\n \u2022 repository:read\n \u2022 account:read\n\n" + theme.warning("Note: API tokens expire (max 1 year)"),
2400
+ boxStyles.info
2401
+ )
2402
+ );
2403
+ logger.log("");
2404
+ try {
2405
+ const email = await input({
2406
+ message: "Atlassian account email:",
2407
+ validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
2408
+ });
2409
+ const apiToken = await input({
2410
+ message: "API Token:",
2411
+ validate: (value) => value.length > 0 ? true : "API token cannot be empty"
2412
+ });
2413
+ const tokenExpiry = await input({
2414
+ message: "Token expiry date (YYYY-MM-DD, optional):",
2415
+ default: "",
2416
+ validate: (value) => {
2417
+ if (!value) return true;
2418
+ const date = new Date(value);
2419
+ return !isNaN(date.getTime()) ? true : "Please enter a valid date (YYYY-MM-DD)";
2745
2420
  }
2746
- throw new GitHubError("Failed to fetch PRs from GitHub", {
2747
- originalError: error.message,
2748
- days
2749
- });
2421
+ });
2422
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
2423
+ const response = await fetch("https://api.bitbucket.org/2.0/user", {
2424
+ headers: { Authorization: `Basic ${auth}` }
2425
+ });
2426
+ if (!response.ok) {
2427
+ logger.log("");
2428
+ logger.log(
2429
+ boxen(
2430
+ theme.error("\u2717 Authentication Failed") + "\n\nInvalid email or API token",
2431
+ boxStyles.error
2432
+ )
2433
+ );
2434
+ logger.log("");
2435
+ process.exit(1);
2436
+ }
2437
+ const user = await response.json();
2438
+ const credentials = {
2439
+ accessToken: apiToken,
2440
+ username: email
2441
+ // Store email in username field
2442
+ };
2443
+ if (tokenExpiry) {
2444
+ const expiryDate = new Date(tokenExpiry);
2445
+ credentials.expiresAt = expiryDate.getTime();
2750
2446
  }
2447
+ await storageService.setServiceCredentials("bitbucket", credentials);
2448
+ logger.log("");
2449
+ logger.log(
2450
+ boxen(
2451
+ theme.success("\u2713 Successfully authenticated with Bitbucket") + `
2452
+
2453
+ Email: ${email}
2454
+ User: ${user.display_name}
2455
+ ` + (tokenExpiry ? `Expires: ${tokenExpiry}` : ""),
2456
+ boxStyles.success
2457
+ )
2458
+ );
2459
+ logger.log("");
2460
+ } catch (error) {
2461
+ const err = error;
2462
+ logger.log("");
2463
+ logger.log(
2464
+ boxen(
2465
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2466
+ boxStyles.error
2467
+ )
2468
+ );
2469
+ logger.log("");
2470
+ process.exit(1);
2751
2471
  }
2752
- /**
2753
- * Get merged PRs by current user (PRs authored by the current user)
2754
- */
2755
- async getPRsByCurrentUser(options = {}) {
2756
- const currentUser = await this.getCurrentGitHubUser();
2757
- if (!currentUser) {
2758
- logger.warning("Could not determine GitHub user, returning all PRs");
2759
- return this.getMergedPRs(options);
2472
+ }
2473
+ async function authGitLab() {
2474
+ logger.log("");
2475
+ logger.log(
2476
+ boxen(
2477
+ theme.info("GitLab Personal Access Token Authentication") + "\n\nCreate a Personal Access Token at:\n" + colors.highlight("https://gitlab.com/-/profile/personal_access_tokens") + "\n\nRequired scopes:\n \u2022 api (full API access)\n \u2022 read_api (read API)\n \u2022 read_user (read user info)\n\n" + theme.warning("For self-hosted GitLab, check your instance docs"),
2478
+ boxStyles.info
2479
+ )
2480
+ );
2481
+ logger.log("");
2482
+ try {
2483
+ const instanceUrl = await input({
2484
+ message: "GitLab instance URL (press Enter for gitlab.com):",
2485
+ default: "https://gitlab.com"
2486
+ });
2487
+ const accessToken = await input({
2488
+ message: "Personal Access Token:",
2489
+ validate: (value) => value.length > 0 ? true : "Token cannot be empty"
2490
+ });
2491
+ const testUrl = `${instanceUrl.replace(/\/$/, "")}/api/v4/user`;
2492
+ const response = await fetch(testUrl, {
2493
+ headers: { "PRIVATE-TOKEN": accessToken }
2494
+ });
2495
+ if (!response.ok) {
2496
+ logger.log("");
2497
+ logger.log(
2498
+ boxen(
2499
+ theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL or access token",
2500
+ boxStyles.error
2501
+ )
2502
+ );
2503
+ logger.log("");
2504
+ process.exit(1);
2505
+ }
2506
+ const user = await response.json();
2507
+ const credentials = {
2508
+ accessToken,
2509
+ instanceUrl: instanceUrl === "https://gitlab.com" ? void 0 : instanceUrl
2510
+ };
2511
+ await storageService.setServiceCredentials("gitlab", credentials);
2512
+ logger.log("");
2513
+ logger.log(
2514
+ boxen(
2515
+ theme.success("\u2713 Successfully authenticated with GitLab") + `
2516
+
2517
+ Instance: ${instanceUrl}
2518
+ User: ${user.name} (@${user.username})`,
2519
+ boxStyles.success
2520
+ )
2521
+ );
2522
+ logger.log("");
2523
+ } catch (error) {
2524
+ const err = error;
2525
+ logger.log("");
2526
+ logger.log(
2527
+ boxen(
2528
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2529
+ boxStyles.error
2530
+ )
2531
+ );
2532
+ logger.log("");
2533
+ process.exit(1);
2534
+ }
2535
+ }
2536
+ async function authAtlassian() {
2537
+ logger.log("");
2538
+ logger.log(
2539
+ boxen(
2540
+ 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",
2541
+ boxStyles.info
2542
+ )
2543
+ );
2544
+ logger.log("");
2545
+ try {
2546
+ const instanceUrl = await input({
2547
+ message: "Atlassian instance URL (e.g., company.atlassian.net):",
2548
+ validate: (value) => value.length > 0 ? true : "Instance URL cannot be empty"
2549
+ });
2550
+ const email = await input({
2551
+ message: "Atlassian account email:",
2552
+ validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
2553
+ });
2554
+ const apiToken = await input({
2555
+ message: "API Token:",
2556
+ validate: (value) => value.length > 0 ? true : "API token cannot be empty"
2557
+ });
2558
+ const testInstanceUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
2559
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
2560
+ const response = await fetch(`${testInstanceUrl}/rest/api/2/myself`, {
2561
+ headers: { Authorization: `Basic ${auth}` }
2562
+ });
2563
+ if (!response.ok) {
2564
+ logger.log("");
2565
+ logger.log(
2566
+ boxen(
2567
+ 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)",
2568
+ boxStyles.error
2569
+ )
2570
+ );
2571
+ logger.log("");
2572
+ process.exit(1);
2760
2573
  }
2761
- logger.debug(`Filtering PRs by author: ${currentUser}`);
2762
- return this.getMergedPRs({
2763
- ...options,
2764
- author: currentUser
2574
+ const user = await response.json();
2575
+ const credentials = {
2576
+ accessToken: apiToken,
2577
+ username: email,
2578
+ instanceUrl: instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`
2579
+ };
2580
+ await storageService.setServiceCredentials("jira", credentials);
2581
+ await storageService.setServiceCredentials("confluence", credentials);
2582
+ await storageService.setServiceCredentials("bitbucket", {
2583
+ ...credentials,
2584
+ instanceUrl: "https://api.bitbucket.org"
2585
+ // Bitbucket uses different API base
2765
2586
  });
2766
- }
2767
- /**
2768
- * Transform a GitHub PR into a GitCommit structure
2769
- * This allows PR data to work with existing commit-based UI and API
2770
- */
2771
- transformPRToCommit(pr) {
2772
- const title = pr.title;
2773
- const body = pr.body || "";
2774
- const truncatedBody = body.length > this.MAX_BODY_LENGTH ? body.substring(0, this.MAX_BODY_LENGTH) + "...[truncated]" : body;
2775
- const message = truncatedBody ? `${title}
2587
+ logger.log("");
2588
+ logger.log(
2589
+ boxen(
2590
+ theme.success("\u2713 Successfully authenticated with Atlassian") + `
2776
2591
 
2777
- ${truncatedBody}` : title;
2778
- return {
2779
- sha: `pr-${pr.number}`,
2780
- message,
2781
- author: pr.author.login,
2782
- authorEmail: "",
2783
- // Not available from gh PR API
2784
- date: pr.mergedAt,
2785
- url: pr.url,
2786
- diffStats: {
2787
- filesChanged: pr.changedFiles,
2788
- insertions: pr.additions,
2789
- deletions: pr.deletions
2790
- }
2791
- };
2592
+ Instance: ${instanceUrl}
2593
+ User: ${user.displayName}
2594
+ Email: ${user.emailAddress}
2595
+
2596
+ Services configured:
2597
+ \u2022 Jira
2598
+ \u2022 Confluence
2599
+ \u2022 Bitbucket`,
2600
+ boxStyles.success
2601
+ )
2602
+ );
2603
+ logger.log("");
2604
+ } catch (error) {
2605
+ const err = error;
2606
+ logger.log("");
2607
+ logger.log(
2608
+ boxen(
2609
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2610
+ boxStyles.error
2611
+ )
2612
+ );
2613
+ logger.log("");
2614
+ process.exit(1);
2792
2615
  }
2616
+ }
2617
+
2618
+ // src/commands/sync.ts
2619
+ init_esm_shims();
2620
+
2621
+ // node_modules/@inquirer/core/dist/esm/lib/errors.mjs
2622
+ init_esm_shims();
2623
+ var CancelPromptError = class extends Error {
2624
+ name = "CancelPromptError";
2625
+ message = "Prompt was canceled";
2793
2626
  };
2794
- var githubService = new GitHubService();
2627
+
2628
+ // src/commands/sync.ts
2629
+ init_api_service();
2630
+ init_storage_service();
2631
+ init_auth_service();
2632
+ init_env_loader();
2633
+ init_config_loader();
2634
+ import { select as select2 } from "@inquirer/prompts";
2635
+ import boxen6 from "boxen";
2636
+
2637
+ // src/sync/adapter-factory.ts
2638
+ init_esm_shims();
2795
2639
 
2796
2640
  // src/sync/github-adapter.ts
2641
+ init_esm_shims();
2797
2642
  var GitHubSyncAdapter = class {
2798
2643
  name = "github";
2799
2644
  async validate() {
@@ -2846,9 +2691,9 @@ init_esm_shims();
2846
2691
  init_errors();
2847
2692
  init_logger();
2848
2693
  init_storage_service();
2849
- import { exec as exec4 } from "child_process";
2850
- import { promisify as promisify4 } from "util";
2851
- var execAsync4 = promisify4(exec4);
2694
+ import { exec as exec3 } from "child_process";
2695
+ import { promisify as promisify3 } from "util";
2696
+ var execAsync3 = promisify3(exec3);
2852
2697
  var BitbucketService = class {
2853
2698
  BITBUCKET_API_BASE = "https://api.bitbucket.org/2.0";
2854
2699
  MAX_DESCRIPTION_LENGTH = 5e3;
@@ -2933,7 +2778,7 @@ var BitbucketService = class {
2933
2778
  */
2934
2779
  async getRepoFromGit() {
2935
2780
  try {
2936
- const { stdout } = await execAsync4("command git remote get-url origin");
2781
+ const { stdout } = await execAsync3("command git remote get-url origin");
2937
2782
  const remoteUrl = stdout.trim();
2938
2783
  const parsed = this.parseRemoteUrl(remoteUrl);
2939
2784
  if (!parsed) {
@@ -3128,10 +2973,10 @@ init_esm_shims();
3128
2973
  init_errors();
3129
2974
  init_logger();
3130
2975
  init_storage_service();
3131
- import { exec as exec5 } from "child_process";
3132
- import { promisify as promisify5 } from "util";
2976
+ import { exec as exec4 } from "child_process";
2977
+ import { promisify as promisify4 } from "util";
3133
2978
  import { URLSearchParams as URLSearchParams3 } from "url";
3134
- var execAsync5 = promisify5(exec5);
2979
+ var execAsync4 = promisify4(exec4);
3135
2980
  var GitLabService = class {
3136
2981
  DEFAULT_INSTANCE = "https://gitlab.com";
3137
2982
  MAX_DESCRIPTION_LENGTH = 5e3;
@@ -3217,7 +3062,7 @@ var GitLabService = class {
3217
3062
  */
3218
3063
  async getProjectFromGit() {
3219
3064
  try {
3220
- const { stdout } = await execAsync5("git remote get-url origin");
3065
+ const { stdout } = await execAsync4("git remote get-url origin");
3221
3066
  const remoteUrl = stdout.trim();
3222
3067
  const parsed = this.parseRemoteUrl(remoteUrl);
3223
3068
  if (!parsed) {
@@ -4109,7 +3954,7 @@ init_logger();
4109
3954
 
4110
3955
  // src/ui/prompts.ts
4111
3956
  init_esm_shims();
4112
- import { checkbox, confirm, input as input2, select as select2, editor } from "@inquirer/prompts";
3957
+ import { checkbox, confirm, input as input2, select, editor } from "@inquirer/prompts";
4113
3958
  import boxen4 from "boxen";
4114
3959
 
4115
3960
  // src/ui/formatters.ts
@@ -4265,7 +4110,7 @@ async function promptDaysToScan(defaultDays = 30) {
4265
4110
  { name: "90 days", value: "90", description: "Last 3 months" },
4266
4111
  { name: "Custom", value: "custom", description: "Enter custom number of days" }
4267
4112
  ];
4268
- const selected = await select2({
4113
+ const selected = await select({
4269
4114
  message: "How many days back should we scan for PRs?",
4270
4115
  choices,
4271
4116
  default: "30"
@@ -4297,7 +4142,7 @@ async function promptSortOption() {
4297
4142
  { name: "By files (most files)", value: "files", description: "Most files changed" },
4298
4143
  { name: "No sorting", value: "none", description: "Keep original order" }
4299
4144
  ];
4300
- return await select2({
4145
+ return await select({
4301
4146
  message: "How would you like to sort the PRs?",
4302
4147
  choices,
4303
4148
  default: "date"
@@ -4311,7 +4156,7 @@ async function promptSelectOrganisation(organisations) {
4311
4156
  value: org.id
4312
4157
  }))
4313
4158
  ];
4314
- const selected = await select2({
4159
+ const selected = await select({
4315
4160
  message: "Attach brags to which company?",
4316
4161
  choices,
4317
4162
  default: "none"
@@ -4361,7 +4206,7 @@ ${theme.label("PR Link")} ${colors.link(prUrl)}`;
4361
4206
  }
4362
4207
  console.log(boxen4(bragDetails, boxStyles.info));
4363
4208
  console.log("");
4364
- const action = await select2({
4209
+ const action = await select({
4365
4210
  message: `What would you like to do with this brag?`,
4366
4211
  choices: [
4367
4212
  { name: "\u2713 Accept", value: "accept", description: "Add this brag as-is" },
@@ -4446,70 +4291,371 @@ async function ensureAuthenticated() {
4446
4291
  if (!shouldAuth) {
4447
4292
  logger.log("");
4448
4293
  logger.info(
4449
- theme.secondary("Authentication skipped. Run ") + theme.command("bragduck init") + theme.secondary(" when you're ready to authenticate.")
4294
+ theme.secondary("Authentication skipped. Run ") + theme.command("bragduck auth login") + theme.secondary(" when you're ready to authenticate.")
4295
+ );
4296
+ logger.log("");
4297
+ return false;
4298
+ }
4299
+ try {
4300
+ await initCommand();
4301
+ return true;
4302
+ } catch {
4303
+ return false;
4304
+ }
4305
+ }
4306
+
4307
+ // src/ui/spinners.ts
4308
+ init_esm_shims();
4309
+ import ora2 from "ora";
4310
+ function createSpinner(text) {
4311
+ return ora2({
4312
+ text,
4313
+ color: "cyan",
4314
+ spinner: "dots"
4315
+ });
4316
+ }
4317
+ function createStepSpinner(currentStep, totalSteps, text) {
4318
+ const stepIndicator = theme.step(currentStep, totalSteps);
4319
+ return ora2({
4320
+ text: `${stepIndicator} ${text}`,
4321
+ color: "cyan",
4322
+ spinner: "dots"
4323
+ });
4324
+ }
4325
+ function fetchingBragsSpinner() {
4326
+ return createSpinner("Fetching your brags...");
4327
+ }
4328
+ function succeedSpinner(spinner, text) {
4329
+ if (text) {
4330
+ spinner.succeed(colors.success(text));
4331
+ } else {
4332
+ spinner.succeed();
4333
+ }
4334
+ }
4335
+ function failSpinner(spinner, text) {
4336
+ if (text) {
4337
+ spinner.fail(colors.error(text));
4338
+ } else {
4339
+ spinner.fail();
4340
+ }
4341
+ }
4342
+ function succeedStepSpinner(spinner, currentStep, totalSteps, text) {
4343
+ const stepIndicator = theme.step(currentStep, totalSteps);
4344
+ spinner.succeed(`${stepIndicator} ${colors.success(text)}`);
4345
+ }
4346
+ function failStepSpinner(spinner, currentStep, totalSteps, text) {
4347
+ const stepIndicator = theme.step(currentStep, totalSteps);
4348
+ spinner.fail(`${stepIndicator} ${colors.error(text)}`);
4349
+ }
4350
+
4351
+ // src/commands/sync.ts
4352
+ async function promptSelectService() {
4353
+ const allServices = [
4354
+ "github",
4355
+ "gitlab",
4356
+ "bitbucket",
4357
+ "atlassian",
4358
+ "jira",
4359
+ "confluence"
4360
+ ];
4361
+ const authenticatedServices = await storageService.getAuthenticatedServices();
4362
+ const authenticatedSyncServices = authenticatedServices.filter(
4363
+ (service) => service !== "bragduck" && allServices.includes(service)
4364
+ );
4365
+ const serviceChoices = await Promise.all(
4366
+ allServices.map(async (service) => {
4367
+ const isAuth = await storageService.isServiceAuthenticated(service);
4368
+ const indicator = isAuth ? "\u2713" : "\u2717";
4369
+ const serviceLabel = service.charAt(0).toUpperCase() + service.slice(1);
4370
+ return {
4371
+ name: `${indicator} ${serviceLabel}`,
4372
+ value: service,
4373
+ description: isAuth ? "Authenticated" : "Not authenticated"
4374
+ };
4375
+ })
4376
+ );
4377
+ if (authenticatedSyncServices.length > 0) {
4378
+ const serviceNames = authenticatedSyncServices.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(", ");
4379
+ serviceChoices.push({
4380
+ name: `\u2713 All authenticated services (${serviceNames})`,
4381
+ value: "all",
4382
+ description: `Sync all ${authenticatedSyncServices.length} authenticated service${authenticatedSyncServices.length > 1 ? "s" : ""}`
4383
+ });
4384
+ }
4385
+ const selected = await select2({
4386
+ message: "Select a service to sync:",
4387
+ choices: serviceChoices
4388
+ });
4389
+ return selected;
4390
+ }
4391
+ async function syncSingleService(sourceType, options, TOTAL_STEPS) {
4392
+ const adapter = AdapterFactory.getAdapter(sourceType);
4393
+ const repoSpinner = createStepSpinner(2, TOTAL_STEPS, "Validating repository");
4394
+ repoSpinner.start();
4395
+ await adapter.validate();
4396
+ const repoInfo = await adapter.getRepositoryInfo();
4397
+ succeedStepSpinner(repoSpinner, 2, TOTAL_STEPS, `Repository: ${theme.value(repoInfo.fullName)}`);
4398
+ logger.log("");
4399
+ let days = options.days;
4400
+ if (!days) {
4401
+ const defaultDays = storageService.getConfig("defaultCommitDays");
4402
+ days = await promptDaysToScan(defaultDays);
4403
+ logger.log("");
4404
+ }
4405
+ const fetchSpinner = createStepSpinner(
4406
+ 3,
4407
+ TOTAL_STEPS,
4408
+ `Fetching work items from the last ${days} days`
4409
+ );
4410
+ fetchSpinner.start();
4411
+ const workItems = await adapter.fetchWorkItems({
4412
+ days,
4413
+ author: options.all ? void 0 : await adapter.getCurrentUser() || void 0
4414
+ });
4415
+ if (workItems.length === 0) {
4416
+ failStepSpinner(fetchSpinner, 3, TOTAL_STEPS, `No work items found in the last ${days} days`);
4417
+ logger.log("");
4418
+ logger.info("Try increasing the number of days or check your activity");
4419
+ return { created: 0, skipped: 0 };
4420
+ }
4421
+ succeedStepSpinner(
4422
+ fetchSpinner,
4423
+ 3,
4424
+ TOTAL_STEPS,
4425
+ `Found ${theme.count(workItems.length)} work item${workItems.length > 1 ? "s" : ""}`
4426
+ );
4427
+ logger.log("");
4428
+ logger.log(formatCommitStats(workItems));
4429
+ logger.log("");
4430
+ let sortedCommits = [...workItems];
4431
+ if (workItems.length > 1) {
4432
+ const sortOption = await promptSortOption();
4433
+ logger.log("");
4434
+ if (sortOption === "date") {
4435
+ sortedCommits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
4436
+ } else if (sortOption === "size") {
4437
+ sortedCommits.sort((a, b) => {
4438
+ const sizeA = (a.diffStats?.insertions || 0) + (a.diffStats?.deletions || 0);
4439
+ const sizeB = (b.diffStats?.insertions || 0) + (b.diffStats?.deletions || 0);
4440
+ return sizeB - sizeA;
4441
+ });
4442
+ } else if (sortOption === "files") {
4443
+ sortedCommits.sort((a, b) => {
4444
+ const filesA = a.diffStats?.filesChanged || 0;
4445
+ const filesB = b.diffStats?.filesChanged || 0;
4446
+ return filesB - filesA;
4447
+ });
4448
+ }
4449
+ }
4450
+ const selectedShas = await promptSelectCommits(sortedCommits);
4451
+ if (selectedShas.length === 0) {
4452
+ logger.log("");
4453
+ logger.info(theme.secondary("No work items selected. Sync cancelled."));
4454
+ logger.log("");
4455
+ return { created: 0, skipped: 0 };
4456
+ }
4457
+ const selectedCommits = sortedCommits.filter((c) => selectedShas.includes(c.sha));
4458
+ logger.log(formatSelectionSummary(selectedCommits.length, selectedCommits));
4459
+ logger.log("");
4460
+ const existingBrags = await apiService.listBrags({ limit: 100 });
4461
+ logger.debug(`Fetched ${existingBrags.brags.length} existing brags`);
4462
+ const existingUrls = new Set(existingBrags.brags.flatMap((b) => b.attachments || []));
4463
+ logger.debug(`Existing URLs in attachments: ${existingUrls.size}`);
4464
+ const duplicates = selectedCommits.filter((c) => c.url && existingUrls.has(c.url));
4465
+ const newCommits = selectedCommits.filter((c) => !c.url || !existingUrls.has(c.url));
4466
+ logger.debug(`Duplicates: ${duplicates.length}, New: ${newCommits.length}`);
4467
+ if (duplicates.length > 0) {
4468
+ logger.log("");
4469
+ logger.info(
4470
+ colors.warning(
4471
+ `${duplicates.length} work item${duplicates.length > 1 ? "s" : ""} already added to Bragduck - skipping`
4472
+ )
4473
+ );
4474
+ logger.log("");
4475
+ }
4476
+ if (newCommits.length === 0) {
4477
+ logger.log("");
4478
+ logger.info(
4479
+ theme.secondary("All selected work items already exist in Bragduck. Nothing to refine.")
4480
+ );
4481
+ logger.log("");
4482
+ return { created: 0, skipped: duplicates.length };
4483
+ }
4484
+ const refineSpinner = createStepSpinner(
4485
+ 4,
4486
+ TOTAL_STEPS,
4487
+ `Refining ${theme.count(newCommits.length)} work item${newCommits.length > 1 ? "s" : ""} with AI`
4488
+ );
4489
+ refineSpinner.start();
4490
+ const refineRequest = {
4491
+ brags: newCommits.map((c) => ({
4492
+ text: c.message,
4493
+ date: c.date,
4494
+ title: c.message.split("\n")[0]
4495
+ }))
4496
+ };
4497
+ const refineResponse = await apiService.refineBrags(refineRequest);
4498
+ let refinedBrags = refineResponse.refined_brags;
4499
+ succeedStepSpinner(refineSpinner, 4, TOTAL_STEPS, "Work items refined successfully");
4500
+ logger.log("");
4501
+ logger.info("Preview of refined brags:");
4502
+ logger.log("");
4503
+ logger.log(formatRefinedCommitsTable(refinedBrags, newCommits));
4504
+ logger.log("");
4505
+ const acceptedBrags = await promptReviewBrags(refinedBrags, newCommits);
4506
+ if (acceptedBrags.length === 0) {
4507
+ logger.log("");
4508
+ logger.info(theme.secondary("No brags selected for creation. Sync cancelled."));
4509
+ logger.log("");
4510
+ return { created: 0, skipped: duplicates.length };
4511
+ }
4512
+ logger.log("");
4513
+ let selectedOrgId = null;
4514
+ const userInfo = authService.getUserInfo();
4515
+ if (userInfo?.id) {
4516
+ try {
4517
+ const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
4518
+ if (orgsResponse.items.length > 0) {
4519
+ selectedOrgId = await promptSelectOrganisation(orgsResponse.items);
4520
+ logger.log("");
4521
+ }
4522
+ } catch {
4523
+ logger.debug("Failed to fetch organisations, skipping org selection");
4524
+ }
4525
+ }
4526
+ const createSpinner2 = createStepSpinner(
4527
+ 5,
4528
+ TOTAL_STEPS,
4529
+ `Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
4530
+ );
4531
+ createSpinner2.start();
4532
+ const createRequest = {
4533
+ brags: acceptedBrags.map((refined, index) => {
4534
+ const originalCommit = newCommits[index];
4535
+ return {
4536
+ commit_sha: originalCommit?.sha || `brag-${index}`,
4537
+ title: refined.refined_title,
4538
+ description: refined.refined_description,
4539
+ tags: refined.suggested_tags,
4540
+ repository: repoInfo.url,
4541
+ date: originalCommit?.date || "",
4542
+ commit_url: originalCommit?.url || "",
4543
+ impact_score: refined.suggested_impactLevel,
4544
+ impact_description: refined.impact_description,
4545
+ attachments: originalCommit?.url ? [originalCommit.url] : [],
4546
+ orgId: selectedOrgId || void 0,
4547
+ // External fields for non-git sources (Jira, Confluence, etc.)
4548
+ externalId: originalCommit?.externalId,
4549
+ externalType: originalCommit?.externalType,
4550
+ externalSource: originalCommit?.externalSource,
4551
+ externalUrl: originalCommit?.externalUrl
4552
+ };
4553
+ })
4554
+ };
4555
+ const createResponse = await apiService.createBrags(createRequest);
4556
+ succeedStepSpinner(
4557
+ createSpinner2,
4558
+ 5,
4559
+ TOTAL_STEPS,
4560
+ `Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
4561
+ );
4562
+ logger.log("");
4563
+ return { created: createResponse.created, skipped: duplicates.length };
4564
+ }
4565
+ async function syncAllAuthenticatedServices(options) {
4566
+ const TOTAL_STEPS = 5;
4567
+ const allServices = [
4568
+ "github",
4569
+ "gitlab",
4570
+ "bitbucket",
4571
+ "atlassian",
4572
+ "jira",
4573
+ "confluence"
4574
+ ];
4575
+ const authenticatedServices = await storageService.getAuthenticatedServices();
4576
+ const servicesToSync = authenticatedServices.filter(
4577
+ (service) => service !== "bragduck" && allServices.includes(service)
4578
+ );
4579
+ if (servicesToSync.length === 0) {
4580
+ logger.log("");
4581
+ logger.error("No authenticated services found.");
4582
+ logger.log("");
4583
+ logger.info("Authenticate a service first:");
4584
+ logger.info(` ${theme.command("bragduck auth github")}`);
4585
+ logger.info(` ${theme.command("bragduck auth atlassian")}`);
4586
+ return;
4587
+ }
4588
+ logger.info(
4589
+ theme.highlight(
4590
+ `Syncing ${servicesToSync.length} authenticated service${servicesToSync.length > 1 ? "s" : ""}...`
4591
+ )
4592
+ );
4593
+ logger.log("");
4594
+ const results = [];
4595
+ for (let i = 0; i < servicesToSync.length; i++) {
4596
+ const service = servicesToSync[i];
4597
+ const serviceLabel = service.charAt(0).toUpperCase() + service.slice(1);
4598
+ logger.log(
4599
+ colors.highlight(`\u2501\u2501\u2501 Syncing ${i + 1}/${servicesToSync.length}: ${serviceLabel} \u2501\u2501\u2501`)
4450
4600
  );
4451
4601
  logger.log("");
4452
- return false;
4602
+ try {
4603
+ const result = await syncSingleService(service, options, TOTAL_STEPS);
4604
+ results.push({ service, created: result.created, skipped: result.skipped });
4605
+ logger.log("");
4606
+ } catch (error) {
4607
+ const err = error;
4608
+ logger.log("");
4609
+ logger.warn(`Failed to sync ${serviceLabel}: ${err.message}`);
4610
+ logger.log("");
4611
+ results.push({ service, created: 0, skipped: 0, error: err.message });
4612
+ }
4453
4613
  }
4454
- try {
4455
- await initCommand();
4456
- return true;
4457
- } catch {
4458
- return false;
4614
+ logger.log("");
4615
+ logger.log(colors.highlight("\u2501\u2501\u2501 Sync Summary \u2501\u2501\u2501"));
4616
+ logger.log("");
4617
+ const successful = results.filter((r) => !r.error);
4618
+ const failed = results.filter((r) => r.error);
4619
+ const totalCreated = successful.reduce((sum, r) => sum + r.created, 0);
4620
+ if (successful.length > 0) {
4621
+ logger.log(
4622
+ theme.success(
4623
+ `\u2713 Successfully synced ${successful.length}/${servicesToSync.length} service${servicesToSync.length > 1 ? "s" : ""}:`
4624
+ )
4625
+ );
4626
+ for (const result of successful) {
4627
+ const serviceLabel = result.service.charAt(0).toUpperCase() + result.service.slice(1);
4628
+ logger.info(
4629
+ ` \u2022 ${serviceLabel}: ${result.created} brag${result.created !== 1 ? "s" : ""} created${result.skipped > 0 ? `, ${result.skipped} skipped` : ""}`
4630
+ );
4631
+ }
4632
+ logger.log("");
4459
4633
  }
4460
- }
4461
-
4462
- // src/commands/sync.ts
4463
- init_errors();
4464
-
4465
- // src/ui/spinners.ts
4466
- init_esm_shims();
4467
- import ora2 from "ora";
4468
- function createSpinner(text) {
4469
- return ora2({
4470
- text,
4471
- color: "cyan",
4472
- spinner: "dots"
4473
- });
4474
- }
4475
- function createStepSpinner(currentStep, totalSteps, text) {
4476
- const stepIndicator = theme.step(currentStep, totalSteps);
4477
- return ora2({
4478
- text: `${stepIndicator} ${text}`,
4479
- color: "cyan",
4480
- spinner: "dots"
4481
- });
4482
- }
4483
- function fetchingBragsSpinner() {
4484
- return createSpinner("Fetching your brags...");
4485
- }
4486
- function succeedSpinner(spinner, text) {
4487
- if (text) {
4488
- spinner.succeed(colors.success(text));
4489
- } else {
4490
- spinner.succeed();
4634
+ if (failed.length > 0) {
4635
+ logger.log(
4636
+ colors.warning(`\u2717 Failed to sync ${failed.length} service${failed.length > 1 ? "s" : ""}:`)
4637
+ );
4638
+ for (const result of failed) {
4639
+ const serviceLabel = result.service.charAt(0).toUpperCase() + result.service.slice(1);
4640
+ logger.info(` \u2022 ${serviceLabel}: ${result.error}`);
4641
+ }
4642
+ logger.log("");
4491
4643
  }
4492
- }
4493
- function failSpinner(spinner, text) {
4494
- if (text) {
4495
- spinner.fail(colors.error(text));
4496
- } else {
4497
- spinner.fail();
4644
+ if (totalCreated > 0) {
4645
+ logger.log(boxen6(formatSuccessMessage(totalCreated), boxStyles.success));
4646
+ } else if (successful.length > 0) {
4647
+ logger.log(
4648
+ boxen6(
4649
+ theme.secondary("No new brags created (all items already exist or were skipped)"),
4650
+ boxStyles.info
4651
+ )
4652
+ );
4498
4653
  }
4499
4654
  }
4500
- function succeedStepSpinner(spinner, currentStep, totalSteps, text) {
4501
- const stepIndicator = theme.step(currentStep, totalSteps);
4502
- spinner.succeed(`${stepIndicator} ${colors.success(text)}`);
4503
- }
4504
- function failStepSpinner(spinner, currentStep, totalSteps, text) {
4505
- const stepIndicator = theme.step(currentStep, totalSteps);
4506
- spinner.fail(`${stepIndicator} ${colors.error(text)}`);
4507
- }
4508
-
4509
- // src/commands/sync.ts
4510
4655
  async function syncCommand(options = {}) {
4511
4656
  logger.log("");
4512
4657
  const TOTAL_STEPS = 5;
4658
+ let sourceType;
4513
4659
  try {
4514
4660
  const isAuthenticated = await ensureAuthenticated();
4515
4661
  if (!isAuthenticated) {
@@ -4535,274 +4681,54 @@ async function syncCommand(options = {}) {
4535
4681
  return;
4536
4682
  }
4537
4683
  logger.debug(`Subscription tier "${subscriptionStatus.tier}" - proceeding with sync`);
4538
- const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Detecting repository source");
4539
- detectionSpinner.start();
4540
- let sourceType = options.source;
4541
- if (!sourceType) {
4542
- const envConfig = loadEnvConfig();
4543
- sourceType = envConfig.source;
4544
- }
4545
- if (!sourceType) {
4546
- const projectConfig = await findProjectConfig();
4547
- sourceType = projectConfig?.defaultSource;
4548
- }
4549
- if (!sourceType) {
4550
- try {
4551
- const detectionResult = await sourceDetector.detectSources({
4552
- allowInteractive: true,
4553
- respectPriority: true,
4554
- showAuthStatus: true
4555
- });
4556
- sourceType = detectionResult.recommended;
4557
- if (detectionResult.detected.length > 1) {
4558
- logger.debug(
4559
- `Detected sources: ${detectionResult.detected.map((s) => s.type).join(", ")}`
4560
- );
4561
- }
4562
- if (!sourceType && detectionResult.detected.length === 0) {
4563
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "No supported sources detected");
4564
- logger.log("");
4565
- logger.info("Make sure you are in a git repository with a remote URL");
4566
- logger.info("Or use --source flag for non-git sources:");
4567
- logger.info(` ${theme.command("bragduck sync --source jira")}`);
4568
- logger.info(` ${theme.command("bragduck sync --source confluence")}`);
4569
- return;
4570
- }
4571
- } catch (error) {
4572
- if (error instanceof GitError) {
4573
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Not a git repository");
4574
- logger.log("");
4575
- logger.info("For non-git sources, use --source flag:");
4576
- logger.info(` ${theme.command("bragduck sync --source jira")}`);
4577
- logger.info(` ${theme.command("bragduck sync --source confluence")}`);
4578
- logger.log("");
4579
- logger.info("Or set default source in config:");
4580
- logger.info(` ${theme.command("bragduck config set defaultSource jira")}`);
4581
- return;
4582
- }
4583
- throw error;
4584
- }
4585
- }
4586
- if (!sourceType || !AdapterFactory.isSupported(sourceType)) {
4587
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Could not determine source");
4588
- try {
4589
- const detected = await sourceDetector.detectSources();
4590
- if (detected.detected.length > 0) {
4591
- logger.log("");
4592
- logger.info("Detected sources:");
4593
- for (const source of detected.detected) {
4594
- const authStatus2 = source.isAuthenticated ? "\u2713 authenticated" : "\u2717 not authenticated";
4595
- const repo = source.owner && source.repo ? `${source.owner}/${source.repo}` : source.host || "configured";
4596
- logger.info(` \u2022 ${source.type} (${authStatus2}) - ${repo}`);
4597
- }
4598
- logger.log("");
4599
- }
4600
- } catch {
4601
- }
4602
- logger.info("Specify source explicitly:");
4603
- logger.info(` ${theme.command("bragduck sync --source <type>")}`);
4604
- logger.log("");
4605
- logger.info("Supported sources: github, gitlab, bitbucket, jira, confluence");
4606
- return;
4607
- }
4608
- if (sourceType === "jira" || sourceType === "confluence") {
4609
- const creds = await storageService.getServiceCredentials(sourceType);
4610
- const envInstance = loadEnvConfig()[`${sourceType}Instance`];
4611
- const projectConfig = await findProjectConfig();
4612
- const configInstance = projectConfig?.[`${sourceType}Instance`];
4613
- if (!creds?.instanceUrl && !envInstance && !configInstance) {
4614
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `No ${sourceType} instance configured`);
4684
+ let selectedSource;
4685
+ if (options.source) {
4686
+ sourceType = options.source;
4687
+ if (!AdapterFactory.isSupported(sourceType)) {
4615
4688
  logger.log("");
4616
- logger.info("Configure instance via:");
4617
- logger.info(` ${theme.command("bragduck auth atlassian")} (interactive)`);
4618
- logger.info(
4619
- ` ${theme.command(`bragduck config set ${sourceType}Instance company.atlassian.net`)}`
4620
- );
4621
- logger.info(
4622
- ` ${theme.command(`export BRAGDUCK_${sourceType.toUpperCase()}_INSTANCE=company.atlassian.net`)}`
4623
- );
4689
+ logger.error(`Unsupported source: ${options.source}`);
4690
+ logger.log("");
4691
+ logger.info("Supported sources: github, gitlab, bitbucket, atlassian, jira, confluence");
4624
4692
  return;
4625
4693
  }
4626
- }
4627
- succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source: ${theme.value(sourceType)}`);
4628
- logger.log("");
4629
- const adapter = AdapterFactory.getAdapter(sourceType);
4630
- const repoSpinner = createStepSpinner(2, TOTAL_STEPS, "Validating repository");
4631
- repoSpinner.start();
4632
- await adapter.validate();
4633
- const repoInfo = await adapter.getRepositoryInfo();
4634
- succeedStepSpinner(
4635
- repoSpinner,
4636
- 2,
4637
- TOTAL_STEPS,
4638
- `Repository: ${theme.value(repoInfo.fullName)}`
4639
- );
4640
- logger.log("");
4641
- let days = options.days;
4642
- if (!days) {
4643
- const defaultDays = storageService.getConfig("defaultCommitDays");
4644
- days = await promptDaysToScan(defaultDays);
4645
- logger.log("");
4646
- }
4647
- const fetchSpinner = createStepSpinner(
4648
- 3,
4649
- TOTAL_STEPS,
4650
- `Fetching work items from the last ${days} days`
4651
- );
4652
- fetchSpinner.start();
4653
- const workItems = await adapter.fetchWorkItems({
4654
- days,
4655
- author: options.all ? void 0 : await adapter.getCurrentUser() || void 0
4656
- });
4657
- if (workItems.length === 0) {
4658
- failStepSpinner(fetchSpinner, 3, TOTAL_STEPS, `No work items found in the last ${days} days`);
4659
- logger.log("");
4660
- logger.info("Try increasing the number of days or check your activity");
4661
- return;
4662
- }
4663
- succeedStepSpinner(
4664
- fetchSpinner,
4665
- 3,
4666
- TOTAL_STEPS,
4667
- `Found ${theme.count(workItems.length)} work item${workItems.length > 1 ? "s" : ""}`
4668
- );
4669
- logger.log("");
4670
- logger.log(formatCommitStats(workItems));
4671
- logger.log("");
4672
- let sortedCommits = [...workItems];
4673
- if (workItems.length > 1) {
4674
- const sortOption = await promptSortOption();
4675
- logger.log("");
4676
- if (sortOption === "date") {
4677
- sortedCommits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
4678
- } else if (sortOption === "size") {
4679
- sortedCommits.sort((a, b) => {
4680
- const sizeA = (a.diffStats?.insertions || 0) + (a.diffStats?.deletions || 0);
4681
- const sizeB = (b.diffStats?.insertions || 0) + (b.diffStats?.deletions || 0);
4682
- return sizeB - sizeA;
4683
- });
4684
- } else if (sortOption === "files") {
4685
- sortedCommits.sort((a, b) => {
4686
- const filesA = a.diffStats?.filesChanged || 0;
4687
- const filesB = b.diffStats?.filesChanged || 0;
4688
- return filesB - filesA;
4689
- });
4690
- }
4691
- }
4692
- const selectedShas = await promptSelectCommits(sortedCommits);
4693
- if (selectedShas.length === 0) {
4694
- logger.log("");
4695
- logger.info(theme.secondary("No work items selected. Sync cancelled."));
4696
- logger.log("");
4697
- return;
4698
- }
4699
- const selectedCommits = sortedCommits.filter((c) => selectedShas.includes(c.sha));
4700
- logger.log(formatSelectionSummary(selectedCommits.length, selectedCommits));
4701
- logger.log("");
4702
- const existingBrags = await apiService.listBrags({ limit: 100 });
4703
- logger.debug(`Fetched ${existingBrags.brags.length} existing brags`);
4704
- const existingUrls = new Set(existingBrags.brags.flatMap((b) => b.attachments || []));
4705
- logger.debug(`Existing URLs in attachments: ${existingUrls.size}`);
4706
- const duplicates = selectedCommits.filter((c) => c.url && existingUrls.has(c.url));
4707
- const newCommits = selectedCommits.filter((c) => !c.url || !existingUrls.has(c.url));
4708
- logger.debug(`Duplicates: ${duplicates.length}, New: ${newCommits.length}`);
4709
- if (duplicates.length > 0) {
4710
- logger.log("");
4711
- logger.info(
4712
- colors.warning(
4713
- `${duplicates.length} work item${duplicates.length > 1 ? "s" : ""} already added to Bragduck - skipping`
4714
- )
4715
- );
4716
- logger.log("");
4717
- }
4718
- if (newCommits.length === 0) {
4719
- logger.log("");
4720
- logger.info(
4721
- theme.secondary("All selected work items already exist in Bragduck. Nothing to refine.")
4722
- );
4723
- logger.log("");
4724
- return;
4725
- }
4726
- const refineSpinner = createStepSpinner(
4727
- 4,
4728
- TOTAL_STEPS,
4729
- `Refining ${theme.count(newCommits.length)} work item${newCommits.length > 1 ? "s" : ""} with AI`
4730
- );
4731
- refineSpinner.start();
4732
- const refineRequest = {
4733
- brags: newCommits.map((c) => ({
4734
- text: c.message,
4735
- date: c.date,
4736
- title: c.message.split("\n")[0]
4737
- }))
4738
- };
4739
- const refineResponse = await apiService.refineBrags(refineRequest);
4740
- let refinedBrags = refineResponse.refined_brags;
4741
- succeedStepSpinner(refineSpinner, 4, TOTAL_STEPS, "Work items refined successfully");
4742
- logger.log("");
4743
- logger.info("Preview of refined brags:");
4744
- logger.log("");
4745
- logger.log(formatRefinedCommitsTable(refinedBrags, newCommits));
4746
- logger.log("");
4747
- const acceptedBrags = await promptReviewBrags(refinedBrags, newCommits);
4748
- if (acceptedBrags.length === 0) {
4694
+ selectedSource = sourceType;
4695
+ } else {
4749
4696
  logger.log("");
4750
- logger.info(theme.secondary("No brags selected for creation. Sync cancelled."));
4697
+ selectedSource = await promptSelectService();
4751
4698
  logger.log("");
4752
- return;
4753
4699
  }
4754
- logger.log("");
4755
- let selectedOrgId = null;
4756
- const userInfo = authService.getUserInfo();
4757
- if (userInfo?.id) {
4758
- try {
4759
- const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
4760
- if (orgsResponse.items.length > 0) {
4761
- selectedOrgId = await promptSelectOrganisation(orgsResponse.items);
4700
+ if (selectedSource === "all") {
4701
+ await syncAllAuthenticatedServices(options);
4702
+ } else {
4703
+ const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Preparing sync");
4704
+ detectionSpinner.start();
4705
+ sourceType = selectedSource;
4706
+ if (sourceType === "jira" || sourceType === "confluence") {
4707
+ const creds = await storageService.getServiceCredentials(sourceType);
4708
+ const envInstance = loadEnvConfig()[`${sourceType}Instance`];
4709
+ const projectConfig = await findProjectConfig();
4710
+ const configInstance = projectConfig?.[`${sourceType}Instance`];
4711
+ if (!creds?.instanceUrl && !envInstance && !configInstance) {
4712
+ failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `No ${sourceType} instance configured`);
4762
4713
  logger.log("");
4714
+ logger.info("Configure instance via:");
4715
+ logger.info(` ${theme.command("bragduck auth atlassian")} (interactive)`);
4716
+ logger.info(
4717
+ ` ${theme.command(`bragduck config set ${sourceType}Instance company.atlassian.net`)}`
4718
+ );
4719
+ logger.info(
4720
+ ` ${theme.command(`export BRAGDUCK_${sourceType.toUpperCase()}_INSTANCE=company.atlassian.net`)}`
4721
+ );
4722
+ return;
4763
4723
  }
4764
- } catch {
4765
- logger.debug("Failed to fetch organisations, skipping org selection");
4724
+ }
4725
+ succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source: ${theme.value(sourceType)}`);
4726
+ logger.log("");
4727
+ const result = await syncSingleService(sourceType, options, TOTAL_STEPS);
4728
+ if (result.created > 0) {
4729
+ logger.log(boxen6(formatSuccessMessage(result.created), boxStyles.success));
4766
4730
  }
4767
4731
  }
4768
- const createSpinner2 = createStepSpinner(
4769
- 5,
4770
- TOTAL_STEPS,
4771
- `Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
4772
- );
4773
- createSpinner2.start();
4774
- const createRequest = {
4775
- brags: acceptedBrags.map((refined, index) => {
4776
- const originalCommit = newCommits[index];
4777
- return {
4778
- commit_sha: originalCommit?.sha || `brag-${index}`,
4779
- title: refined.refined_title,
4780
- description: refined.refined_description,
4781
- tags: refined.suggested_tags,
4782
- repository: repoInfo.url,
4783
- date: originalCommit?.date || "",
4784
- commit_url: originalCommit?.url || "",
4785
- impact_score: refined.suggested_impactLevel,
4786
- impact_description: refined.impact_description,
4787
- attachments: originalCommit?.url ? [originalCommit.url] : [],
4788
- orgId: selectedOrgId || void 0,
4789
- // External fields for non-git sources (Jira, Confluence, etc.)
4790
- externalId: originalCommit?.externalId,
4791
- externalType: originalCommit?.externalType,
4792
- externalSource: originalCommit?.externalSource,
4793
- externalUrl: originalCommit?.externalUrl
4794
- };
4795
- })
4796
- };
4797
- const createResponse = await apiService.createBrags(createRequest);
4798
- succeedStepSpinner(
4799
- createSpinner2,
4800
- 5,
4801
- TOTAL_STEPS,
4802
- `Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
4803
- );
4804
- logger.log("");
4805
- logger.log(boxen6(formatSuccessMessage(createResponse.created), boxStyles.success));
4806
4732
  } catch (error) {
4807
4733
  if (error instanceof CancelPromptError) {
4808
4734
  logger.log("");
@@ -4812,11 +4738,13 @@ async function syncCommand(options = {}) {
4812
4738
  }
4813
4739
  const err = error;
4814
4740
  logger.log("");
4815
- logger.log(boxen6(formatErrorMessage(err.message, getErrorHint2(err)), boxStyles.error));
4741
+ logger.log(
4742
+ boxen6(formatErrorMessage(err.message, getErrorHint2(err, sourceType)), boxStyles.error)
4743
+ );
4816
4744
  process.exit(1);
4817
4745
  }
4818
4746
  }
4819
- function getErrorHint2(error) {
4747
+ function getErrorHint2(error, sourceType) {
4820
4748
  if (error.name === "GitHubError") {
4821
4749
  return 'Make sure you are in a GitHub repository and have authenticated with "gh auth login"';
4822
4750
  }
@@ -4824,6 +4752,9 @@ function getErrorHint2(error) {
4824
4752
  return "Make sure you are in a git repository";
4825
4753
  }
4826
4754
  if (error.name === "TokenExpiredError" || error.name === "AuthenticationError") {
4755
+ if (sourceType === "jira" || sourceType === "confluence") {
4756
+ return 'This is your Bragduck platform session. Run "bragduck auth login" (not "bragduck auth atlassian")';
4757
+ }
4827
4758
  return 'Run "bragduck auth login" to login again';
4828
4759
  }
4829
4760
  if (error.name === "NetworkError") {