@bragduck/cli 2.8.3 → 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.
- package/dist/bin/bragduck.js +889 -868
- package/dist/bin/bragduck.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/bragduck.js
CHANGED
|
@@ -1150,7 +1150,7 @@ var init_auth_service = __esm({
|
|
|
1150
1150
|
refreshToken: tokenResponse.refresh_token,
|
|
1151
1151
|
expiresAt
|
|
1152
1152
|
};
|
|
1153
|
-
await storageService.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
1353
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
1354
1354
|
function getCliVersion() {
|
|
1355
1355
|
try {
|
|
1356
|
-
const packageJsonPath2 =
|
|
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,976 @@ 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/
|
|
1676
|
+
// src/services/github.service.ts
|
|
1678
1677
|
init_esm_shims();
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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
|
-
*
|
|
1716
|
+
* Validate that the current directory is a git repository
|
|
1724
1717
|
*/
|
|
1725
|
-
|
|
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
|
-
*
|
|
1738
|
+
* Get repository information
|
|
1728
1739
|
*/
|
|
1729
|
-
|
|
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
|
-
*
|
|
1762
|
+
* Fetch recent commits
|
|
1732
1763
|
*/
|
|
1733
|
-
|
|
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
|
-
*
|
|
1802
|
+
* Get diff statistics for a specific commit
|
|
1736
1803
|
*/
|
|
1737
|
-
|
|
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
|
-
*
|
|
1842
|
+
* Get commits with their diff statistics
|
|
1740
1843
|
*/
|
|
1741
|
-
|
|
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
|
-
*
|
|
1864
|
+
* Get the current git user email
|
|
1744
1865
|
*/
|
|
1745
|
-
|
|
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
|
-
*
|
|
1876
|
+
* Get the current git user name
|
|
1748
1877
|
*/
|
|
1749
|
-
|
|
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
|
-
*
|
|
1888
|
+
* Filter commits by current user
|
|
1752
1889
|
*/
|
|
1753
|
-
|
|
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
|
-
*
|
|
1911
|
+
* Check if GitHub CLI is installed and available
|
|
1756
1912
|
*/
|
|
1757
|
-
|
|
1913
|
+
async checkGitHubCLI() {
|
|
1914
|
+
try {
|
|
1915
|
+
await execAsync2("command gh --version");
|
|
1916
|
+
return true;
|
|
1917
|
+
} catch {
|
|
1918
|
+
return false;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1758
1921
|
/**
|
|
1759
|
-
*
|
|
1922
|
+
* Validate that GitHub CLI is installed
|
|
1760
1923
|
*/
|
|
1761
|
-
|
|
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
|
-
*
|
|
1933
|
+
* Check if user is authenticated with GitHub CLI
|
|
1764
1934
|
*/
|
|
1765
|
-
|
|
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
|
-
*
|
|
1944
|
+
* Ensure user is authenticated with GitHub CLI
|
|
1768
1945
|
*/
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
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
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
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
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
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
|
-
|
|
2169
|
-
|
|
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
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
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/
|
|
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/
|
|
2118
|
+
// src/ui/theme.ts
|
|
2205
2119
|
init_esm_shims();
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
//
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
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}:`),
|
|
2232
2175
|
/**
|
|
2233
|
-
*
|
|
2176
|
+
* Format values
|
|
2234
2177
|
*/
|
|
2235
|
-
|
|
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
|
-
}
|
|
2178
|
+
value: (text) => colors.highlight(text),
|
|
2254
2179
|
/**
|
|
2255
|
-
*
|
|
2180
|
+
* Format counts and numbers
|
|
2256
2181
|
*/
|
|
2257
|
-
|
|
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
|
-
}
|
|
2182
|
+
count: (num) => colors.highlightCyan(num.toString()),
|
|
2278
2183
|
/**
|
|
2279
|
-
*
|
|
2184
|
+
* Format file paths
|
|
2280
2185
|
*/
|
|
2281
|
-
|
|
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
|
-
}
|
|
2186
|
+
path: (text) => colors.info(text),
|
|
2318
2187
|
/**
|
|
2319
|
-
*
|
|
2188
|
+
* Format step indicators (e.g., "Step 1/5:")
|
|
2320
2189
|
*/
|
|
2321
|
-
|
|
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
|
-
}
|
|
2190
|
+
step: (current, total) => colors.primary(`[${current}/${total}]`),
|
|
2358
2191
|
/**
|
|
2359
|
-
*
|
|
2192
|
+
* Format progress text
|
|
2360
2193
|
*/
|
|
2361
|
-
|
|
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
|
-
}
|
|
2194
|
+
progress: (text) => colors.infoDim(text),
|
|
2380
2195
|
/**
|
|
2381
|
-
*
|
|
2196
|
+
* Format emphasized text
|
|
2382
2197
|
*/
|
|
2383
|
-
|
|
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
|
-
}
|
|
2198
|
+
emphasis: (text) => colors.bold(text),
|
|
2392
2199
|
/**
|
|
2393
|
-
*
|
|
2200
|
+
* Format de-emphasized/secondary text
|
|
2394
2201
|
*/
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2202
|
+
secondary: (text) => colors.dim(text),
|
|
2203
|
+
/**
|
|
2204
|
+
* Format interactive elements (prompts, selections)
|
|
2205
|
+
*/
|
|
2206
|
+
interactive: (text) => colors.primary(text),
|
|
2207
|
+
/**
|
|
2208
|
+
* Format keys in key-value pairs
|
|
2209
|
+
*/
|
|
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
|
|
2402
2256
|
}
|
|
2403
2257
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
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;
|
|
2412
2322
|
}
|
|
2413
|
-
logger.debug(`Filtering commits by user: ${userEmail}`);
|
|
2414
|
-
return this.getCommitsWithStats({
|
|
2415
|
-
...options,
|
|
2416
|
-
author: userEmail
|
|
2417
|
-
});
|
|
2418
2323
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2324
|
+
try {
|
|
2325
|
+
const userInfo = await authService.login();
|
|
2326
|
+
logger.log("");
|
|
2327
|
+
logger.log(
|
|
2328
|
+
boxen(
|
|
2329
|
+
`${chalk3.green.bold("\u2713 Successfully authenticated!")}
|
|
2421
2330
|
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
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);
|
|
2437
2358
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
}
|
|
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`);
|
|
2448
2371
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
} catch {
|
|
2457
|
-
return false;
|
|
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"));
|
|
2458
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"));
|
|
2459
2383
|
}
|
|
2460
|
-
|
|
2461
|
-
|
|
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
|
-
});
|
|
2384
|
+
for (const service of services) {
|
|
2385
|
+
if (service !== "bragduck") {
|
|
2386
|
+
logger.info(`${colors.success("\u2713")} ${service}: Authenticated`);
|
|
2469
2387
|
}
|
|
2470
2388
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
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
|
-
}
|
|
2389
|
+
logger.log("");
|
|
2390
|
+
if (services.length === 0) {
|
|
2391
|
+
logger.info(`Run ${theme.command("bragduck auth login")} to authenticate with Bragduck`);
|
|
2392
|
+
logger.log("");
|
|
2504
2393
|
}
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
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)";
|
|
2525
2420
|
}
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
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);
|
|
2529
2436
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
logger.debug("Failed to get GitHub user");
|
|
2540
|
-
return null;
|
|
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();
|
|
2541
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);
|
|
2542
2471
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
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);
|
|
2574
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);
|
|
2575
2534
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
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);
|
|
2584
2573
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
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
|
|
2589
2586
|
});
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
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}
|
|
2587
|
+
logger.log("");
|
|
2588
|
+
logger.log(
|
|
2589
|
+
boxen(
|
|
2590
|
+
theme.success("\u2713 Successfully authenticated with Atlassian") + `
|
|
2600
2591
|
|
|
2601
|
-
${
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
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);
|
|
2616
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";
|
|
2617
2626
|
};
|
|
2618
|
-
|
|
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();
|
|
2619
2639
|
|
|
2620
2640
|
// src/sync/github-adapter.ts
|
|
2641
|
+
init_esm_shims();
|
|
2621
2642
|
var GitHubSyncAdapter = class {
|
|
2622
2643
|
name = "github";
|
|
2623
2644
|
async validate() {
|