@bragduck/cli 2.8.3 → 2.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,7 +1217,7 @@ 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}`);
@@ -1243,12 +1243,12 @@ __export(version_exports, {
1243
1243
  });
1244
1244
  import { readFileSync as readFileSync3 } from "fs";
1245
1245
  import { fileURLToPath as fileURLToPath4 } from "url";
1246
- import { dirname as dirname3, join as join4 } from "path";
1246
+ import { dirname as dirname3, join as join5 } from "path";
1247
1247
  import chalk4 from "chalk";
1248
1248
  import boxen2 from "boxen";
1249
1249
  function getCurrentVersion() {
1250
1250
  try {
1251
- const packageJsonPath2 = join4(__dirname4, "../../package.json");
1251
+ const packageJsonPath2 = join5(__dirname4, "../../package.json");
1252
1252
  const packageJson2 = JSON.parse(readFileSync3(packageJsonPath2, "utf-8"));
1253
1253
  return packageJson2.version;
1254
1254
  } catch {
@@ -1350,10 +1350,10 @@ import { ofetch as ofetch2 } from "ofetch";
1350
1350
  import { readFileSync as readFileSync4 } from "fs";
1351
1351
  import { fileURLToPath as fileURLToPath5 } from "url";
1352
1352
  import { URLSearchParams as URLSearchParams2 } from "url";
1353
- import { dirname as dirname4, join as join5 } from "path";
1353
+ import { dirname as dirname4, join as join6 } from "path";
1354
1354
  function getCliVersion() {
1355
1355
  try {
1356
- const packageJsonPath2 = join5(__dirname5, "../../package.json");
1356
+ const packageJsonPath2 = join6(__dirname5, "../../package.json");
1357
1357
  const packageJson2 = JSON.parse(readFileSync4(packageJsonPath2, "utf-8"));
1358
1358
  return packageJson2.version;
1359
1359
  } catch {
@@ -1669,955 +1669,980 @@ import { dirname as dirname5, join as join7 } from "path";
1669
1669
  init_esm_shims();
1670
1670
  init_auth_service();
1671
1671
  init_storage_service();
1672
- init_logger();
1673
1672
  import boxen from "boxen";
1674
1673
  import chalk3 from "chalk";
1675
1674
  import { input } from "@inquirer/prompts";
1676
1675
 
1677
- // src/ui/theme.ts
1676
+ // src/services/github.service.ts
1678
1677
  init_esm_shims();
1679
- import chalk2 from "chalk";
1680
- var colors = {
1681
- // Primary colors for main actions and interactive elements
1682
- primary: chalk2.cyan,
1683
- // Success states
1684
- success: chalk2.green,
1685
- successBold: chalk2.green.bold,
1686
- // Warning states
1687
- warning: chalk2.yellow,
1688
- warningBold: chalk2.yellow.bold,
1689
- // Error states
1690
- error: chalk2.red,
1691
- errorBold: chalk2.red.bold,
1692
- // Info and metadata
1693
- info: chalk2.gray,
1694
- infoDim: chalk2.dim,
1695
- // Highlighted/important data
1696
- highlight: chalk2.yellow.bold,
1697
- highlightCyan: chalk2.cyan.bold,
1698
- // Text emphasis
1699
- bold: chalk2.bold,
1700
- dim: chalk2.dim,
1701
- // Special purpose
1702
- white: chalk2.white,
1703
- gray: chalk2.gray,
1704
- // Links and URLs
1705
- link: chalk2.blue.underline
1706
- };
1707
- var theme = {
1708
- /**
1709
- * Format command names and CLI actions
1710
- */
1711
- command: (text) => colors.primary(text),
1712
- /**
1713
- * Format success messages
1714
- */
1715
- success: (text) => colors.success(`\u2713 ${text}`),
1716
- successBold: (text) => colors.successBold(`\u2713 ${text}`),
1717
- /**
1718
- * Format error messages
1719
- */
1720
- error: (text) => colors.error(`\u2717 ${text}`),
1721
- 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
+ }
1722
1715
  /**
1723
- * Format warning messages
1716
+ * Validate that the current directory is a git repository
1724
1717
  */
1725
- 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
+ }
1726
1737
  /**
1727
- * Format info messages
1738
+ * Get repository information
1728
1739
  */
1729
- 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
+ }
1730
1761
  /**
1731
- * Format labels (e.g., "User:", "Email:")
1762
+ * Fetch recent commits
1732
1763
  */
1733
- 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
+ }
1734
1801
  /**
1735
- * Format values
1802
+ * Get diff statistics for a specific commit
1736
1803
  */
1737
- 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
+ }
1738
1841
  /**
1739
- * Format counts and numbers
1842
+ * Get commits with their diff statistics
1740
1843
  */
1741
- 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
+ }
1742
1863
  /**
1743
- * Format file paths
1864
+ * Get the current git user email
1744
1865
  */
1745
- 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
+ }
1746
1875
  /**
1747
- * Format step indicators (e.g., "Step 1/5:")
1876
+ * Get the current git user name
1748
1877
  */
1749
- 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
+ }
1750
1887
  /**
1751
- * Format progress text
1888
+ * Filter commits by current user
1752
1889
  */
1753
- 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";
1754
1910
  /**
1755
- * Format emphasized text
1911
+ * Check if GitHub CLI is installed and available
1756
1912
  */
1757
- 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
+ }
1758
1921
  /**
1759
- * Format de-emphasized/secondary text
1922
+ * Validate that GitHub CLI is installed
1760
1923
  */
1761
- 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
+ }
1762
1932
  /**
1763
- * Format interactive elements (prompts, selections)
1933
+ * Check if user is authenticated with GitHub CLI
1764
1934
  */
1765
- 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
+ }
1766
1943
  /**
1767
- * Format keys in key-value pairs
1944
+ * Ensure user is authenticated with GitHub CLI
1768
1945
  */
1769
- key: (text) => colors.white(text)
1770
- };
1771
- var boxStyles = {
1772
- success: {
1773
- borderColor: "green",
1774
- borderStyle: "round",
1775
- padding: 1,
1776
- margin: 1
1777
- },
1778
- error: {
1779
- borderColor: "red",
1780
- borderStyle: "round",
1781
- padding: 1,
1782
- margin: 1
1783
- },
1784
- warning: {
1785
- borderColor: "yellow",
1786
- borderStyle: "round",
1787
- padding: 1,
1788
- margin: 1
1789
- },
1790
- info: {
1791
- borderColor: "cyan",
1792
- borderStyle: "round",
1793
- padding: 1,
1794
- margin: 1
1795
- },
1796
- plain: {
1797
- borderColor: "gray",
1798
- borderStyle: "round",
1799
- padding: 1,
1800
- margin: 1
1801
- }
1802
- };
1803
- var tableStyles = {
1804
- default: {
1805
- style: {
1806
- head: [],
1807
- border: ["gray"]
1808
- }
1809
- },
1810
- compact: {
1811
- style: {
1812
- head: [],
1813
- border: ["dim"],
1814
- 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
+ });
1815
1952
  }
1816
1953
  }
1817
- };
1818
- var sizeIndicators = {
1819
- small: colors.success("\u25CF"),
1820
- medium: colors.warning("\u25CF"),
1821
- large: colors.error("\u25CF")
1822
- };
1823
- function getSizeIndicator(insertions, deletions) {
1824
- const total = insertions + deletions;
1825
- if (total < 50) {
1826
- return `${sizeIndicators.small} Small`;
1827
- } else if (total < 200) {
1828
- return `${sizeIndicators.medium} Medium`;
1829
- } else {
1830
- 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 };
1831
1965
  }
1832
- }
1833
- function formatDiffStats(insertions, deletions) {
1834
- return `${colors.success(`+${insertions}`)} ${colors.error(`-${deletions}`)}`;
1835
- }
1836
-
1837
- // src/commands/auth.ts
1838
- async function authCommand(subcommand) {
1839
- if (!subcommand || subcommand === "login") {
1840
- await authLogin();
1841
- } else if (subcommand === "status") {
1842
- await authStatus();
1843
- } else if (subcommand === "bitbucket") {
1844
- await authBitbucket();
1845
- } else if (subcommand === "gitlab") {
1846
- await authGitLab();
1847
- } else if (subcommand === "atlassian") {
1848
- await authAtlassian();
1849
- } else {
1850
- logger.error(`Unknown auth subcommand: ${subcommand}`);
1851
- logger.info("Available subcommands: login, status, bitbucket, gitlab, atlassian");
1852
- 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
+ }
1853
1999
  }
1854
- }
1855
- async function authLogin() {
1856
- logger.log("");
1857
- logger.info("Authenticating with Bragduck...");
1858
- logger.log("");
1859
- const isAuthenticated = await storageService.isServiceAuthenticated("bragduck");
1860
- if (isAuthenticated) {
1861
- const userInfo = authService.getUserInfo();
1862
- if (userInfo) {
1863
- logger.log(
1864
- boxen(
1865
- `${chalk3.yellow("Already authenticated!")}
1866
-
1867
- ${chalk3.gray("User:")} ${userInfo.name}
1868
- ${chalk3.gray("Email:")} ${userInfo.email}
1869
-
1870
- ${chalk3.dim("Run")} ${chalk3.cyan("bragduck logout")} ${chalk3.dim("to sign out")}`,
1871
- {
1872
- padding: 1,
1873
- margin: 1,
1874
- borderStyle: "round",
1875
- borderColor: "yellow"
1876
- }
1877
- )
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"
1878
2008
  );
1879
- logger.log("");
1880
- return;
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
+ });
1881
2024
  }
1882
2025
  }
1883
- try {
1884
- const userInfo = await authService.login();
1885
- logger.log("");
1886
- logger.log(
1887
- boxen(
1888
- `${chalk3.green.bold("\u2713 Successfully authenticated!")}
1889
-
1890
- ${chalk3.gray("Welcome,")} ${chalk3.cyan(userInfo.name)}
1891
- ${chalk3.gray("Email:")} ${userInfo.email}
1892
-
1893
- ${chalk3.dim("Use")} ${chalk3.cyan("bragduck sync")} ${chalk3.dim("to create brags")}`,
1894
- {
1895
- padding: 1,
1896
- margin: 1,
1897
- borderStyle: "round",
1898
- borderColor: "green"
1899
- }
1900
- )
1901
- );
1902
- logger.log("");
1903
- } catch (error) {
1904
- const err = error;
1905
- logger.log("");
1906
- logger.log(
1907
- boxen(`${chalk3.red.bold("\u2717 Authentication Failed")}
1908
-
1909
- ${err.message}`, {
1910
- padding: 1,
1911
- margin: 1,
1912
- borderStyle: "round",
1913
- borderColor: "red"
1914
- })
1915
- );
1916
- process.exit(1);
1917
- }
1918
- }
1919
- async function authStatus() {
1920
- logger.log("");
1921
- logger.info("Authentication Status:");
1922
- logger.log("");
1923
- const services = await storageService.getAuthenticatedServices();
1924
- if (services.length === 0) {
1925
- logger.info(theme.secondary("Not authenticated with any services"));
1926
- logger.log("");
1927
- logger.info(`Run ${theme.command("bragduck auth login")} to authenticate`);
1928
- logger.log("");
1929
- return;
1930
- }
1931
- const bragduckAuth = await storageService.isServiceAuthenticated("bragduck");
1932
- if (bragduckAuth) {
1933
- const userInfo = authService.getUserInfo();
1934
- logger.info(`${theme.success("\u2713")} Bragduck: ${userInfo?.name || "Authenticated"}`);
1935
- } else {
1936
- logger.info(`${theme.error("\u2717")} Bragduck: Not authenticated`);
1937
- }
1938
- for (const service of services) {
1939
- if (service !== "bragduck") {
1940
- logger.info(`${theme.success("\u2713")} ${service}: Authenticated`);
2026
+ /**
2027
+ * Get the current authenticated GitHub user
2028
+ */
2029
+ async getCurrentGitHubUser() {
2030
+ try {
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;
1941
2036
  }
1942
2037
  }
1943
- logger.log("");
1944
- }
1945
- async function authBitbucket() {
1946
- logger.log("");
1947
- logger.log(
1948
- boxen(
1949
- 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)"),
1950
- boxStyles.info
1951
- )
1952
- );
1953
- logger.log("");
1954
- try {
1955
- const email = await input({
1956
- message: "Atlassian account email:",
1957
- validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
1958
- });
1959
- const apiToken = await input({
1960
- message: "API Token:",
1961
- validate: (value) => value.length > 0 ? true : "API token cannot be empty"
1962
- });
1963
- const tokenExpiry = await input({
1964
- message: "Token expiry date (YYYY-MM-DD, optional):",
1965
- default: "",
1966
- validate: (value) => {
1967
- if (!value) return true;
1968
- const date = new Date(value);
1969
- return !isNaN(date.getTime()) ? true : "Please enter a valid date (YYYY-MM-DD)";
2038
+ /**
2039
+ * Fetch merged PRs from GitHub
2040
+ */
2041
+ async getMergedPRs(options = {}) {
2042
+ const { days = 30, limit, author } = options;
2043
+ try {
2044
+ await this.ensureGitHubCLI();
2045
+ await this.ensureAuthentication();
2046
+ logger.debug(`Fetching merged PRs from the last ${days} days`);
2047
+ const since = /* @__PURE__ */ new Date();
2048
+ since.setDate(since.getDate() - days);
2049
+ const sinceDate = since.toISOString().split("T")[0];
2050
+ let searchQuery = `merged:>=${sinceDate}`;
2051
+ if (author) {
2052
+ searchQuery += ` author:${author}`;
1970
2053
  }
1971
- });
1972
- const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
1973
- const response = await fetch("https://api.bitbucket.org/2.0/user", {
1974
- headers: { Authorization: `Basic ${auth}` }
1975
- });
1976
- if (!response.ok) {
1977
- logger.log("");
1978
- logger.log(
1979
- boxen(
1980
- theme.error("\u2717 Authentication Failed") + "\n\nInvalid email or API token",
1981
- boxStyles.error
1982
- )
1983
- );
1984
- logger.log("");
1985
- process.exit(1);
1986
- }
1987
- const user = await response.json();
1988
- const credentials = {
1989
- accessToken: apiToken,
1990
- username: email
1991
- // Store email in username field
1992
- };
1993
- if (tokenExpiry) {
1994
- const expiryDate = new Date(tokenExpiry);
1995
- credentials.expiresAt = expiryDate.getTime();
1996
- }
1997
- await storageService.setServiceCredentials("bitbucket", credentials);
1998
- logger.log("");
1999
- logger.log(
2000
- boxen(
2001
- theme.success("\u2713 Successfully authenticated with Bitbucket") + `
2002
-
2003
- Email: ${email}
2004
- User: ${user.display_name}
2005
- ` + (tokenExpiry ? `Expires: ${tokenExpiry}` : ""),
2006
- boxStyles.success
2007
- )
2008
- );
2009
- logger.log("");
2010
- } catch (error) {
2011
- const err = error;
2012
- logger.log("");
2013
- logger.log(
2014
- boxen(
2015
- theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2016
- boxStyles.error
2017
- )
2018
- );
2019
- logger.log("");
2020
- process.exit(1);
2021
- }
2022
- }
2023
- async function authGitLab() {
2024
- logger.log("");
2025
- logger.log(
2026
- boxen(
2027
- 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"),
2028
- boxStyles.info
2029
- )
2030
- );
2031
- logger.log("");
2032
- try {
2033
- const instanceUrl = await input({
2034
- message: "GitLab instance URL (press Enter for gitlab.com):",
2035
- default: "https://gitlab.com"
2036
- });
2037
- const accessToken = await input({
2038
- message: "Personal Access Token:",
2039
- validate: (value) => value.length > 0 ? true : "Token cannot be empty"
2040
- });
2041
- const testUrl = `${instanceUrl.replace(/\/$/, "")}/api/v4/user`;
2042
- const response = await fetch(testUrl, {
2043
- headers: { "PRIVATE-TOKEN": accessToken }
2044
- });
2045
- if (!response.ok) {
2046
- logger.log("");
2047
- logger.log(
2048
- boxen(
2049
- theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL or access token",
2050
- boxStyles.error
2051
- )
2052
- );
2053
- logger.log("");
2054
- process.exit(1);
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;
2061
+ } catch (error) {
2062
+ if (error instanceof GitHubError || error instanceof GitError) {
2063
+ throw error;
2064
+ }
2065
+ throw new GitHubError("Failed to fetch PRs from GitHub", {
2066
+ originalError: error.message,
2067
+ days
2068
+ });
2055
2069
  }
2056
- const user = await response.json();
2057
- const credentials = {
2058
- accessToken,
2059
- instanceUrl: instanceUrl === "https://gitlab.com" ? void 0 : instanceUrl
2060
- };
2061
- await storageService.setServiceCredentials("gitlab", credentials);
2062
- logger.log("");
2063
- logger.log(
2064
- boxen(
2065
- theme.success("\u2713 Successfully authenticated with GitLab") + `
2066
-
2067
- Instance: ${instanceUrl}
2068
- User: ${user.name} (@${user.username})`,
2069
- boxStyles.success
2070
- )
2071
- );
2072
- logger.log("");
2073
- } catch (error) {
2074
- const err = error;
2075
- logger.log("");
2076
- logger.log(
2077
- boxen(
2078
- theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2079
- boxStyles.error
2080
- )
2081
- );
2082
- logger.log("");
2083
- process.exit(1);
2084
2070
  }
2085
- }
2086
- async function authAtlassian() {
2087
- logger.log("");
2088
- logger.log(
2089
- boxen(
2090
- 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",
2091
- boxStyles.info
2092
- )
2093
- );
2094
- logger.log("");
2095
- try {
2096
- const instanceUrl = await input({
2097
- message: "Atlassian instance URL (e.g., company.atlassian.net):",
2098
- validate: (value) => value.length > 0 ? true : "Instance URL cannot be empty"
2099
- });
2100
- const email = await input({
2101
- message: "Atlassian account email:",
2102
- validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
2103
- });
2104
- const apiToken = await input({
2105
- message: "API Token:",
2106
- validate: (value) => value.length > 0 ? true : "API token cannot be empty"
2107
- });
2108
- const testInstanceUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
2109
- const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
2110
- const response = await fetch(`${testInstanceUrl}/rest/api/2/myself`, {
2111
- headers: { Authorization: `Basic ${auth}` }
2112
- });
2113
- if (!response.ok) {
2114
- logger.log("");
2115
- logger.log(
2116
- boxen(
2117
- 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)",
2118
- boxStyles.error
2119
- )
2120
- );
2121
- logger.log("");
2122
- process.exit(1);
2071
+ /**
2072
+ * Get merged PRs by current user (PRs authored by the current user)
2073
+ */
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);
2123
2079
  }
2124
- const user = await response.json();
2125
- const credentials = {
2126
- accessToken: apiToken,
2127
- username: email,
2128
- instanceUrl: instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`
2129
- };
2130
- await storageService.setServiceCredentials("jira", credentials);
2131
- await storageService.setServiceCredentials("confluence", credentials);
2132
- await storageService.setServiceCredentials("bitbucket", {
2133
- ...credentials,
2134
- instanceUrl: "https://api.bitbucket.org"
2135
- // Bitbucket uses different API base
2080
+ logger.debug(`Filtering PRs by author: ${currentUser}`);
2081
+ return this.getMergedPRs({
2082
+ ...options,
2083
+ author: currentUser
2136
2084
  });
2137
- logger.log("");
2138
- logger.log(
2139
- boxen(
2140
- theme.success("\u2713 Successfully authenticated with Atlassian") + `
2141
-
2142
- Instance: ${instanceUrl}
2143
- User: ${user.displayName}
2144
- Email: ${user.emailAddress}
2145
-
2146
- Services configured:
2147
- \u2022 Jira
2148
- \u2022 Confluence
2149
- \u2022 Bitbucket`,
2150
- boxStyles.success
2151
- )
2152
- );
2153
- logger.log("");
2154
- } catch (error) {
2155
- const err = error;
2156
- logger.log("");
2157
- logger.log(
2158
- boxen(
2159
- theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2160
- boxStyles.error
2161
- )
2162
- );
2163
- logger.log("");
2164
- process.exit(1);
2165
2085
  }
2166
- }
2167
-
2168
- // src/commands/sync.ts
2169
- init_esm_shims();
2086
+ /**
2087
+ * Transform a GitHub PR into a GitCommit structure
2088
+ * This allows PR data to work with existing commit-based UI and API
2089
+ */
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}
2170
2095
 
