@bragduck/cli 2.4.0 → 2.6.0
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 +749 -9
- package/dist/bin/bragduck.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/bragduck.js
CHANGED
|
@@ -392,7 +392,7 @@ var init_storage_service = __esm({
|
|
|
392
392
|
});
|
|
393
393
|
|
|
394
394
|
// src/utils/errors.ts
|
|
395
|
-
var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError;
|
|
395
|
+
var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError;
|
|
396
396
|
var init_errors = __esm({
|
|
397
397
|
"src/utils/errors.ts"() {
|
|
398
398
|
"use strict";
|
|
@@ -459,6 +459,18 @@ var init_errors = __esm({
|
|
|
459
459
|
this.name = "GitHubError";
|
|
460
460
|
}
|
|
461
461
|
};
|
|
462
|
+
BitbucketError = class extends BragduckError {
|
|
463
|
+
constructor(message, details) {
|
|
464
|
+
super(message, "BITBUCKET_ERROR", details);
|
|
465
|
+
this.name = "BitbucketError";
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
GitLabError = class extends BragduckError {
|
|
469
|
+
constructor(message, details) {
|
|
470
|
+
super(message, "GITLAB_ERROR", details);
|
|
471
|
+
this.name = "GitLabError";
|
|
472
|
+
}
|
|
473
|
+
};
|
|
462
474
|
}
|
|
463
475
|
});
|
|
464
476
|
|
|
@@ -1437,6 +1449,7 @@ init_storage_service();
|
|
|
1437
1449
|
init_logger();
|
|
1438
1450
|
import boxen from "boxen";
|
|
1439
1451
|
import chalk3 from "chalk";
|
|
1452
|
+
import { input } from "@inquirer/prompts";
|
|
1440
1453
|
|
|
1441
1454
|
// src/ui/theme.ts
|
|
1442
1455
|
init_esm_shims();
|
|
@@ -1604,9 +1617,13 @@ async function authCommand(subcommand) {
|
|
|
1604
1617
|
await authLogin();
|
|
1605
1618
|
} else if (subcommand === "status") {
|
|
1606
1619
|
await authStatus();
|
|
1620
|
+
} else if (subcommand === "bitbucket") {
|
|
1621
|
+
await authBitbucket();
|
|
1622
|
+
} else if (subcommand === "gitlab") {
|
|
1623
|
+
await authGitLab();
|
|
1607
1624
|
} else {
|
|
1608
1625
|
logger.error(`Unknown auth subcommand: ${subcommand}`);
|
|
1609
|
-
logger.info("Available subcommands: login, status");
|
|
1626
|
+
logger.info("Available subcommands: login, status, bitbucket, gitlab");
|
|
1610
1627
|
process.exit(1);
|
|
1611
1628
|
}
|
|
1612
1629
|
}
|
|
@@ -1700,6 +1717,147 @@ async function authStatus() {
|
|
|
1700
1717
|
}
|
|
1701
1718
|
logger.log("");
|
|
1702
1719
|
}
|
|
1720
|
+
async function authBitbucket() {
|
|
1721
|
+
logger.log("");
|
|
1722
|
+
logger.log(
|
|
1723
|
+
boxen(
|
|
1724
|
+
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)"),
|
|
1725
|
+
boxStyles.info
|
|
1726
|
+
)
|
|
1727
|
+
);
|
|
1728
|
+
logger.log("");
|
|
1729
|
+
try {
|
|
1730
|
+
const email = await input({
|
|
1731
|
+
message: "Atlassian account email:",
|
|
1732
|
+
validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
|
|
1733
|
+
});
|
|
1734
|
+
const apiToken = await input({
|
|
1735
|
+
message: "API Token:",
|
|
1736
|
+
validate: (value) => value.length > 0 ? true : "API token cannot be empty"
|
|
1737
|
+
});
|
|
1738
|
+
const tokenExpiry = await input({
|
|
1739
|
+
message: "Token expiry date (YYYY-MM-DD, optional):",
|
|
1740
|
+
default: "",
|
|
1741
|
+
validate: (value) => {
|
|
1742
|
+
if (!value) return true;
|
|
1743
|
+
const date = new Date(value);
|
|
1744
|
+
return !isNaN(date.getTime()) ? true : "Please enter a valid date (YYYY-MM-DD)";
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
1748
|
+
const response = await fetch("https://api.bitbucket.org/2.0/user", {
|
|
1749
|
+
headers: { Authorization: `Basic ${auth}` }
|
|
1750
|
+
});
|
|
1751
|
+
if (!response.ok) {
|
|
1752
|
+
logger.log("");
|
|
1753
|
+
logger.log(
|
|
1754
|
+
boxen(
|
|
1755
|
+
theme.error("\u2717 Authentication Failed") + "\n\nInvalid email or API token",
|
|
1756
|
+
boxStyles.error
|
|
1757
|
+
)
|
|
1758
|
+
);
|
|
1759
|
+
logger.log("");
|
|
1760
|
+
process.exit(1);
|
|
1761
|
+
}
|
|
1762
|
+
const user = await response.json();
|
|
1763
|
+
const credentials = {
|
|
1764
|
+
accessToken: apiToken,
|
|
1765
|
+
username: email
|
|
1766
|
+
// Store email in username field
|
|
1767
|
+
};
|
|
1768
|
+
if (tokenExpiry) {
|
|
1769
|
+
const expiryDate = new Date(tokenExpiry);
|
|
1770
|
+
credentials.expiresAt = expiryDate.getTime();
|
|
1771
|
+
}
|
|
1772
|
+
await storageService.setServiceCredentials("bitbucket", credentials);
|
|
1773
|
+
logger.log("");
|
|
1774
|
+
logger.log(
|
|
1775
|
+
boxen(
|
|
1776
|
+
theme.success("\u2713 Successfully authenticated with Bitbucket") + `
|
|
1777
|
+
|
|
1778
|
+
Email: ${email}
|
|
1779
|
+
User: ${user.display_name}
|
|
1780
|
+
` + (tokenExpiry ? `Expires: ${tokenExpiry}` : ""),
|
|
1781
|
+
boxStyles.success
|
|
1782
|
+
)
|
|
1783
|
+
);
|
|
1784
|
+
logger.log("");
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
const err = error;
|
|
1787
|
+
logger.log("");
|
|
1788
|
+
logger.log(
|
|
1789
|
+
boxen(
|
|
1790
|
+
theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
|
|
1791
|
+
boxStyles.error
|
|
1792
|
+
)
|
|
1793
|
+
);
|
|
1794
|
+
logger.log("");
|
|
1795
|
+
process.exit(1);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
async function authGitLab() {
|
|
1799
|
+
logger.log("");
|
|
1800
|
+
logger.log(
|
|
1801
|
+
boxen(
|
|
1802
|
+
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"),
|
|
1803
|
+
boxStyles.info
|
|
1804
|
+
)
|
|
1805
|
+
);
|
|
1806
|
+
logger.log("");
|
|
1807
|
+
try {
|
|
1808
|
+
const instanceUrl = await input({
|
|
1809
|
+
message: "GitLab instance URL (press Enter for gitlab.com):",
|
|
1810
|
+
default: "https://gitlab.com"
|
|
1811
|
+
});
|
|
1812
|
+
const accessToken = await input({
|
|
1813
|
+
message: "Personal Access Token:",
|
|
1814
|
+
validate: (value) => value.length > 0 ? true : "Token cannot be empty"
|
|
1815
|
+
});
|
|
1816
|
+
const testUrl = `${instanceUrl.replace(/\/$/, "")}/api/v4/user`;
|
|
1817
|
+
const response = await fetch(testUrl, {
|
|
1818
|
+
headers: { "PRIVATE-TOKEN": accessToken }
|
|
1819
|
+
});
|
|
1820
|
+
if (!response.ok) {
|
|
1821
|
+
logger.log("");
|
|
1822
|
+
logger.log(
|
|
1823
|
+
boxen(
|
|
1824
|
+
theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL or access token",
|
|
1825
|
+
boxStyles.error
|
|
1826
|
+
)
|
|
1827
|
+
);
|
|
1828
|
+
logger.log("");
|
|
1829
|
+
process.exit(1);
|
|
1830
|
+
}
|
|
1831
|
+
const user = await response.json();
|
|
1832
|
+
const credentials = {
|
|
1833
|
+
accessToken,
|
|
1834
|
+
instanceUrl: instanceUrl === "https://gitlab.com" ? void 0 : instanceUrl
|
|
1835
|
+
};
|
|
1836
|
+
await storageService.setServiceCredentials("gitlab", credentials);
|
|
1837
|
+
logger.log("");
|
|
1838
|
+
logger.log(
|
|
1839
|
+
boxen(
|
|
1840
|
+
theme.success("\u2713 Successfully authenticated with GitLab") + `
|
|
1841
|
+
|
|
1842
|
+
Instance: ${instanceUrl}
|
|
1843
|
+
User: ${user.name} (@${user.username})`,
|
|
1844
|
+
boxStyles.success
|
|
1845
|
+
)
|
|
1846
|
+
);
|
|
1847
|
+
logger.log("");
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
const err = error;
|
|
1850
|
+
logger.log("");
|
|
1851
|
+
logger.log(
|
|
1852
|
+
boxen(
|
|
1853
|
+
theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
|
|
1854
|
+
boxStyles.error
|
|
1855
|
+
)
|
|
1856
|
+
);
|
|
1857
|
+
logger.log("");
|
|
1858
|
+
process.exit(1);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1703
1861
|
|
|
1704
1862
|
// src/commands/sync.ts
|
|
1705
1863
|
init_esm_shims();
|
|
@@ -1720,6 +1878,7 @@ import boxen6 from "boxen";
|
|
|
1720
1878
|
// src/utils/source-detector.ts
|
|
1721
1879
|
init_esm_shims();
|
|
1722
1880
|
init_errors();
|
|
1881
|
+
init_storage_service();
|
|
1723
1882
|
import { exec as exec2 } from "child_process";
|
|
1724
1883
|
import { promisify as promisify2 } from "util";
|
|
1725
1884
|
var execAsync2 = promisify2(exec2);
|
|
@@ -1821,6 +1980,10 @@ var SourceDetector = class {
|
|
|
1821
1980
|
if (type === "github") {
|
|
1822
1981
|
await execAsync2("command gh auth status");
|
|
1823
1982
|
return true;
|
|
1983
|
+
} else if (type === "bitbucket" || type === "atlassian") {
|
|
1984
|
+
return await storageService.isServiceAuthenticated("bitbucket");
|
|
1985
|
+
} else if (type === "gitlab") {
|
|
1986
|
+
return await storageService.isServiceAuthenticated("gitlab");
|
|
1824
1987
|
}
|
|
1825
1988
|
return false;
|
|
1826
1989
|
} catch {
|
|
@@ -2318,6 +2481,581 @@ var GitHubSyncAdapter = class {
|
|
|
2318
2481
|
};
|
|
2319
2482
|
var githubSyncAdapter = new GitHubSyncAdapter();
|
|
2320
2483
|
|
|
2484
|
+
// src/sync/bitbucket-adapter.ts
|
|
2485
|
+
init_esm_shims();
|
|
2486
|
+
|
|
2487
|
+
// src/services/bitbucket.service.ts
|
|
2488
|
+
init_esm_shims();
|
|
2489
|
+
init_errors();
|
|
2490
|
+
init_logger();
|
|
2491
|
+
init_storage_service();
|
|
2492
|
+
import { exec as exec4 } from "child_process";
|
|
2493
|
+
import { promisify as promisify4 } from "util";
|
|
2494
|
+
var execAsync4 = promisify4(exec4);
|
|
2495
|
+
var BitbucketService = class {
|
|
2496
|
+
BITBUCKET_API_BASE = "https://api.bitbucket.org/2.0";
|
|
2497
|
+
MAX_DESCRIPTION_LENGTH = 5e3;
|
|
2498
|
+
/**
|
|
2499
|
+
* Get stored Bitbucket credentials
|
|
2500
|
+
*/
|
|
2501
|
+
async getCredentials() {
|
|
2502
|
+
const creds = await storageService.getServiceCredentials("bitbucket");
|
|
2503
|
+
if (!creds || !creds.username || !creds.accessToken) {
|
|
2504
|
+
throw new BitbucketError("Not authenticated with Bitbucket", {
|
|
2505
|
+
hint: "Run: bragduck auth bitbucket"
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
if (creds.expiresAt && creds.expiresAt < Date.now()) {
|
|
2509
|
+
throw new BitbucketError("API token has expired", {
|
|
2510
|
+
hint: "Run: bragduck auth bitbucket"
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
return {
|
|
2514
|
+
email: creds.username,
|
|
2515
|
+
// username field stores email
|
|
2516
|
+
apiToken: creds.accessToken
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Make authenticated request to Bitbucket API
|
|
2521
|
+
*/
|
|
2522
|
+
async request(endpoint) {
|
|
2523
|
+
const { email, apiToken } = await this.getCredentials();
|
|
2524
|
+
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
2525
|
+
logger.debug(`Bitbucket API: GET ${endpoint}`);
|
|
2526
|
+
const response = await fetch(`${this.BITBUCKET_API_BASE}${endpoint}`, {
|
|
2527
|
+
headers: {
|
|
2528
|
+
Authorization: `Basic ${auth}`,
|
|
2529
|
+
Accept: "application/json"
|
|
2530
|
+
}
|
|
2531
|
+
});
|
|
2532
|
+
if (!response.ok) {
|
|
2533
|
+
const statusText = response.statusText;
|
|
2534
|
+
const status = response.status;
|
|
2535
|
+
if (status === 401) {
|
|
2536
|
+
throw new BitbucketError("Invalid or expired API token", {
|
|
2537
|
+
hint: "Run: bragduck auth bitbucket",
|
|
2538
|
+
originalError: statusText
|
|
2539
|
+
});
|
|
2540
|
+
} else if (status === 403) {
|
|
2541
|
+
throw new BitbucketError("Forbidden - check token permissions", {
|
|
2542
|
+
hint: "Token needs: pullrequest:read, repository:read, account:read",
|
|
2543
|
+
originalError: statusText
|
|
2544
|
+
});
|
|
2545
|
+
} else if (status === 404) {
|
|
2546
|
+
throw new BitbucketError("Repository or resource not found", {
|
|
2547
|
+
originalError: statusText
|
|
2548
|
+
});
|
|
2549
|
+
} else if (status === 429) {
|
|
2550
|
+
throw new BitbucketError("Rate limit exceeded", {
|
|
2551
|
+
hint: "Wait a few minutes before trying again",
|
|
2552
|
+
originalError: statusText
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
throw new BitbucketError(`API request failed: ${statusText}`, {
|
|
2556
|
+
originalError: statusText
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
return response.json();
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Extract workspace and repo from git remote URL
|
|
2563
|
+
*/
|
|
2564
|
+
parseRemoteUrl(url) {
|
|
2565
|
+
const match = url.match(/bitbucket\.org[:/]([^/]+)\/([^/.]+)/);
|
|
2566
|
+
if (match && match[1] && match[2]) {
|
|
2567
|
+
return {
|
|
2568
|
+
workspace: match[1],
|
|
2569
|
+
repo: match[2].replace(/\.git$/, "")
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
return null;
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Get repository info from git remote
|
|
2576
|
+
*/
|
|
2577
|
+
async getRepoFromGit() {
|
|
2578
|
+
try {
|
|
2579
|
+
const { stdout } = await execAsync4("command git remote get-url origin");
|
|
2580
|
+
const remoteUrl = stdout.trim();
|
|
2581
|
+
const parsed = this.parseRemoteUrl(remoteUrl);
|
|
2582
|
+
if (!parsed) {
|
|
2583
|
+
throw new BitbucketError("Could not parse Bitbucket repository from git remote", {
|
|
2584
|
+
hint: "Ensure this is a Bitbucket repository"
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
return parsed;
|
|
2588
|
+
} catch (error) {
|
|
2589
|
+
if (error instanceof BitbucketError) {
|
|
2590
|
+
throw error;
|
|
2591
|
+
}
|
|
2592
|
+
throw new GitError("Could not get git remote URL");
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Validate that this is a Bitbucket repository and credentials work
|
|
2597
|
+
*/
|
|
2598
|
+
async validateBitbucketRepository() {
|
|
2599
|
+
await gitService.validateRepository();
|
|
2600
|
+
const { workspace, repo } = await this.getRepoFromGit();
|
|
2601
|
+
try {
|
|
2602
|
+
await this.request(`/repositories/${workspace}/${repo}`);
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
if (error instanceof BitbucketError) {
|
|
2605
|
+
throw error;
|
|
2606
|
+
}
|
|
2607
|
+
throw new BitbucketError("Could not access Bitbucket repository via API", {
|
|
2608
|
+
hint: "Check that this is a Bitbucket repository and your credentials are valid",
|
|
2609
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
/**
|
|
2614
|
+
* Get repository information
|
|
2615
|
+
*/
|
|
2616
|
+
async getRepositoryInfo() {
|
|
2617
|
+
const { workspace, repo } = await this.getRepoFromGit();
|
|
2618
|
+
const bitbucketRepo = await this.request(
|
|
2619
|
+
`/repositories/${workspace}/${repo}`
|
|
2620
|
+
);
|
|
2621
|
+
return {
|
|
2622
|
+
owner: workspace,
|
|
2623
|
+
name: bitbucketRepo.name,
|
|
2624
|
+
fullName: bitbucketRepo.full_name,
|
|
2625
|
+
url: bitbucketRepo.links.html.href
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* Get current user's account ID
|
|
2630
|
+
*/
|
|
2631
|
+
async getCurrentUserAccountId() {
|
|
2632
|
+
const user = await this.request("/user");
|
|
2633
|
+
return user.account_id;
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Get current user's username (nickname)
|
|
2637
|
+
*/
|
|
2638
|
+
async getCurrentGitHubUser() {
|
|
2639
|
+
try {
|
|
2640
|
+
const user = await this.request("/user");
|
|
2641
|
+
return user.nickname;
|
|
2642
|
+
} catch {
|
|
2643
|
+
return null;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Fetch merged pull requests with optional filtering
|
|
2648
|
+
*/
|
|
2649
|
+
async getMergedPRs(options = {}) {
|
|
2650
|
+
const { workspace, repo } = await this.getRepoFromGit();
|
|
2651
|
+
const queries = ['state="MERGED"'];
|
|
2652
|
+
if (options.days) {
|
|
2653
|
+
const since = /* @__PURE__ */ new Date();
|
|
2654
|
+
since.setDate(since.getDate() - options.days);
|
|
2655
|
+
queries.push(`updated_on>=${since.toISOString()}`);
|
|
2656
|
+
}
|
|
2657
|
+
if (options.author) {
|
|
2658
|
+
queries.push(`author.account_id="${options.author}"`);
|
|
2659
|
+
}
|
|
2660
|
+
const queryString = queries.join(" AND ");
|
|
2661
|
+
const allPRs = [];
|
|
2662
|
+
let endpoint = `/repositories/${workspace}/${repo}/pullrequests?q=${encodeURIComponent(queryString)}&pagelen=100`;
|
|
2663
|
+
while (endpoint) {
|
|
2664
|
+
const response = await this.request(endpoint);
|
|
2665
|
+
allPRs.push(...response.values);
|
|
2666
|
+
logger.debug(
|
|
2667
|
+
`Fetched ${response.values.length} PRs (total: ${allPRs.length})${response.next ? ", fetching next page..." : ""}`
|
|
2668
|
+
);
|
|
2669
|
+
if (options.limit && allPRs.length >= options.limit) {
|
|
2670
|
+
return allPRs.slice(0, options.limit);
|
|
2671
|
+
}
|
|
2672
|
+
if (response.next) {
|
|
2673
|
+
const url = new URL(response.next);
|
|
2674
|
+
endpoint = url.pathname + url.search;
|
|
2675
|
+
} else {
|
|
2676
|
+
endpoint = "";
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
return allPRs;
|
|
2680
|
+
}
|
|
2681
|
+
/**
|
|
2682
|
+
* Fetch PRs for the current authenticated user
|
|
2683
|
+
*/
|
|
2684
|
+
async getPRsByCurrentUser(options = {}) {
|
|
2685
|
+
const accountId = await this.getCurrentUserAccountId();
|
|
2686
|
+
return this.getMergedPRs({
|
|
2687
|
+
...options,
|
|
2688
|
+
author: accountId
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Transform Bitbucket PR to GitCommit format
|
|
2693
|
+
*/
|
|
2694
|
+
transformPRToCommit(pr) {
|
|
2695
|
+
let message = pr.title;
|
|
2696
|
+
if (pr.description) {
|
|
2697
|
+
const truncatedDesc = pr.description.substring(0, this.MAX_DESCRIPTION_LENGTH);
|
|
2698
|
+
message = `${pr.title}
|
|
2699
|
+
|
|
2700
|
+
${truncatedDesc}`;
|
|
2701
|
+
}
|
|
2702
|
+
return {
|
|
2703
|
+
sha: `pr-${pr.id}`,
|
|
2704
|
+
message,
|
|
2705
|
+
author: pr.author.nickname,
|
|
2706
|
+
authorEmail: "",
|
|
2707
|
+
// Not available in Bitbucket API
|
|
2708
|
+
date: pr.created_on,
|
|
2709
|
+
url: pr.links.html.href,
|
|
2710
|
+
diffStats: {
|
|
2711
|
+
filesChanged: 0,
|
|
2712
|
+
// Would require separate API call to diffstat endpoint
|
|
2713
|
+
insertions: 0,
|
|
2714
|
+
deletions: 0
|
|
2715
|
+
}
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
var bitbucketService = new BitbucketService();
|
|
2720
|
+
|
|
2721
|
+
// src/sync/bitbucket-adapter.ts
|
|
2722
|
+
var BitbucketSyncAdapter = class {
|
|
2723
|
+
name = "bitbucket";
|
|
2724
|
+
async validate() {
|
|
2725
|
+
await bitbucketService.validateBitbucketRepository();
|
|
2726
|
+
}
|
|
2727
|
+
async getRepositoryInfo() {
|
|
2728
|
+
const info = await bitbucketService.getRepositoryInfo();
|
|
2729
|
+
return {
|
|
2730
|
+
owner: info.owner,
|
|
2731
|
+
name: info.name,
|
|
2732
|
+
fullName: info.fullName,
|
|
2733
|
+
url: info.url
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
async fetchWorkItems(options) {
|
|
2737
|
+
let prs;
|
|
2738
|
+
if (options.author) {
|
|
2739
|
+
prs = await bitbucketService.getMergedPRs({
|
|
2740
|
+
days: options.days,
|
|
2741
|
+
limit: options.limit,
|
|
2742
|
+
author: options.author
|
|
2743
|
+
});
|
|
2744
|
+
} else {
|
|
2745
|
+
prs = await bitbucketService.getPRsByCurrentUser({
|
|
2746
|
+
days: options.days,
|
|
2747
|
+
limit: options.limit
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
return prs.map((pr) => bitbucketService.transformPRToCommit(pr));
|
|
2751
|
+
}
|
|
2752
|
+
async isAuthenticated() {
|
|
2753
|
+
try {
|
|
2754
|
+
await bitbucketService.validateBitbucketRepository();
|
|
2755
|
+
return true;
|
|
2756
|
+
} catch {
|
|
2757
|
+
return false;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
async getCurrentUser() {
|
|
2761
|
+
return bitbucketService.getCurrentGitHubUser();
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
var bitbucketSyncAdapter = new BitbucketSyncAdapter();
|
|
2765
|
+
|
|
2766
|
+
// src/sync/gitlab-adapter.ts
|
|
2767
|
+
init_esm_shims();
|
|
2768
|
+
|
|
2769
|
+
// src/services/gitlab.service.ts
|
|
2770
|
+
init_esm_shims();
|
|
2771
|
+
init_errors();
|
|
2772
|
+
init_logger();
|
|
2773
|
+
init_storage_service();
|
|
2774
|
+
import { exec as exec5 } from "child_process";
|
|
2775
|
+
import { promisify as promisify5 } from "util";
|
|
2776
|
+
import { URLSearchParams as URLSearchParams3 } from "url";
|
|
2777
|
+
var execAsync5 = promisify5(exec5);
|
|
2778
|
+
var GitLabService = class {
|
|
2779
|
+
DEFAULT_INSTANCE = "https://gitlab.com";
|
|
2780
|
+
MAX_DESCRIPTION_LENGTH = 5e3;
|
|
2781
|
+
/**
|
|
2782
|
+
* Get stored GitLab credentials
|
|
2783
|
+
*/
|
|
2784
|
+
async getCredentials() {
|
|
2785
|
+
const creds = await storageService.getServiceCredentials("gitlab");
|
|
2786
|
+
if (!creds || !creds.accessToken) {
|
|
2787
|
+
throw new GitLabError("Not authenticated with GitLab", {
|
|
2788
|
+
hint: "Run: bragduck auth gitlab"
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
if (creds.expiresAt && creds.expiresAt < Date.now()) {
|
|
2792
|
+
throw new GitLabError("Access token has expired", {
|
|
2793
|
+
hint: "Run: bragduck auth gitlab"
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
return {
|
|
2797
|
+
accessToken: creds.accessToken,
|
|
2798
|
+
instanceUrl: creds.instanceUrl || this.DEFAULT_INSTANCE
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Make authenticated request to GitLab API
|
|
2803
|
+
*/
|
|
2804
|
+
async request(endpoint) {
|
|
2805
|
+
const { accessToken, instanceUrl } = await this.getCredentials();
|
|
2806
|
+
const baseUrl = `${instanceUrl}/api/v4`;
|
|
2807
|
+
const url = `${baseUrl}${endpoint}`;
|
|
2808
|
+
logger.debug(`GitLab API: GET ${endpoint}`);
|
|
2809
|
+
const response = await fetch(url, {
|
|
2810
|
+
headers: {
|
|
2811
|
+
"PRIVATE-TOKEN": accessToken,
|
|
2812
|
+
Accept: "application/json"
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
if (!response.ok) {
|
|
2816
|
+
const statusText = response.statusText;
|
|
2817
|
+
const status = response.status;
|
|
2818
|
+
if (status === 401) {
|
|
2819
|
+
throw new GitLabError("Invalid or expired access token", {
|
|
2820
|
+
hint: "Run: bragduck auth gitlab",
|
|
2821
|
+
originalError: statusText
|
|
2822
|
+
});
|
|
2823
|
+
} else if (status === 403) {
|
|
2824
|
+
throw new GitLabError("Insufficient token permissions", {
|
|
2825
|
+
hint: "Token requires scopes: api, read_api, read_user",
|
|
2826
|
+
originalError: statusText
|
|
2827
|
+
});
|
|
2828
|
+
} else if (status === 404) {
|
|
2829
|
+
throw new GitLabError("Resource not found", {
|
|
2830
|
+
hint: "Check repository URL and token permissions",
|
|
2831
|
+
originalError: statusText
|
|
2832
|
+
});
|
|
2833
|
+
} else if (status === 429) {
|
|
2834
|
+
throw new GitLabError("Rate limit exceeded", {
|
|
2835
|
+
hint: "Wait a moment and try again",
|
|
2836
|
+
originalError: statusText
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
throw new GitLabError(`API request failed: ${statusText}`, {
|
|
2840
|
+
originalError: statusText
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
return response.json();
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Extract namespace and project from git remote URL
|
|
2847
|
+
*/
|
|
2848
|
+
parseRemoteUrl(url) {
|
|
2849
|
+
const match = url.match(/[:/]([\w-]+)\/([\w-]+?)(?:\.git)?$/);
|
|
2850
|
+
if (match && match[1] && match[2]) {
|
|
2851
|
+
return {
|
|
2852
|
+
namespace: match[1],
|
|
2853
|
+
project: match[2]
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
return null;
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Get project path from git remote
|
|
2860
|
+
*/
|
|
2861
|
+
async getProjectFromGit() {
|
|
2862
|
+
try {
|
|
2863
|
+
const { stdout } = await execAsync5("git remote get-url origin");
|
|
2864
|
+
const remoteUrl = stdout.trim();
|
|
2865
|
+
const parsed = this.parseRemoteUrl(remoteUrl);
|
|
2866
|
+
if (!parsed) {
|
|
2867
|
+
throw new GitLabError("Could not parse GitLab project from remote URL", {
|
|
2868
|
+
url: remoteUrl
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
return {
|
|
2872
|
+
...parsed,
|
|
2873
|
+
projectPath: `${parsed.namespace}/${parsed.project}`
|
|
2874
|
+
};
|
|
2875
|
+
} catch (error) {
|
|
2876
|
+
if (error instanceof GitLabError) {
|
|
2877
|
+
throw error;
|
|
2878
|
+
}
|
|
2879
|
+
throw new GitError("Could not get git remote URL");
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Validate that this is a GitLab repository and credentials work
|
|
2884
|
+
*/
|
|
2885
|
+
async validateGitLabRepository() {
|
|
2886
|
+
await gitService.validateRepository();
|
|
2887
|
+
const { projectPath } = await this.getProjectFromGit();
|
|
2888
|
+
const encodedPath = encodeURIComponent(projectPath);
|
|
2889
|
+
try {
|
|
2890
|
+
await this.request(`/projects/${encodedPath}`);
|
|
2891
|
+
} catch (error) {
|
|
2892
|
+
if (error instanceof GitLabError) {
|
|
2893
|
+
throw error;
|
|
2894
|
+
}
|
|
2895
|
+
throw new GitLabError("Could not access GitLab project via API", {
|
|
2896
|
+
hint: "Check that this is a GitLab repository and your token is valid",
|
|
2897
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Get repository information
|
|
2903
|
+
*/
|
|
2904
|
+
async getRepositoryInfo() {
|
|
2905
|
+
const { projectPath } = await this.getProjectFromGit();
|
|
2906
|
+
const encodedPath = encodeURIComponent(projectPath);
|
|
2907
|
+
const project = await this.request(`/projects/${encodedPath}`);
|
|
2908
|
+
const [owner, name] = projectPath.split("/");
|
|
2909
|
+
return {
|
|
2910
|
+
owner: owner || "",
|
|
2911
|
+
name: name || "",
|
|
2912
|
+
fullName: project.path_with_namespace,
|
|
2913
|
+
url: project.web_url
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* Fetch merged MRs with pagination
|
|
2918
|
+
*/
|
|
2919
|
+
async getMergedMRs(options = {}) {
|
|
2920
|
+
const { projectPath } = await this.getProjectFromGit();
|
|
2921
|
+
const encodedPath = encodeURIComponent(projectPath);
|
|
2922
|
+
const params = new URLSearchParams3({
|
|
2923
|
+
state: "merged",
|
|
2924
|
+
order_by: "updated_at",
|
|
2925
|
+
sort: "desc",
|
|
2926
|
+
per_page: "100"
|
|
2927
|
+
});
|
|
2928
|
+
if (options.days) {
|
|
2929
|
+
const since = /* @__PURE__ */ new Date();
|
|
2930
|
+
since.setDate(since.getDate() - options.days);
|
|
2931
|
+
params.append("updated_after", since.toISOString());
|
|
2932
|
+
}
|
|
2933
|
+
if (options.author) {
|
|
2934
|
+
params.append("author_username", options.author);
|
|
2935
|
+
}
|
|
2936
|
+
const allMRs = [];
|
|
2937
|
+
let page = 1;
|
|
2938
|
+
while (true) {
|
|
2939
|
+
params.set("page", page.toString());
|
|
2940
|
+
const endpoint = `/projects/${encodedPath}/merge_requests?${params}`;
|
|
2941
|
+
const mrs = await this.request(endpoint);
|
|
2942
|
+
allMRs.push(...mrs);
|
|
2943
|
+
logger.debug(
|
|
2944
|
+
`Fetched ${mrs.length} MRs (total: ${allMRs.length})${mrs.length === 100 ? ", fetching next page..." : ""}`
|
|
2945
|
+
);
|
|
2946
|
+
if (options.limit && allMRs.length >= options.limit) {
|
|
2947
|
+
return allMRs.slice(0, options.limit);
|
|
2948
|
+
}
|
|
2949
|
+
if (mrs.length < 100) {
|
|
2950
|
+
break;
|
|
2951
|
+
}
|
|
2952
|
+
page++;
|
|
2953
|
+
}
|
|
2954
|
+
return allMRs;
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Get MRs by current user
|
|
2958
|
+
*/
|
|
2959
|
+
async getMRsByCurrentUser(options = {}) {
|
|
2960
|
+
const username = await this.getCurrentGitLabUser();
|
|
2961
|
+
if (!username) {
|
|
2962
|
+
throw new GitLabError("Could not get current user username");
|
|
2963
|
+
}
|
|
2964
|
+
return this.getMergedMRs({
|
|
2965
|
+
...options,
|
|
2966
|
+
author: username
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Get current authenticated user
|
|
2971
|
+
*/
|
|
2972
|
+
async getCurrentGitLabUser() {
|
|
2973
|
+
try {
|
|
2974
|
+
const user = await this.request("/user");
|
|
2975
|
+
return user.username;
|
|
2976
|
+
} catch {
|
|
2977
|
+
return null;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
/**
|
|
2981
|
+
* Transform GitLab MR to GitCommit format
|
|
2982
|
+
*/
|
|
2983
|
+
transformMRToCommit(mr) {
|
|
2984
|
+
const message = this.formatMessage(mr.title, mr.description);
|
|
2985
|
+
return {
|
|
2986
|
+
sha: `mr-${mr.iid}`,
|
|
2987
|
+
// Use iid (project-scoped)
|
|
2988
|
+
message,
|
|
2989
|
+
author: mr.author.username,
|
|
2990
|
+
authorEmail: "",
|
|
2991
|
+
// Not available in MR response
|
|
2992
|
+
date: mr.created_at,
|
|
2993
|
+
url: mr.web_url,
|
|
2994
|
+
diffStats: {
|
|
2995
|
+
filesChanged: mr.diff_stats?.file_count || 0,
|
|
2996
|
+
insertions: mr.diff_stats?.additions || 0,
|
|
2997
|
+
deletions: mr.diff_stats?.deletions || 0
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Format MR message with title and description
|
|
3003
|
+
*/
|
|
3004
|
+
formatMessage(title, description) {
|
|
3005
|
+
if (!description) return title;
|
|
3006
|
+
const truncated = description.substring(0, this.MAX_DESCRIPTION_LENGTH);
|
|
3007
|
+
return `${title}
|
|
3008
|
+
|
|
3009
|
+
${truncated}`;
|
|
3010
|
+
}
|
|
3011
|
+
};
|
|
3012
|
+
var gitlabService = new GitLabService();
|
|
3013
|
+
|
|
3014
|
+
// src/sync/gitlab-adapter.ts
|
|
3015
|
+
var GitLabSyncAdapter = class {
|
|
3016
|
+
name = "gitlab";
|
|
3017
|
+
async validate() {
|
|
3018
|
+
await gitlabService.validateGitLabRepository();
|
|
3019
|
+
}
|
|
3020
|
+
async getRepositoryInfo() {
|
|
3021
|
+
const info = await gitlabService.getRepositoryInfo();
|
|
3022
|
+
return {
|
|
3023
|
+
owner: info.owner,
|
|
3024
|
+
name: info.name,
|
|
3025
|
+
fullName: info.fullName,
|
|
3026
|
+
url: info.url
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
async fetchWorkItems(options) {
|
|
3030
|
+
let mrs;
|
|
3031
|
+
if (options.author) {
|
|
3032
|
+
mrs = await gitlabService.getMergedMRs({
|
|
3033
|
+
days: options.days,
|
|
3034
|
+
limit: options.limit,
|
|
3035
|
+
author: options.author
|
|
3036
|
+
});
|
|
3037
|
+
} else {
|
|
3038
|
+
mrs = await gitlabService.getMRsByCurrentUser({
|
|
3039
|
+
days: options.days,
|
|
3040
|
+
limit: options.limit
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
return mrs.map((mr) => gitlabService.transformMRToCommit(mr));
|
|
3044
|
+
}
|
|
3045
|
+
async isAuthenticated() {
|
|
3046
|
+
try {
|
|
3047
|
+
await gitlabService.validateGitLabRepository();
|
|
3048
|
+
return true;
|
|
3049
|
+
} catch {
|
|
3050
|
+
return false;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
async getCurrentUser() {
|
|
3054
|
+
return gitlabService.getCurrentGitLabUser();
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
var gitlabSyncAdapter = new GitLabSyncAdapter();
|
|
3058
|
+
|
|
2321
3059
|
// src/sync/adapter-factory.ts
|
|
2322
3060
|
var AdapterFactory = class {
|
|
2323
3061
|
/**
|
|
@@ -2327,10 +3065,12 @@ var AdapterFactory = class {
|
|
|
2327
3065
|
switch (source) {
|
|
2328
3066
|
case "github":
|
|
2329
3067
|
return githubSyncAdapter;
|
|
2330
|
-
case "gitlab":
|
|
2331
3068
|
case "bitbucket":
|
|
2332
3069
|
case "atlassian":
|
|
2333
|
-
|
|
3070
|
+
return bitbucketSyncAdapter;
|
|
3071
|
+
// Bitbucket Cloud and Server use same adapter
|
|
3072
|
+
case "gitlab":
|
|
3073
|
+
return gitlabSyncAdapter;
|
|
2334
3074
|
default:
|
|
2335
3075
|
throw new Error(`Unknown source type: ${source}`);
|
|
2336
3076
|
}
|
|
@@ -2339,7 +3079,7 @@ var AdapterFactory = class {
|
|
|
2339
3079
|
* Check if adapter is available for source
|
|
2340
3080
|
*/
|
|
2341
3081
|
static isSupported(source) {
|
|
2342
|
-
return source === "github";
|
|
3082
|
+
return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab";
|
|
2343
3083
|
}
|
|
2344
3084
|
};
|
|
2345
3085
|
|
|
@@ -2479,7 +3219,7 @@ init_logger();
|
|
|
2479
3219
|
|
|
2480
3220
|
// src/ui/prompts.ts
|
|
2481
3221
|
init_esm_shims();
|
|
2482
|
-
import { checkbox, confirm, input, select, editor } from "@inquirer/prompts";
|
|
3222
|
+
import { checkbox, confirm, input as input2, select, editor } from "@inquirer/prompts";
|
|
2483
3223
|
import boxen4 from "boxen";
|
|
2484
3224
|
|
|
2485
3225
|
// src/ui/formatters.ts
|
|
@@ -2641,7 +3381,7 @@ async function promptDaysToScan(defaultDays = 30) {
|
|
|
2641
3381
|
default: "30"
|
|
2642
3382
|
});
|
|
2643
3383
|
if (selected === "custom") {
|
|
2644
|
-
const customDays = await
|
|
3384
|
+
const customDays = await input2({
|
|
2645
3385
|
message: "Enter number of days:",
|
|
2646
3386
|
default: defaultDays.toString(),
|
|
2647
3387
|
validate: (value) => {
|
|
@@ -2777,7 +3517,7 @@ ${theme.label("PR Link")} ${colors.link(prUrl)}`;
|
|
|
2777
3517
|
let editedBrag = { ...currentBrag };
|
|
2778
3518
|
if (action === "edit-title" || action === "edit-both") {
|
|
2779
3519
|
console.log("");
|
|
2780
|
-
const newTitle = await
|
|
3520
|
+
const newTitle = await input2({
|
|
2781
3521
|
message: "Enter new title:",
|
|
2782
3522
|
default: currentBrag.refined_title
|
|
2783
3523
|
});
|
|
@@ -3784,7 +4524,7 @@ var packageJsonPath = join7(__dirname6, "../../package.json");
|
|
|
3784
4524
|
var packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
|
|
3785
4525
|
var program = new Command();
|
|
3786
4526
|
program.name("bragduck").description("CLI tool for managing developer achievements and brags\nAliases: bd, duck, brag").version(packageJson.version, "-v, --version", "Display version number").helpOption("-h, --help", "Display help information").option("--skip-version-check", "Skip automatic version check on startup").option("--debug", "Enable debug mode (shows detailed logs)");
|
|
3787
|
-
program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status)").action(async (subcommand) => {
|
|
4527
|
+
program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status, bitbucket, gitlab)").action(async (subcommand) => {
|
|
3788
4528
|
try {
|
|
3789
4529
|
await authCommand(subcommand);
|
|
3790
4530
|
} catch (error) {
|