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