2171
- // node_modules/@inquirer/core/dist/esm/lib/errors.mjs
2172
- init_esm_shims();
2173
- var CancelPromptError = class extends Error {
2174
- name = "CancelPromptError";
2175
- message = "Prompt was canceled";
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
+ };
2111
+ }
2176
2112
  };
2113
+ var githubService = new GitHubService();
2177
2114
 
2178
- // src/commands/sync.ts
2179
- init_api_service();
2180
- init_storage_service();
2181
- init_auth_service();
2182
- init_env_loader();
2183
- init_config_loader();
2184
- import { select as select2 } from "@inquirer/prompts";
2185
- import boxen6 from "boxen";
2186
-
2187
- // src/sync/adapter-factory.ts
2188
- init_esm_shims();
2189
-
2190
- // src/sync/github-adapter.ts
2191
- init_esm_shims();
2192
-
2193
- // src/services/github.service.ts
2194
- init_esm_shims();
2195
- init_errors();
2115
+ // src/commands/auth.ts
2196
2116
  init_logger();
2197
- import { exec as exec2 } from "child_process";
2198
- import { promisify as promisify2 } from "util";
2199
-
2200
- // src/services/git.service.ts
2201
- init_esm_shims();
2202
- import simpleGit from "simple-git";
2203
2117
 
2204
- // src/utils/validators.ts
2118
+ // src/ui/theme.ts
2205
2119
  init_esm_shims();
2206
- init_errors();
2207
- import { existsSync as existsSync2 } from "fs";
2208
- import { join as join6 } from "path";
2209
- function validateGitRepository(path3) {
2210
- const gitDir = join6(path3, ".git");
2211
- if (!existsSync2(gitDir)) {
2212
- throw new GitError(
2213
- "Not a git repository. Please run this command from within a git repository.",
2214
- {
2215
- path: path3,
2216
- hint: 'Run "git init" to initialize a git repository, or navigate to an existing one'
2217
- }
2218
- );
2219
- }
2220
- }
2221
-
2222
- // src/services/git.service.ts
2223
- init_errors();
2224
- init_logger();
2225
- var GitService = class {
2226
- git;
2227
- repoPath;
2228
- constructor(repoPath = process.cwd()) {
2229
- this.repoPath = repoPath;
2230
- this.git = simpleGit(repoPath);
2231
- }
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),
2232
2179
  /**
2233
- * Validate that the current directory is a git repository
2180
+ * Format highlighted text
2234
2181
  */
2235
- async validateRepository() {
2236
- try {
2237
- validateGitRepository(this.repoPath);
2238
- const isRepo = await this.git.checkIsRepo();
2239
- if (!isRepo) {
2240
- throw new GitError("Not a valid git repository", {
2241
- path: this.repoPath
2242
- });
2243
- }
2244
- } catch (error) {
2245
- if (error instanceof GitError) {
2246
- throw error;
2247
- }
2248
- throw new GitError("Failed to validate git repository", {
2249
- originalError: error.message,
2250
- path: this.repoPath
2251
- });
2252
- }
2253
- }
2182
+ highlight: (text) => colors.highlight(text),
2254
2183
  /**
2255
- * Get repository information
2184
+ * Format counts and numbers
2256
2185
  */
2257
- async getRepositoryInfo() {
2258
- try {
2259
- await this.validateRepository();
2260
- const status = await this.git.status();
2261
- const remotes = await this.git.getRemotes(true);
2262
- const primaryRemote = remotes.find((r) => r.name === "origin");
2263
- return {
2264
- path: this.repoPath,
2265
- currentBranch: status.current || "unknown",
2266
- remoteUrl: primaryRemote?.refs?.fetch || primaryRemote?.refs?.push,
2267
- isClean: status.isClean()
2268
- };
2269
- } catch (error) {
2270
- if (error instanceof GitError) {
2271
- throw error;
2272
- }
2273
- throw new GitError("Failed to get repository information", {
2274
- originalError: error.message
2275
- });
2276
- }
2277
- }
2186
+ count: (num) => colors.highlightCyan(num.toString()),
2278
2187
  /**
2279
- * Fetch recent commits
2188
+ * Format file paths
2280
2189
  */
2281
- async getRecentCommits(options = {}) {
2282
- const { days = 30, limit, author } = options;
2283
- try {
2284
- await this.validateRepository();
2285
- logger.debug(`Fetching commits from last ${days} days`);
2286
- const since = /* @__PURE__ */ new Date();
2287
- since.setDate(since.getDate() - days);
2288
- const logOptions = {
2289
- "--since": since.toISOString(),
2290
- "--no-merges": null
2291
- };
2292
- if (limit) {
2293
- logOptions.maxCount = limit;
2294
- }
2295
- if (author) {
2296
- logOptions["--author"] = author;
2297
- }
2298
- const log = await this.git.log(logOptions);
2299
- logger.debug(`Found ${log.all.length} commits`);
2300
- const commits = log.all.map((commit) => ({
2301
- sha: commit.hash,
2302
- message: commit.message,
2303
- author: commit.author_name,
2304
- authorEmail: commit.author_email,
2305
- date: commit.date
2306
- }));
2307
- return commits;
2308
- } catch (error) {
2309
- if (error instanceof GitError) {
2310
- throw error;
2311
- }
2312
- throw new GitError("Failed to fetch commits", {
2313
- originalError: error.message,
2314
- days
2315
- });
2316
- }
2317
- }
2190
+ path: (text) => colors.info(text),
2318
2191
  /**
2319
- * Get diff statistics for a specific commit
2192
+ * Format step indicators (e.g., "Step 1/5:")
2320
2193
  */
2321
- async getCommitStats(sha) {
2322
- try {
2323
- logger.debug(`Getting stats for commit ${sha}`);
2324
- const diffSummary = await this.git.diffSummary([`${sha}^`, sha]);
2325
- const stats = {
2326
- filesChanged: diffSummary.files.length,
2327
- insertions: diffSummary.insertions,
2328
- deletions: diffSummary.deletions
2329
- };
2330
- logger.debug(
2331
- `Commit ${sha}: ${stats.filesChanged} files, +${stats.insertions} -${stats.deletions}`
2332
- );
2333
- return stats;
2334
- } catch (error) {
2335
- if (error.message && error.message.includes("unknown revision")) {
2336
- logger.debug(`Commit ${sha} has no parent (first commit)`);
2337
- try {
2338
- const diffSummary = await this.git.diffSummary([sha]);
2339
- return {
2340
- filesChanged: diffSummary.files.length,
2341
- insertions: diffSummary.insertions,
2342
- deletions: diffSummary.deletions
2343
- };
2344
- } catch {
2345
- return {
2346
- filesChanged: 0,
2347
- insertions: 0,
2348
- deletions: 0
2349
- };
2350
- }
2351
- }
2352
- throw new GitError("Failed to get commit statistics", {
2353
- originalError: error.message,
2354
- sha
2355
- });
2356
- }
2357
- }
2194
+ step: (current, total) => colors.primary(`[${current}/${total}]`),
2358
2195
  /**
2359
- * Get commits with their diff statistics
2196
+ * Format progress text
2360
2197
  */
2361
- async getCommitsWithStats(options = {}) {
2362
- const commits = await this.getRecentCommits(options);
2363
- logger.debug(`Fetching diff stats for ${commits.length} commits`);
2364
- const commitsWithStats = await Promise.all(
2365
- commits.map(async (commit) => {
2366
- try {
2367
- const stats = await this.getCommitStats(commit.sha);
2368
- return {
2369
- ...commit,
2370
- diffStats: stats
2371
- };
2372
- } catch {
2373
- logger.debug(`Failed to get stats for commit ${commit.sha}, continuing without stats`);
2374
- return commit;
2375
- }
2376
- })
2377
- );
2378
- return commitsWithStats;
2379
- }
2198
+ progress: (text) => colors.infoDim(text),
2380
2199
  /**
2381
- * Get the current git user email
2200
+ * Format emphasized text
2382
2201
  */
2383
- async getCurrentUserEmail() {
2384
- try {
2385
- const email = await this.git.raw(["config", "user.email"]);
2386
- return email.trim() || null;
2387
- } catch {
2388
- logger.debug("Failed to get git user email");
2389
- return null;
2390
- }
2391
- }
2202
+ emphasis: (text) => colors.bold(text),
2392
2203
  /**
2393
- * Get the current git user name
2204
+ * Format de-emphasized/secondary text
2394
2205
  */
2395
- async getCurrentUserName() {
2396
- try {
2397
- const name = await this.git.raw(["config", "user.name"]);
2398
- return name.trim() || null;
2399
- } catch {
2400
- logger.debug("Failed to get git user name");
2401
- return null;
2206
+ secondary: (text) => colors.dim(text),
2207
+ /**
2208
+ * Format interactive elements (prompts, selections)
2209
+ */
2210
+ interactive: (text) => colors.primary(text),
2211
+ /**
2212
+ * Format keys in key-value pairs
2213
+ */
2214
+ key: (text) => colors.white(text)
2215
+ };
2216
+ var boxStyles = {
2217
+ success: {
2218
+ borderColor: "green",
2219
+ borderStyle: "round",
2220
+ padding: 1,
2221
+ margin: 1
2222
+ },
2223
+ error: {
2224
+ borderColor: "red",
2225
+ borderStyle: "round",
2226
+ padding: 1,
2227
+ margin: 1
2228
+ },
2229
+ warning: {
2230
+ borderColor: "yellow",
2231
+ borderStyle: "round",
2232
+ padding: 1,
2233
+ margin: 1
2234
+ },
2235
+ info: {
2236
+ borderColor: "cyan",
2237
+ borderStyle: "round",
2238
+ padding: 1,
2239
+ margin: 1
2240
+ },
2241
+ plain: {
2242
+ borderColor: "gray",
2243
+ borderStyle: "round",
2244
+ padding: 1,
2245
+ margin: 1
2246
+ }
2247
+ };
2248
+ var tableStyles = {
2249
+ default: {
2250
+ style: {
2251
+ head: [],
2252
+ border: ["gray"]
2253
+ }
2254
+ },
2255
+ compact: {
2256
+ style: {
2257
+ head: [],
2258
+ border: ["dim"],
2259
+ compact: true
2402
2260
  }
2403
2261
  }
2404
- /**
2405
- * Filter commits by current user
2406
- */
2407
- async getCommitsByCurrentUser(options = {}) {
2408
- const userEmail = await this.getCurrentUserEmail();
2409
- if (!userEmail) {
2410
- logger.warning("Could not determine git user email, returning all commits");
2411
- return this.getCommitsWithStats(options);
2262
+ };
2263
+ var sizeIndicators = {
2264
+ small: colors.success("\u25CF"),
2265
+ medium: colors.warning("\u25CF"),
2266
+ large: colors.error("\u25CF")
2267
+ };
2268
+ function getSizeIndicator(insertions, deletions) {
2269
+ const total = insertions + deletions;
2270
+ if (total < 50) {
2271
+ return `${sizeIndicators.small} Small`;
2272
+ } else if (total < 200) {
2273
+ return `${sizeIndicators.medium} Medium`;
2274
+ } else {
2275
+ return `${sizeIndicators.large} Large`;
2276
+ }
2277
+ }
2278
+ function formatDiffStats(insertions, deletions) {
2279
+ return `${colors.success(`+${insertions}`)} ${colors.error(`-${deletions}`)}`;
2280
+ }
2281
+
2282
+ // src/commands/auth.ts
2283
+ async function authCommand(subcommand) {
2284
+ if (!subcommand || subcommand === "login") {
2285
+ await authLogin();
2286
+ } else if (subcommand === "status") {
2287
+ await authStatus();
2288
+ } else if (subcommand === "bitbucket") {
2289
+ await authBitbucket();
2290
+ } else if (subcommand === "gitlab") {
2291
+ await authGitLab();
2292
+ } else if (subcommand === "atlassian") {
2293
+ await authAtlassian();
2294
+ } else {
2295
+ logger.error(`Unknown auth subcommand: ${subcommand}`);
2296
+ logger.info("Available subcommands: login, status, bitbucket, gitlab, atlassian");
2297
+ process.exit(1);
2298
+ }
2299
+ }
2300
+ async function authLogin() {
2301
+ logger.log("");
2302
+ logger.info("Authenticating with Bragduck...");
2303
+ logger.log("");
2304
+ const isAuthenticated = await storageService.isServiceAuthenticated("bragduck");
2305
+ if (isAuthenticated) {
2306
+ const userInfo = authService.getUserInfo();
2307
+ if (userInfo) {
2308
+ logger.log(
2309
+ boxen(
2310
+ `${chalk3.yellow("Already authenticated!")}
2311
+
2312
+ ${chalk3.gray("User:")} ${userInfo.name}
2313
+ ${chalk3.gray("Email:")} ${userInfo.email}
2314
+
2315
+ ${chalk3.dim("Run")} ${chalk3.cyan("bragduck logout")} ${chalk3.dim("to sign out")}`,
2316
+ {
2317
+ padding: 1,
2318
+ margin: 1,
2319
+ borderStyle: "round",
2320
+ borderColor: "yellow"
2321
+ }
2322
+ )
2323
+ );
2324
+ logger.log("");
2325
+ return;
2412
2326
  }
2413
- logger.debug(`Filtering commits by user: ${userEmail}`);
2414
- return this.getCommitsWithStats({
2415
- ...options,
2416
- author: userEmail
2417
- });
2418
2327
  }
2419
- };
2420
- var gitService = new GitService();
2328
+ try {
2329
+ const userInfo = await authService.login();
2330
+ logger.log("");
2331
+ logger.log(
2332
+ boxen(
2333
+ `${chalk3.green.bold("\u2713 Successfully authenticated!")}
2421
2334
 
2422
- // src/services/github.service.ts
2423
- var execAsync2 = promisify2(exec2);
2424
- var GitHubService = class {
2425
- MAX_BODY_LENGTH = 5e3;
2426
- PR_SEARCH_FIELDS = "number,title,body,author,mergedAt,additions,deletions,changedFiles,url,labels";
2427
- /**
2428
- * Check if GitHub CLI is installed and available
2429
- */
2430
- async checkGitHubCLI() {
2431
- try {
2432
- await execAsync2("command gh --version");
2433
- return true;
2434
- } catch {
2435
- return false;
2436
- }
2335
+ ${chalk3.gray("Welcome,")} ${chalk3.cyan(userInfo.name)}
2336
+ ${chalk3.gray("Email:")} ${userInfo.email}
2337
+
2338
+ ${chalk3.dim("Use")} ${chalk3.cyan("bragduck sync")} ${chalk3.dim("to create brags")}`,
2339
+ {
2340
+ padding: 1,
2341
+ margin: 1,
2342
+ borderStyle: "round",
2343
+ borderColor: "green"
2344
+ }
2345
+ )
2346
+ );
2347
+ logger.log("");
2348
+ } catch (error) {
2349
+ const err = error;
2350
+ logger.log("");
2351
+ logger.log(
2352
+ boxen(`${chalk3.red.bold("\u2717 Authentication Failed")}
2353
+
2354
+ ${err.message}`, {
2355
+ padding: 1,
2356
+ margin: 1,
2357
+ borderStyle: "round",
2358
+ borderColor: "red"
2359
+ })
2360
+ );
2361
+ process.exit(1);
2437
2362
  }
2438
- /**
2439
- * Validate that GitHub CLI is installed
2440
- */
2441
- async ensureGitHubCLI() {
2442
- const isInstalled = await this.checkGitHubCLI();
2443
- if (!isInstalled) {
2444
- throw new GitHubError("GitHub CLI (gh) is not installed", {
2445
- hint: "Install from https://cli.github.com/"
2446
- });
2447
- }
2363
+ }
2364
+ async function authStatus() {
2365
+ logger.log("");
2366
+ logger.info("Authentication Status:");
2367
+ logger.log("");
2368
+ const services = await storageService.getAuthenticatedServices();
2369
+ const bragduckAuth = await storageService.isServiceAuthenticated("bragduck");
2370
+ if (bragduckAuth) {
2371
+ const userInfo = authService.getUserInfo();
2372
+ logger.info(`${colors.success("\u2713")} Bragduck: ${userInfo?.name || "Authenticated"}`);
2373
+ } else {
2374
+ logger.info(`${colors.error("\u2717")} Bragduck: Not authenticated`);
2448
2375
  }
2449
- /**
2450
- * Check if user is authenticated with GitHub CLI
2451
- */
2452
- async checkAuthentication() {
2453
- try {
2454
- await execAsync2("command gh auth status");
2455
- return true;
2456
- } catch {
2457
- return false;
2376
+ const ghStatus = await githubService.getAuthStatus();
2377
+ if (ghStatus.installed) {
2378
+ if (ghStatus.authenticated) {
2379
+ logger.info(`${colors.success("\u2713")} GitHub CLI (gh): Authenticated`);
2380
+ } else {
2381
+ logger.info(`${colors.error("\u2717")} GitHub CLI (gh): Not authenticated`);
2382
+ logger.info(theme.secondary(" Run: gh auth login"));
2458
2383
  }
2384
+ } else {
2385
+ logger.info(`${colors.error("\u2717")} GitHub CLI (gh): Not installed`);
2386
+ logger.info(theme.secondary(" Install from: https://cli.github.com"));
2459
2387
  }
2460
- /**
2461
- * Ensure user is authenticated with GitHub CLI
2462
- */
2463
- async ensureAuthentication() {
2464
- const isAuthenticated = await this.checkAuthentication();
2465
- if (!isAuthenticated) {
2466
- throw new GitHubError("Not authenticated with GitHub", {
2467
- hint: 'Run "gh auth login" to authenticate'
2468
- });
2388
+ for (const service of services) {
2389
+ if (service !== "bragduck") {
2390
+ logger.info(`${colors.success("\u2713")} ${service}: Authenticated`);
2469
2391
  }
2470
2392
  }
2471
- /**
2472
- * Validate that the current repository is hosted on GitHub
2473
- */
2474
- async validateGitHubRepository() {
2475
- try {
2476
- await this.ensureGitHubCLI();
2477
- await this.ensureAuthentication();
2478
- await gitService.validateRepository();
2479
- const { stdout } = await execAsync2("command gh repo view --json url");
2480
- const data = JSON.parse(stdout);
2481
- if (!data.url) {
2482
- throw new GitHubError("This repository is not hosted on GitHub", {
2483
- hint: "Only GitHub repositories are currently supported for PR scanning"
2484
- });
2485
- }
2486
- } catch (error) {
2487
- if (error instanceof GitHubError || error instanceof GitError) {
2488
- throw error;
2489
- }
2490
- if (error.message?.includes("not a git repository")) {
2491
- throw new GitHubError("Not a git repository", {
2492
- hint: "Navigate to a git repository directory"
2493
- });
2494
- }
2495
- if (error.message?.includes("Could not resolve to a Repository")) {
2496
- throw new GitHubError("This repository is not hosted on GitHub", {
2497
- hint: "Only GitHub repositories are currently supported for PR scanning"
2498
- });
2499
- }
2500
- throw new GitHubError("Failed to validate GitHub repository", {
2501
- originalError: error.message
2502
- });
2503
- }
2393
+ logger.log("");
2394
+ if (services.length === 0) {
2395
+ logger.info(`Run ${theme.command("bragduck auth login")} to authenticate with Bragduck`);
2396
+ logger.log("");
2504
2397
  }
2505
- /**
2506
- * Get GitHub repository information
2507
- */
2508
- async getRepositoryInfo() {
2509
- try {
2510
- await this.ensureGitHubCLI();
2511
- const { stdout } = await execAsync2(
2512
- "command gh repo view --json owner,name,url,nameWithOwner"
2513
- );
2514
- const data = JSON.parse(stdout);
2515
- return {
2516
- owner: data.owner.login,
2517
- name: data.name,
2518
- fullName: data.nameWithOwner,
2519
- url: data.url,
2520
- isGitHub: true
2521
- };
2522
- } catch (error) {
2523
- if (error instanceof GitHubError || error instanceof GitError) {
2524
- throw error;
2398
+ }
2399
+ async function authBitbucket() {
2400
+ logger.log("");
2401
+ logger.log(
2402
+ boxen(
2403
+ 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)"),
2404
+ boxStyles.info
2405
+ )
2406
+ );
2407
+ logger.log("");
2408
+ try {
2409
+ const email = await input({
2410
+ message: "Atlassian account email:",
2411
+ validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
2412
+ });
2413
+ const apiToken = await input({
2414
+ message: "API Token:",
2415
+ validate: (value) => value.length > 0 ? true : "API token cannot be empty"
2416
+ });
2417
+ const tokenExpiry = await input({
2418
+ message: "Token expiry date (YYYY-MM-DD, optional):",
2419
+ default: "",
2420
+ validate: (value) => {
2421
+ if (!value) return true;
2422
+ const date = new Date(value);
2423
+ return !isNaN(date.getTime()) ? true : "Please enter a valid date (YYYY-MM-DD)";
2525
2424
  }
2526
- throw new GitHubError("Failed to get GitHub repository information", {
2527
- originalError: error.message
2528
- });
2425
+ });
2426
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
2427
+ const response = await fetch("https://api.bitbucket.org/2.0/user", {
2428
+ headers: { Authorization: `Basic ${auth}` }
2429
+ });
2430
+ if (!response.ok) {
2431
+ logger.log("");
2432
+ logger.log(
2433
+ boxen(
2434
+ theme.error("\u2717 Authentication Failed") + "\n\nInvalid email or API token",
2435
+ boxStyles.error
2436
+ )
2437
+ );
2438
+ logger.log("");
2439
+ process.exit(1);
2529
2440
  }
2530
- }
2531
- /**
2532
- * Get the current authenticated GitHub user
2533
- */
2534
- async getCurrentGitHubUser() {
2535
- try {
2536
- const { stdout } = await execAsync2("command gh api user --jq .login");
2537
- return stdout.trim() || null;
2538
- } catch {
2539
- logger.debug("Failed to get GitHub user");
2540
- return null;
2441
+ const user = await response.json();
2442
+ const credentials = {
2443
+ accessToken: apiToken,
2444
+ username: email
2445
+ // Store email in username field
2446
+ };
2447
+ if (tokenExpiry) {
2448
+ const expiryDate = new Date(tokenExpiry);
2449
+ credentials.expiresAt = expiryDate.getTime();
2541
2450
  }
2451
+ await storageService.setServiceCredentials("bitbucket", credentials);
2452
+ logger.log("");
2453
+ logger.log(
2454
+ boxen(
2455
+ theme.success("\u2713 Successfully authenticated with Bitbucket") + `
2456
+
2457
+ Email: ${email}
2458
+ User: ${user.display_name}
2459
+ ` + (tokenExpiry ? `Expires: ${tokenExpiry}` : ""),
2460
+ boxStyles.success
2461
+ )
2462
+ );
2463
+ logger.log("");
2464
+ } catch (error) {
2465
+ const err = error;
2466
+ logger.log("");
2467
+ logger.log(
2468
+ boxen(
2469
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2470
+ boxStyles.error
2471
+ )
2472
+ );
2473
+ logger.log("");
2474
+ process.exit(1);
2542
2475
  }
2543
- /**
2544
- * Fetch merged PRs from GitHub
2545
- */
2546
- async getMergedPRs(options = {}) {
2547
- const { days = 30, limit, author } = options;
2548
- try {
2549
- await this.ensureGitHubCLI();
2550
- await this.ensureAuthentication();
2551
- logger.debug(`Fetching merged PRs from the last ${days} days`);
2552
- const since = /* @__PURE__ */ new Date();
2553
- since.setDate(since.getDate() - days);
2554
- const sinceDate = since.toISOString().split("T")[0];
2555
- let searchQuery = `merged:>=${sinceDate}`;
2556
- if (author) {
2557
- searchQuery += ` author:${author}`;
2558
- }
2559
- const limitArg = limit ? `--limit ${limit}` : "";
2560
- const command = `command gh pr list --state merged --json ${this.PR_SEARCH_FIELDS} --search "${searchQuery}" ${limitArg}`;
2561
- logger.debug(`Running: ${command}`);
2562
- const { stdout } = await execAsync2(command);
2563
- const prs = JSON.parse(stdout);
2564
- logger.debug(`Found ${prs.length} merged PRs`);
2565
- return prs;
2566
- } catch (error) {
2567
- if (error instanceof GitHubError || error instanceof GitError) {
2568
- throw error;
2569
- }
2570
- throw new GitHubError("Failed to fetch PRs from GitHub", {
2571
- originalError: error.message,
2572
- days
2573
- });
2476
+ }
2477
+ async function authGitLab() {
2478
+ logger.log("");
2479
+ logger.log(
2480
+ boxen(
2481
+ 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"),
2482
+ boxStyles.info
2483
+ )
2484
+ );
2485
+ logger.log("");
2486
+ try {
2487
+ const instanceUrl = await input({
2488
+ message: "GitLab instance URL (press Enter for gitlab.com):",
2489
+ default: "https://gitlab.com"
2490
+ });
2491
+ const accessToken = await input({
2492
+ message: "Personal Access Token:",
2493
+ validate: (value) => value.length > 0 ? true : "Token cannot be empty"
2494
+ });
2495
+ const testUrl = `${instanceUrl.replace(/\/$/, "")}/api/v4/user`;
2496
+ const response = await fetch(testUrl, {
2497
+ headers: { "PRIVATE-TOKEN": accessToken }
2498
+ });
2499
+ if (!response.ok) {
2500
+ logger.log("");
2501
+ logger.log(
2502
+ boxen(
2503
+ theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL or access token",
2504
+ boxStyles.error
2505
+ )
2506
+ );
2507
+ logger.log("");
2508
+ process.exit(1);
2574
2509
  }
2510
+ const user = await response.json();
2511
+ const credentials = {
2512
+ accessToken,
2513
+ instanceUrl: instanceUrl === "https://gitlab.com" ? void 0 : instanceUrl
2514
+ };
2515
+ await storageService.setServiceCredentials("gitlab", credentials);
2516
+ logger.log("");
2517
+ logger.log(
2518
+ boxen(
2519
+ theme.success("\u2713 Successfully authenticated with GitLab") + `
2520
+
2521
+ Instance: ${instanceUrl}
2522
+ User: ${user.name} (@${user.username})`,
2523
+ boxStyles.success
2524
+ )
2525
+ );
2526
+ logger.log("");
2527
+ } catch (error) {
2528
+ const err = error;
2529
+ logger.log("");
2530
+ logger.log(
2531
+ boxen(
2532
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2533
+ boxStyles.error
2534
+ )
2535
+ );
2536
+ logger.log("");
2537
+ process.exit(1);
2575
2538
  }
2576
- /**
2577
- * Get merged PRs by current user (PRs authored by the current user)
2578
- */
2579
- async getPRsByCurrentUser(options = {}) {
2580
- const currentUser = await this.getCurrentGitHubUser();
2581
- if (!currentUser) {
2582
- logger.warning("Could not determine GitHub user, returning all PRs");
2583
- return this.getMergedPRs(options);
2539
+ }
2540
+ async function authAtlassian() {
2541
+ logger.log("");
2542
+ logger.log(
2543
+ boxen(
2544
+ 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",
2545
+ boxStyles.info
2546
+ )
2547
+ );
2548
+ logger.log("");
2549
+ try {
2550
+ const instanceUrl = await input({
2551
+ message: "Atlassian instance URL (e.g., company.atlassian.net):",
2552
+ validate: (value) => value.length > 0 ? true : "Instance URL cannot be empty"
2553
+ });
2554
+ const email = await input({
2555
+ message: "Atlassian account email:",
2556
+ validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
2557
+ });
2558
+ const apiToken = await input({
2559
+ message: "API Token:",
2560
+ validate: (value) => value.length > 0 ? true : "API token cannot be empty"
2561
+ });
2562
+ const testInstanceUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
2563
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
2564
+ const response = await fetch(`${testInstanceUrl}/rest/api/2/myself`, {
2565
+ headers: { Authorization: `Basic ${auth}` }
2566
+ });
2567
+ if (!response.ok) {
2568
+ logger.log("");
2569
+ logger.log(
2570
+ boxen(
2571
+ 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)",
2572
+ boxStyles.error
2573
+ )
2574
+ );
2575
+ logger.log("");
2576
+ process.exit(1);
2584
2577
  }
2585
- logger.debug(`Filtering PRs by author: ${currentUser}`);
2586
- return this.getMergedPRs({
2587
- ...options,
2588
- author: currentUser
2578
+ const user = await response.json();
2579
+ const credentials = {
2580
+ accessToken: apiToken,
2581
+ username: email,
2582
+ instanceUrl: instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`
2583
+ };
2584
+ await storageService.setServiceCredentials("jira", credentials);
2585
+ await storageService.setServiceCredentials("confluence", credentials);
2586
+ await storageService.setServiceCredentials("bitbucket", {
2587
+ ...credentials,
2588
+ instanceUrl: "https://api.bitbucket.org"
2589
+ // Bitbucket uses different API base
2589
2590
  });
2590
- }
2591
- /**
2592
- * Transform a GitHub PR into a GitCommit structure
2593
- * This allows PR data to work with existing commit-based UI and API
2594
- */
2595
- transformPRToCommit(pr) {
2596
- const title = pr.title;
2597
- const body = pr.body || "";
2598
- const truncatedBody = body.length > this.MAX_BODY_LENGTH ? body.substring(0, this.MAX_BODY_LENGTH) + "...[truncated]" : body;
2599
- const message = truncatedBody ? `${title}
2591
+ logger.log("");
2592
+ logger.log(
2593
+ boxen(
2594
+ theme.success("\u2713 Successfully authenticated with Atlassian") + `
2600
2595
 
2601
- ${truncatedBody}` : title;
2602
- return {
2603
- sha: `pr-${pr.number}`,
2604
- message,
2605
- author: pr.author.login,
2606
- authorEmail: "",
2607
- // Not available from gh PR API
2608
- date: pr.mergedAt,
2609
- url: pr.url,
2610
- diffStats: {
2611
- filesChanged: pr.changedFiles,
2612
- insertions: pr.additions,
2613
- deletions: pr.deletions
2614
- }
2615
- };
2596
+ Instance: ${instanceUrl}
2597
+ User: ${user.displayName}
2598
+ Email: ${user.emailAddress}
2599
+
2600
+ Services configured:
2601
+ \u2022 Jira
2602
+ \u2022 Confluence
2603
+ \u2022 Bitbucket`,
2604
+ boxStyles.success
2605
+ )
2606
+ );
2607
+ logger.log("");
2608
+ } catch (error) {
2609
+ const err = error;
2610
+ logger.log("");
2611
+ logger.log(
2612
+ boxen(
2613
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
2614
+ boxStyles.error
2615
+ )
2616
+ );
2617
+ logger.log("");
2618
+ process.exit(1);
2616
2619
  }
2620
+ }
2621
+
2622
+ // src/commands/sync.ts
2623
+ init_esm_shims();
2624
+
2625
+ // node_modules/@inquirer/core/dist/esm/lib/errors.mjs
2626
+ init_esm_shims();
2627
+ var CancelPromptError = class extends Error {
2628
+ name = "CancelPromptError";
2629
+ message = "Prompt was canceled";
2617
2630
  };
2618
- var githubService = new GitHubService();
2631
+
2632
+ // src/commands/sync.ts
2633
+ init_api_service();
2634
+ init_storage_service();
2635
+ init_auth_service();
2636
+ init_env_loader();
2637
+ init_config_loader();
2638
+ import { select as select2 } from "@inquirer/prompts";
2639
+ import boxen6 from "boxen";
2640
+
2641
+ // src/sync/adapter-factory.ts
2642
+ init_esm_shims();
2619
2643
 
2620
2644
  // src/sync/github-adapter.ts
2645
+ init_esm_shims();
2621
2646
  var GitHubSyncAdapter = class {
2622
2647
  name = "github";
2623
2648
  async validate() {
@@ -3305,6 +3330,11 @@ var JiraService = class {
3305
3330
  throw new JiraError("Resource not found", {
3306
3331
  originalError: statusText
3307
3332
  });
3333
+ } else if (status === 410) {
3334
+ throw new JiraError("Resource no longer available (deleted, archived, or moved)", {
3335
+ hint: "The Jira issue, project, or instance may have been deleted/archived. Verify your instance URL and that the resource still exists.",
3336
+ originalError: statusText
3337
+ });
3308
3338
  } else if (status === 429) {
3309
3339
  throw new JiraError("Rate limit exceeded", {
3310
3340
  hint: "Wait a few minutes before trying again",
@@ -3574,6 +3604,11 @@ var ConfluenceService = class {
3574
3604
  throw new ConfluenceError("Resource not found", {
3575
3605
  originalError: statusText
3576
3606
  });
3607
+ } else if (status === 410) {
3608
+ throw new ConfluenceError("Resource no longer available (deleted, archived, or moved)", {
3609
+ hint: "The Confluence page, space, or instance may have been deleted/archived. Verify your instance URL and that the resource still exists.",
3610
+ originalError: statusText
3611
+ });
3577
3612
  } else if (status === 429) {
3578
3613
  throw new ConfluenceError("Rate limit exceeded", {
3579
3614
  hint: "Wait a few minutes before trying again",