@bragduck/cli 2.28.0 → 2.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/bragduck.js +475 -377
- package/dist/bin/bragduck.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
package/dist/bin/bragduck.js
CHANGED
|
@@ -22,7 +22,7 @@ var init_esm_shims = __esm({
|
|
|
22
22
|
import { config } from "dotenv";
|
|
23
23
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
24
24
|
import { dirname, join } from "path";
|
|
25
|
-
var __filename, __dirname, APP_NAME, CONFIG_KEYS, DEFAULT_CONFIG, OAUTH_CONFIG, API_ENDPOINTS, ENCRYPTION_CONFIG, STORAGE_PATHS, HTTP_STATUS, CONFIG_FILES;
|
|
25
|
+
var __filename, __dirname, APP_NAME, CONFIG_KEYS, DEFAULT_CONFIG, OAUTH_CONFIG, ATLASSIAN_OAUTH_CONFIG, API_ENDPOINTS, ENCRYPTION_CONFIG, STORAGE_PATHS, HTTP_STATUS, CONFIG_FILES;
|
|
26
26
|
var init_constants = __esm({
|
|
27
27
|
"src/constants.ts"() {
|
|
28
28
|
"use strict";
|
|
@@ -57,11 +57,23 @@ var init_constants = __esm({
|
|
|
57
57
|
MIN_PORT: 8e3,
|
|
58
58
|
MAX_PORT: 9e3
|
|
59
59
|
};
|
|
60
|
+
ATLASSIAN_OAUTH_CONFIG = {
|
|
61
|
+
CLIENT_ID: "kJlsd66DLTnENE8p7Ru2JwqZg1Sie4yZ",
|
|
62
|
+
AUTH_URL: "https://auth.atlassian.com/authorize",
|
|
63
|
+
AUDIENCE: "api.atlassian.com",
|
|
64
|
+
SCOPES: "read:jira-work read:jira-user read:page:confluence read:confluence-user offline_access read:me",
|
|
65
|
+
ACCESSIBLE_RESOURCES_URL: "https://api.atlassian.com/oauth/token/accessible-resources",
|
|
66
|
+
API_GATEWAY_URL: "https://api.atlassian.com"
|
|
67
|
+
};
|
|
60
68
|
API_ENDPOINTS = {
|
|
61
69
|
AUTH: {
|
|
62
70
|
INITIATE: "/v1/auth/cli/initiate",
|
|
63
71
|
TOKEN: "/v1/auth/cli/token"
|
|
64
72
|
},
|
|
73
|
+
ATLASSIAN: {
|
|
74
|
+
TOKEN: "/v1/auth/atlassian/token",
|
|
75
|
+
REFRESH: "/v1/auth/atlassian/refresh"
|
|
76
|
+
},
|
|
65
77
|
BRAGS: {
|
|
66
78
|
CREATE: "/v1/brags",
|
|
67
79
|
LIST: "/v1/brags",
|
|
@@ -157,7 +169,7 @@ var init_env_loader = __esm({
|
|
|
157
169
|
});
|
|
158
170
|
|
|
159
171
|
// src/utils/errors.ts
|
|
160
|
-
var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError;
|
|
172
|
+
var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError, AtlassianError;
|
|
161
173
|
var init_errors = __esm({
|
|
162
174
|
"src/utils/errors.ts"() {
|
|
163
175
|
"use strict";
|
|
@@ -248,6 +260,12 @@ var init_errors = __esm({
|
|
|
248
260
|
this.name = "ConfluenceError";
|
|
249
261
|
}
|
|
250
262
|
};
|
|
263
|
+
AtlassianError = class extends BragduckError {
|
|
264
|
+
constructor(message, details) {
|
|
265
|
+
super(message, "ATLASSIAN_ERROR", details);
|
|
266
|
+
this.name = "AtlassianError";
|
|
267
|
+
}
|
|
268
|
+
};
|
|
251
269
|
}
|
|
252
270
|
});
|
|
253
271
|
|
|
@@ -1241,15 +1259,15 @@ __export(version_exports, {
|
|
|
1241
1259
|
getCurrentVersion: () => getCurrentVersion,
|
|
1242
1260
|
version: () => version
|
|
1243
1261
|
});
|
|
1244
|
-
import { readFileSync as
|
|
1245
|
-
import { fileURLToPath as
|
|
1246
|
-
import { dirname as
|
|
1262
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1263
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1264
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
1247
1265
|
import chalk4 from "chalk";
|
|
1248
1266
|
import boxen2 from "boxen";
|
|
1249
1267
|
function getCurrentVersion() {
|
|
1250
1268
|
try {
|
|
1251
|
-
const packageJsonPath2 =
|
|
1252
|
-
const packageJson2 = JSON.parse(
|
|
1269
|
+
const packageJsonPath2 = join6(__dirname5, "../../package.json");
|
|
1270
|
+
const packageJson2 = JSON.parse(readFileSync4(packageJsonPath2, "utf-8"));
|
|
1253
1271
|
return packageJson2.version;
|
|
1254
1272
|
} catch {
|
|
1255
1273
|
logger.debug("Failed to read package.json version");
|
|
@@ -1331,7 +1349,7 @@ function formatVersion(includePrefix = true) {
|
|
|
1331
1349
|
const prefix = includePrefix ? "v" : "";
|
|
1332
1350
|
return `${prefix}${version}`;
|
|
1333
1351
|
}
|
|
1334
|
-
var
|
|
1352
|
+
var __filename5, __dirname5, version;
|
|
1335
1353
|
var init_version = __esm({
|
|
1336
1354
|
"src/utils/version.ts"() {
|
|
1337
1355
|
"use strict";
|
|
@@ -1339,22 +1357,22 @@ var init_version = __esm({
|
|
|
1339
1357
|
init_api_service();
|
|
1340
1358
|
init_storage_service();
|
|
1341
1359
|
init_logger();
|
|
1342
|
-
|
|
1343
|
-
|
|
1360
|
+
__filename5 = fileURLToPath5(import.meta.url);
|
|
1361
|
+
__dirname5 = dirname4(__filename5);
|
|
1344
1362
|
version = getCurrentVersion();
|
|
1345
1363
|
}
|
|
1346
1364
|
});
|
|
1347
1365
|
|
|
1348
1366
|
// src/services/api.service.ts
|
|
1349
|
-
import { ofetch as
|
|
1350
|
-
import { readFileSync as
|
|
1351
|
-
import { fileURLToPath as
|
|
1352
|
-
import { URLSearchParams as
|
|
1353
|
-
import { dirname as
|
|
1367
|
+
import { ofetch as ofetch3 } from "ofetch";
|
|
1368
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1369
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
1370
|
+
import { URLSearchParams as URLSearchParams3 } from "url";
|
|
1371
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
1354
1372
|
function getCliVersion() {
|
|
1355
1373
|
try {
|
|
1356
|
-
const packageJsonPath2 =
|
|
1357
|
-
const packageJson2 = JSON.parse(
|
|
1374
|
+
const packageJsonPath2 = join7(__dirname6, "../../package.json");
|
|
1375
|
+
const packageJson2 = JSON.parse(readFileSync5(packageJsonPath2, "utf-8"));
|
|
1358
1376
|
return packageJson2.version;
|
|
1359
1377
|
} catch {
|
|
1360
1378
|
logger.debug("Failed to read package.json version");
|
|
@@ -1366,7 +1384,7 @@ function getPlatformInfo() {
|
|
|
1366
1384
|
const arch = process.arch;
|
|
1367
1385
|
return `${platform}-${arch}`;
|
|
1368
1386
|
}
|
|
1369
|
-
var
|
|
1387
|
+
var __filename6, __dirname6, ApiService, apiService;
|
|
1370
1388
|
var init_api_service = __esm({
|
|
1371
1389
|
"src/services/api.service.ts"() {
|
|
1372
1390
|
"use strict";
|
|
@@ -1375,14 +1393,14 @@ var init_api_service = __esm({
|
|
|
1375
1393
|
init_constants();
|
|
1376
1394
|
init_errors();
|
|
1377
1395
|
init_logger();
|
|
1378
|
-
|
|
1379
|
-
|
|
1396
|
+
__filename6 = fileURLToPath6(import.meta.url);
|
|
1397
|
+
__dirname6 = dirname5(__filename6);
|
|
1380
1398
|
ApiService = class {
|
|
1381
1399
|
baseURL;
|
|
1382
1400
|
client;
|
|
1383
1401
|
constructor() {
|
|
1384
1402
|
this.baseURL = process.env.API_BASE_URL || "https://api.bragduck.com";
|
|
1385
|
-
this.client =
|
|
1403
|
+
this.client = ofetch3.create({
|
|
1386
1404
|
baseURL: this.baseURL,
|
|
1387
1405
|
// Request interceptor
|
|
1388
1406
|
onRequest: async ({ request, options }) => {
|
|
@@ -1546,7 +1564,7 @@ var init_api_service = __esm({
|
|
|
1546
1564
|
const { limit = 50, offset = 0, tags, search } = params;
|
|
1547
1565
|
logger.debug(`Listing brags: limit=${limit}, offset=${offset}`);
|
|
1548
1566
|
try {
|
|
1549
|
-
const queryParams = new
|
|
1567
|
+
const queryParams = new URLSearchParams3({
|
|
1550
1568
|
limit: limit.toString(),
|
|
1551
1569
|
offset: offset.toString()
|
|
1552
1570
|
});
|
|
@@ -1643,7 +1661,7 @@ var init_api_service = __esm({
|
|
|
1643
1661
|
*/
|
|
1644
1662
|
setBaseURL(url) {
|
|
1645
1663
|
this.baseURL = url;
|
|
1646
|
-
this.client =
|
|
1664
|
+
this.client = ofetch3.create({
|
|
1647
1665
|
baseURL: url
|
|
1648
1666
|
});
|
|
1649
1667
|
}
|
|
@@ -1661,18 +1679,267 @@ var init_api_service = __esm({
|
|
|
1661
1679
|
// src/cli.ts
|
|
1662
1680
|
init_esm_shims();
|
|
1663
1681
|
import { Command } from "commander";
|
|
1664
|
-
import { readFileSync as
|
|
1665
|
-
import { fileURLToPath as
|
|
1666
|
-
import { dirname as
|
|
1682
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1683
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
1684
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
1667
1685
|
|
|
1668
1686
|
// src/commands/auth.ts
|
|
1669
1687
|
init_esm_shims();
|
|
1670
1688
|
init_auth_service();
|
|
1671
|
-
init_storage_service();
|
|
1672
1689
|
import boxen from "boxen";
|
|
1673
1690
|
import chalk3 from "chalk";
|
|
1674
1691
|
import { input } from "@inquirer/prompts";
|
|
1675
1692
|
|
|
1693
|
+
// src/services/atlassian-auth.service.ts
|
|
1694
|
+
init_esm_shims();
|
|
1695
|
+
init_constants();
|
|
1696
|
+
init_storage_service();
|
|
1697
|
+
init_oauth_server();
|
|
1698
|
+
init_browser();
|
|
1699
|
+
init_errors();
|
|
1700
|
+
init_logger();
|
|
1701
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1702
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1703
|
+
import { fileURLToPath as fileURLToPath4, URLSearchParams as URLSearchParams2 } from "url";
|
|
1704
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
1705
|
+
import { ofetch as ofetch2 } from "ofetch";
|
|
1706
|
+
import { select } from "@inquirer/prompts";
|
|
1707
|
+
var __filename4 = fileURLToPath4(import.meta.url);
|
|
1708
|
+
var __dirname4 = dirname3(__filename4);
|
|
1709
|
+
function getUserAgent2() {
|
|
1710
|
+
try {
|
|
1711
|
+
const packageJsonPath2 = join4(__dirname4, "../../package.json");
|
|
1712
|
+
const packageJson2 = JSON.parse(readFileSync3(packageJsonPath2, "utf-8"));
|
|
1713
|
+
const version2 = packageJson2.version;
|
|
1714
|
+
const platform = process.platform;
|
|
1715
|
+
const arch = process.arch;
|
|
1716
|
+
return `BragDuck-CLI/${version2} (${platform}-${arch})`;
|
|
1717
|
+
} catch {
|
|
1718
|
+
logger.debug("Failed to read package.json version");
|
|
1719
|
+
return "BragDuck-CLI/2.0.0";
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
var AtlassianAuthService = class {
|
|
1723
|
+
apiBaseUrl;
|
|
1724
|
+
refreshPromise = null;
|
|
1725
|
+
constructor() {
|
|
1726
|
+
this.apiBaseUrl = process.env.API_BASE_URL || "https://api.bragduck.com";
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Generate a random state string for CSRF protection
|
|
1730
|
+
*/
|
|
1731
|
+
generateState() {
|
|
1732
|
+
return randomBytes3(32).toString("hex");
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Build the Atlassian OAuth authorization URL
|
|
1736
|
+
*/
|
|
1737
|
+
buildAuthUrl(state, callbackUrl) {
|
|
1738
|
+
const params = new URLSearchParams2({
|
|
1739
|
+
audience: ATLASSIAN_OAUTH_CONFIG.AUDIENCE,
|
|
1740
|
+
client_id: ATLASSIAN_OAUTH_CONFIG.CLIENT_ID,
|
|
1741
|
+
scope: ATLASSIAN_OAUTH_CONFIG.SCOPES,
|
|
1742
|
+
redirect_uri: callbackUrl,
|
|
1743
|
+
state,
|
|
1744
|
+
response_type: "code",
|
|
1745
|
+
prompt: "consent"
|
|
1746
|
+
});
|
|
1747
|
+
return `${ATLASSIAN_OAUTH_CONFIG.AUTH_URL}?${params.toString()}`;
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Exchange authorization code for tokens via BragDuck backend
|
|
1751
|
+
*/
|
|
1752
|
+
async exchangeCodeForToken(code, redirectUri) {
|
|
1753
|
+
try {
|
|
1754
|
+
logger.debug("Exchanging Atlassian authorization code for token via backend");
|
|
1755
|
+
const response = await ofetch2(
|
|
1756
|
+
`${this.apiBaseUrl}${API_ENDPOINTS.ATLASSIAN.TOKEN}`,
|
|
1757
|
+
{
|
|
1758
|
+
method: "POST",
|
|
1759
|
+
body: {
|
|
1760
|
+
code,
|
|
1761
|
+
redirect_uri: redirectUri
|
|
1762
|
+
},
|
|
1763
|
+
headers: {
|
|
1764
|
+
"Content-Type": "application/json",
|
|
1765
|
+
"User-Agent": getUserAgent2()
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
);
|
|
1769
|
+
logger.debug("Atlassian token exchange successful");
|
|
1770
|
+
return response;
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
logger.debug(`Atlassian token exchange failed: ${error.message}`);
|
|
1773
|
+
throw new AtlassianError("Failed to exchange Atlassian authorization code", {
|
|
1774
|
+
originalError: error.message
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Fetch accessible Atlassian Cloud resources (sites) using the access token
|
|
1780
|
+
*/
|
|
1781
|
+
async fetchAccessibleResources(accessToken) {
|
|
1782
|
+
try {
|
|
1783
|
+
logger.debug("Fetching accessible Atlassian resources");
|
|
1784
|
+
const resources = await ofetch2(
|
|
1785
|
+
ATLASSIAN_OAUTH_CONFIG.ACCESSIBLE_RESOURCES_URL,
|
|
1786
|
+
{
|
|
1787
|
+
headers: {
|
|
1788
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1789
|
+
Accept: "application/json"
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
);
|
|
1793
|
+
logger.debug(`Found ${resources.length} accessible resource(s)`);
|
|
1794
|
+
return resources;
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
logger.debug(`Failed to fetch accessible resources: ${error.message}`);
|
|
1797
|
+
throw new AtlassianError("Failed to fetch Atlassian Cloud sites", {
|
|
1798
|
+
originalError: error.message
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Let user pick a site if multiple accessible resources exist
|
|
1804
|
+
*/
|
|
1805
|
+
async selectSite(resources) {
|
|
1806
|
+
if (resources.length === 0) {
|
|
1807
|
+
throw new AtlassianError(
|
|
1808
|
+
"No accessible Atlassian Cloud sites found. Make sure your account has access to at least one site."
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
if (resources.length === 1) {
|
|
1812
|
+
const site = resources[0];
|
|
1813
|
+
logger.debug(`Auto-selecting single site: ${site.name}`);
|
|
1814
|
+
return site;
|
|
1815
|
+
}
|
|
1816
|
+
const selectedId = await select({
|
|
1817
|
+
message: "Select your Atlassian Cloud site:",
|
|
1818
|
+
choices: resources.map((r) => ({
|
|
1819
|
+
name: `${r.name} (${r.url})`,
|
|
1820
|
+
value: r.id
|
|
1821
|
+
}))
|
|
1822
|
+
});
|
|
1823
|
+
return resources.find((r) => r.id === selectedId);
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Full OAuth login flow:
|
|
1827
|
+
* 1. Open browser to Atlassian consent screen
|
|
1828
|
+
* 2. Wait for callback with authorization code
|
|
1829
|
+
* 3. Exchange code for tokens via BragDuck backend
|
|
1830
|
+
* 4. Fetch accessible resources and pick site
|
|
1831
|
+
* 5. Store credentials for jira, confluence, and atlassian services
|
|
1832
|
+
*/
|
|
1833
|
+
async login() {
|
|
1834
|
+
logger.debug("Starting Atlassian OAuth login flow");
|
|
1835
|
+
const state = this.generateState();
|
|
1836
|
+
const callbackUrl = await getCallbackUrl();
|
|
1837
|
+
storageService.setOAuthState({
|
|
1838
|
+
state,
|
|
1839
|
+
createdAt: Date.now()
|
|
1840
|
+
});
|
|
1841
|
+
logger.debug(`OAuth state: ${state}`);
|
|
1842
|
+
logger.debug(`Callback URL: ${callbackUrl}`);
|
|
1843
|
+
const authUrl = this.buildAuthUrl(state, callbackUrl);
|
|
1844
|
+
logger.debug(`Authorization URL: ${authUrl}`);
|
|
1845
|
+
const serverPromise = startOAuthCallbackServer(state);
|
|
1846
|
+
try {
|
|
1847
|
+
await openBrowser(authUrl);
|
|
1848
|
+
} catch {
|
|
1849
|
+
logger.warning("Could not open browser automatically");
|
|
1850
|
+
logger.info("Please open this URL in your browser:");
|
|
1851
|
+
logger.log(authUrl);
|
|
1852
|
+
}
|
|
1853
|
+
let callbackResult;
|
|
1854
|
+
try {
|
|
1855
|
+
callbackResult = await serverPromise;
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
storageService.deleteOAuthState();
|
|
1858
|
+
throw error;
|
|
1859
|
+
}
|
|
1860
|
+
storageService.deleteOAuthState();
|
|
1861
|
+
const tokenResponse = await this.exchangeCodeForToken(callbackResult.code, callbackUrl);
|
|
1862
|
+
const resources = await this.fetchAccessibleResources(tokenResponse.access_token);
|
|
1863
|
+
const site = await this.selectSite(resources);
|
|
1864
|
+
const expiresAt = tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : void 0;
|
|
1865
|
+
const credentials = {
|
|
1866
|
+
accessToken: tokenResponse.access_token,
|
|
1867
|
+
refreshToken: tokenResponse.refresh_token,
|
|
1868
|
+
expiresAt,
|
|
1869
|
+
instanceUrl: site.url,
|
|
1870
|
+
// Real Cloud URL for browse links
|
|
1871
|
+
cloudId: site.id,
|
|
1872
|
+
authMethod: "oauth"
|
|
1873
|
+
};
|
|
1874
|
+
await storageService.setServiceCredentials("jira", credentials);
|
|
1875
|
+
await storageService.setServiceCredentials("confluence", credentials);
|
|
1876
|
+
await storageService.setServiceCredentials("atlassian", credentials);
|
|
1877
|
+
logger.debug("Atlassian OAuth login successful");
|
|
1878
|
+
return { siteName: site.name, siteUrl: site.url };
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Refresh Atlassian OAuth tokens via BragDuck backend.
|
|
1882
|
+
* Uses a singleton promise to prevent concurrent refresh race conditions
|
|
1883
|
+
* (Atlassian uses rotating refresh tokens - each refresh invalidates the previous one).
|
|
1884
|
+
*/
|
|
1885
|
+
async refreshToken() {
|
|
1886
|
+
if (this.refreshPromise) {
|
|
1887
|
+
return this.refreshPromise;
|
|
1888
|
+
}
|
|
1889
|
+
this.refreshPromise = this.doRefreshToken();
|
|
1890
|
+
try {
|
|
1891
|
+
await this.refreshPromise;
|
|
1892
|
+
} finally {
|
|
1893
|
+
this.refreshPromise = null;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
async doRefreshToken() {
|
|
1897
|
+
logger.debug("Refreshing Atlassian OAuth token");
|
|
1898
|
+
const creds = await storageService.getServiceCredentials("jira");
|
|
1899
|
+
if (!creds?.refreshToken) {
|
|
1900
|
+
throw new AtlassianError("No Atlassian refresh token available", {
|
|
1901
|
+
hint: "Run: bragduck auth atlassian"
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
try {
|
|
1905
|
+
const response = await ofetch2(
|
|
1906
|
+
`${this.apiBaseUrl}${API_ENDPOINTS.ATLASSIAN.REFRESH}`,
|
|
1907
|
+
{
|
|
1908
|
+
method: "POST",
|
|
1909
|
+
body: {
|
|
1910
|
+
refresh_token: creds.refreshToken
|
|
1911
|
+
},
|
|
1912
|
+
headers: {
|
|
1913
|
+
"Content-Type": "application/json",
|
|
1914
|
+
"User-Agent": getUserAgent2()
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
);
|
|
1918
|
+
const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1e3 : void 0;
|
|
1919
|
+
const updatedCreds = {
|
|
1920
|
+
...creds,
|
|
1921
|
+
accessToken: response.access_token,
|
|
1922
|
+
refreshToken: response.refresh_token,
|
|
1923
|
+
expiresAt
|
|
1924
|
+
};
|
|
1925
|
+
await storageService.setServiceCredentials("jira", updatedCreds);
|
|
1926
|
+
await storageService.setServiceCredentials("confluence", updatedCreds);
|
|
1927
|
+
await storageService.setServiceCredentials("atlassian", updatedCreds);
|
|
1928
|
+
logger.debug("Atlassian token refresh successful");
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
logger.debug(`Atlassian token refresh failed: ${error.message}`);
|
|
1931
|
+
throw new AtlassianError(
|
|
1932
|
+
'Atlassian token refresh failed. Please run "bragduck auth atlassian" to re-authenticate.',
|
|
1933
|
+
{ originalError: error.message }
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
var atlassianAuthService = new AtlassianAuthService();
|
|
1939
|
+
|
|
1940
|
+
// src/commands/auth.ts
|
|
1941
|
+
init_storage_service();
|
|
1942
|
+
|
|
1676
1943
|
// src/services/github.service.ts
|
|
1677
1944
|
init_esm_shims();
|
|
1678
1945
|
init_errors();
|
|
@@ -1688,9 +1955,9 @@ import simpleGit from "simple-git";
|
|
|
1688
1955
|
init_esm_shims();
|
|
1689
1956
|
init_errors();
|
|
1690
1957
|
import { existsSync as existsSync2 } from "fs";
|
|
1691
|
-
import { join as
|
|
1958
|
+
import { join as join5 } from "path";
|
|
1692
1959
|
function validateGitRepository(path4) {
|
|
1693
|
-
const gitDir =
|
|
1960
|
+
const gitDir = join5(path4, ".git");
|
|
1694
1961
|
if (!existsSync2(gitDir)) {
|
|
1695
1962
|
throw new GitError(
|
|
1696
1963
|
"Not a git repository. Please run this command from within a git repository.",
|
|
@@ -2404,7 +2671,14 @@ async function authStatus() {
|
|
|
2404
2671
|
}
|
|
2405
2672
|
for (const service of services) {
|
|
2406
2673
|
if (service !== "bragduck") {
|
|
2407
|
-
|
|
2674
|
+
const creds = await storageService.getServiceCredentials(service);
|
|
2675
|
+
let methodLabel = "";
|
|
2676
|
+
if (creds?.authMethod === "oauth") {
|
|
2677
|
+
methodLabel = " (Cloud - OAuth)";
|
|
2678
|
+
} else if (creds?.authMethod === "basic") {
|
|
2679
|
+
methodLabel = " (Server - API Token)";
|
|
2680
|
+
}
|
|
2681
|
+
logger.info(`${colors.success("\u2713")} ${service}: Authenticated${methodLabel}`);
|
|
2408
2682
|
}
|
|
2409
2683
|
}
|
|
2410
2684
|
logger.log("");
|
|
@@ -2556,68 +2830,21 @@ User: ${user.name} (@${user.username})`,
|
|
|
2556
2830
|
}
|
|
2557
2831
|
async function authAtlassian() {
|
|
2558
2832
|
logger.log("");
|
|
2559
|
-
logger.
|
|
2560
|
-
boxen(
|
|
2561
|
-
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",
|
|
2562
|
-
boxStyles.info
|
|
2563
|
-
)
|
|
2564
|
-
);
|
|
2833
|
+
logger.info("Opening browser for Atlassian Cloud authentication...");
|
|
2565
2834
|
logger.log("");
|
|
2566
2835
|
try {
|
|
2567
|
-
const
|
|
2568
|
-
message: "Atlassian instance URL (e.g., company.atlassian.net):",
|
|
2569
|
-
validate: (value) => value.length > 0 ? true : "Instance URL cannot be empty"
|
|
2570
|
-
});
|
|
2571
|
-
const email = await input({
|
|
2572
|
-
message: "Atlassian account email:",
|
|
2573
|
-
validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
|
|
2574
|
-
});
|
|
2575
|
-
const apiToken = await input({
|
|
2576
|
-
message: "API Token:",
|
|
2577
|
-
validate: (value) => value.length > 0 ? true : "API token cannot be empty"
|
|
2578
|
-
});
|
|
2579
|
-
const testInstanceUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
|
|
2580
|
-
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
2581
|
-
const response = await fetch(`${testInstanceUrl}/rest/api/2/myself`, {
|
|
2582
|
-
headers: { Authorization: `Basic ${auth}` }
|
|
2583
|
-
});
|
|
2584
|
-
if (!response.ok) {
|
|
2585
|
-
logger.log("");
|
|
2586
|
-
logger.log(
|
|
2587
|
-
boxen(
|
|
2588
|
-
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)",
|
|
2589
|
-
boxStyles.error
|
|
2590
|
-
)
|
|
2591
|
-
);
|
|
2592
|
-
logger.log("");
|
|
2593
|
-
process.exit(1);
|
|
2594
|
-
}
|
|
2595
|
-
const user = await response.json();
|
|
2596
|
-
const credentials = {
|
|
2597
|
-
accessToken: apiToken,
|
|
2598
|
-
username: email,
|
|
2599
|
-
instanceUrl: instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`
|
|
2600
|
-
};
|
|
2601
|
-
await storageService.setServiceCredentials("jira", credentials);
|
|
2602
|
-
await storageService.setServiceCredentials("confluence", credentials);
|
|
2603
|
-
await storageService.setServiceCredentials("bitbucket", {
|
|
2604
|
-
...credentials,
|
|
2605
|
-
instanceUrl: "https://api.bitbucket.org"
|
|
2606
|
-
// Bitbucket uses different API base
|
|
2607
|
-
});
|
|
2836
|
+
const result = await atlassianAuthService.login();
|
|
2608
2837
|
logger.log("");
|
|
2609
2838
|
logger.log(
|
|
2610
2839
|
boxen(
|
|
2611
|
-
theme.success("\u2713 Successfully authenticated with Atlassian") + `
|
|
2840
|
+
theme.success("\u2713 Successfully authenticated with Atlassian Cloud") + `
|
|
2612
2841
|
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
Email: ${user.emailAddress}
|
|
2842
|
+
Site: ${result.siteName}
|
|
2843
|
+
URL: ${result.siteUrl}
|
|
2616
2844
|
|
|
2617
2845
|
Services configured:
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
\u2022 Bitbucket`,
|
|
2846
|
+
- Jira (OAuth)
|
|
2847
|
+
- Confluence (OAuth)`,
|
|
2621
2848
|
boxStyles.success
|
|
2622
2849
|
)
|
|
2623
2850
|
);
|
|
@@ -2650,7 +2877,7 @@ var CancelPromptError = class extends Error {
|
|
|
2650
2877
|
init_api_service();
|
|
2651
2878
|
init_storage_service();
|
|
2652
2879
|
init_auth_service();
|
|
2653
|
-
import { select as
|
|
2880
|
+
import { select as select4 } from "@inquirer/prompts";
|
|
2654
2881
|
import boxen6 from "boxen";
|
|
2655
2882
|
|
|
2656
2883
|
// src/utils/source-detector.ts
|
|
@@ -2659,7 +2886,7 @@ init_errors();
|
|
|
2659
2886
|
init_storage_service();
|
|
2660
2887
|
import { exec as exec3 } from "child_process";
|
|
2661
2888
|
import { promisify as promisify3 } from "util";
|
|
2662
|
-
import { select } from "@inquirer/prompts";
|
|
2889
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
2663
2890
|
var execAsync3 = promisify3(exec3);
|
|
2664
2891
|
var SourceDetector = class {
|
|
2665
2892
|
/**
|
|
@@ -2715,7 +2942,7 @@ var SourceDetector = class {
|
|
|
2715
2942
|
description: source.remoteUrl
|
|
2716
2943
|
};
|
|
2717
2944
|
});
|
|
2718
|
-
return await
|
|
2945
|
+
return await select2({
|
|
2719
2946
|
message: "Multiple sources detected. Which do you want to sync?",
|
|
2720
2947
|
choices
|
|
2721
2948
|
});
|
|
@@ -3231,7 +3458,7 @@ init_logger();
|
|
|
3231
3458
|
init_storage_service();
|
|
3232
3459
|
import { exec as exec5 } from "child_process";
|
|
3233
3460
|
import { promisify as promisify5 } from "util";
|
|
3234
|
-
import { URLSearchParams as
|
|
3461
|
+
import { URLSearchParams as URLSearchParams4 } from "url";
|
|
3235
3462
|
var execAsync5 = promisify5(exec5);
|
|
3236
3463
|
var GitLabService = class {
|
|
3237
3464
|
DEFAULT_INSTANCE = "https://gitlab.com";
|
|
@@ -3377,7 +3604,7 @@ var GitLabService = class {
|
|
|
3377
3604
|
async getMergedMRs(options = {}) {
|
|
3378
3605
|
const { projectPath } = await this.getProjectFromGit();
|
|
3379
3606
|
const encodedPath = encodeURIComponent(projectPath);
|
|
3380
|
-
const params = new
|
|
3607
|
+
const params = new URLSearchParams4({
|
|
3381
3608
|
state: "merged",
|
|
3382
3609
|
order_by: "updated_at",
|
|
3383
3610
|
sort: "desc",
|
|
@@ -3543,41 +3770,76 @@ init_esm_shims();
|
|
|
3543
3770
|
init_errors();
|
|
3544
3771
|
init_logger();
|
|
3545
3772
|
init_storage_service();
|
|
3773
|
+
init_constants();
|
|
3546
3774
|
var JiraService = class {
|
|
3547
3775
|
MAX_DESCRIPTION_LENGTH = 5e3;
|
|
3548
3776
|
/**
|
|
3549
|
-
* Get stored Jira credentials
|
|
3777
|
+
* Get stored Jira credentials, auto-refreshing OAuth tokens if expired
|
|
3550
3778
|
*/
|
|
3551
3779
|
async getCredentials() {
|
|
3552
3780
|
const creds = await storageService.getServiceCredentials("jira");
|
|
3553
|
-
if (!creds || !creds.
|
|
3781
|
+
if (!creds || !creds.accessToken) {
|
|
3554
3782
|
throw new JiraError("Not authenticated with Jira", {
|
|
3555
3783
|
hint: "Run: bragduck auth atlassian"
|
|
3556
3784
|
});
|
|
3557
3785
|
}
|
|
3558
|
-
if (creds.
|
|
3786
|
+
if (creds.authMethod !== "oauth" && (!creds.username || !creds.instanceUrl)) {
|
|
3787
|
+
throw new JiraError("Not authenticated with Jira", {
|
|
3788
|
+
hint: "Run: bragduck auth atlassian"
|
|
3789
|
+
});
|
|
3790
|
+
}
|
|
3791
|
+
if (creds.authMethod === "oauth" && !creds.cloudId) {
|
|
3792
|
+
throw new JiraError("Missing Atlassian Cloud ID", {
|
|
3793
|
+
hint: "Run: bragduck auth atlassian"
|
|
3794
|
+
});
|
|
3795
|
+
}
|
|
3796
|
+
if (creds.authMethod === "oauth" && creds.expiresAt && creds.expiresAt < Date.now()) {
|
|
3797
|
+
if (creds.refreshToken) {
|
|
3798
|
+
logger.debug("Jira OAuth token expired, refreshing...");
|
|
3799
|
+
await atlassianAuthService.refreshToken();
|
|
3800
|
+
const refreshed = await storageService.getServiceCredentials("jira");
|
|
3801
|
+
if (!refreshed?.accessToken) {
|
|
3802
|
+
throw new JiraError("Token refresh failed", {
|
|
3803
|
+
hint: "Run: bragduck auth atlassian"
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
return refreshed;
|
|
3807
|
+
}
|
|
3808
|
+
throw new JiraError("OAuth token has expired and no refresh token available", {
|
|
3809
|
+
hint: "Run: bragduck auth atlassian"
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3812
|
+
if (creds.authMethod !== "oauth" && creds.expiresAt && creds.expiresAt < Date.now()) {
|
|
3559
3813
|
throw new JiraError("API token has expired", {
|
|
3560
3814
|
hint: "Run: bragduck auth atlassian"
|
|
3561
3815
|
});
|
|
3562
3816
|
}
|
|
3563
|
-
return
|
|
3564
|
-
email: creds.username,
|
|
3565
|
-
apiToken: creds.accessToken,
|
|
3566
|
-
instanceUrl: creds.instanceUrl
|
|
3567
|
-
};
|
|
3817
|
+
return creds;
|
|
3568
3818
|
}
|
|
3569
3819
|
/**
|
|
3570
|
-
* Make authenticated request to Jira API
|
|
3820
|
+
* Make authenticated request to Jira API.
|
|
3821
|
+
* Uses OAuth Bearer + API gateway for Cloud, Basic Auth for Server/DC.
|
|
3822
|
+
* Retries once on 401 for OAuth (auto-refresh).
|
|
3571
3823
|
*/
|
|
3572
3824
|
async request(endpoint, method = "GET", body) {
|
|
3573
|
-
const
|
|
3574
|
-
const
|
|
3575
|
-
|
|
3825
|
+
const creds = await this.getCredentials();
|
|
3826
|
+
const isOAuth = creds.authMethod === "oauth";
|
|
3827
|
+
let baseUrl;
|
|
3828
|
+
let authHeader;
|
|
3829
|
+
if (isOAuth) {
|
|
3830
|
+
baseUrl = `${ATLASSIAN_OAUTH_CONFIG.API_GATEWAY_URL}/ex/jira/${creds.cloudId}`;
|
|
3831
|
+
authHeader = `Bearer ${creds.accessToken}`;
|
|
3832
|
+
} else {
|
|
3833
|
+
const instanceUrl = creds.instanceUrl;
|
|
3834
|
+
baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
|
|
3835
|
+
const auth = Buffer.from(`${creds.username}:${creds.accessToken}`).toString("base64");
|
|
3836
|
+
authHeader = `Basic ${auth}`;
|
|
3837
|
+
}
|
|
3576
3838
|
logger.debug(`Jira API: ${method} ${endpoint}`);
|
|
3577
3839
|
const options = {
|
|
3578
3840
|
method,
|
|
3579
3841
|
headers: {
|
|
3580
|
-
Authorization:
|
|
3842
|
+
Authorization: authHeader,
|
|
3581
3843
|
"Content-Type": "application/json",
|
|
3582
3844
|
Accept: "application/json"
|
|
3583
3845
|
}
|
|
@@ -3585,7 +3847,17 @@ var JiraService = class {
|
|
|
3585
3847
|
if (body) {
|
|
3586
3848
|
options.body = JSON.stringify(body);
|
|
3587
3849
|
}
|
|
3588
|
-
|
|
3850
|
+
let response = await fetch(`${baseUrl}${endpoint}`, options);
|
|
3851
|
+
if (response.status === 401 && isOAuth && creds.refreshToken) {
|
|
3852
|
+
logger.debug("Jira OAuth 401, attempting token refresh and retry");
|
|
3853
|
+
await atlassianAuthService.refreshToken();
|
|
3854
|
+
const refreshedCreds = await storageService.getServiceCredentials("jira");
|
|
3855
|
+
if (refreshedCreds?.accessToken) {
|
|
3856
|
+
options.headers.Authorization = `Bearer ${refreshedCreds.accessToken}`;
|
|
3857
|
+
const newBaseUrl = `${ATLASSIAN_OAUTH_CONFIG.API_GATEWAY_URL}/ex/jira/${refreshedCreds.cloudId}`;
|
|
3858
|
+
response = await fetch(`${newBaseUrl}${endpoint}`, options);
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3589
3861
|
if (!response.ok) {
|
|
3590
3862
|
const statusText = response.statusText;
|
|
3591
3863
|
const status = response.status;
|
|
@@ -3653,33 +3925,6 @@ var JiraService = class {
|
|
|
3653
3925
|
return null;
|
|
3654
3926
|
}
|
|
3655
3927
|
}
|
|
3656
|
-
/**
|
|
3657
|
-
* Check if a user object matches the given identifier (accountId, email, or username)
|
|
3658
|
-
*/
|
|
3659
|
-
isMatchingUser(candidate, userIdentifier) {
|
|
3660
|
-
if (!candidate) return false;
|
|
3661
|
-
return candidate.email === userIdentifier || candidate.emailAddress === userIdentifier || candidate.accountId === userIdentifier || candidate.username === userIdentifier || candidate.name === userIdentifier;
|
|
3662
|
-
}
|
|
3663
|
-
/**
|
|
3664
|
-
* Filter issues to only those where the user made contributions within the date range.
|
|
3665
|
-
* Excludes issues where the user's only involvement is a static role (creator/assignee)
|
|
3666
|
-
* with a date outside the scan period.
|
|
3667
|
-
*/
|
|
3668
|
-
filterIssuesByUserContribution(issues, userIdentifier, sinceDate) {
|
|
3669
|
-
const results = [];
|
|
3670
|
-
for (const issue of issues) {
|
|
3671
|
-
const userChanges = (issue.changelog?.histories || []).filter(
|
|
3672
|
-
(h) => this.isMatchingUser(h.author, userIdentifier) && new Date(h.created) >= sinceDate
|
|
3673
|
-
);
|
|
3674
|
-
const isCreatorInRange = this.isMatchingUser(issue.fields.creator, userIdentifier) && new Date(issue.fields.created) >= sinceDate;
|
|
3675
|
-
if (userChanges.length > 0 || isCreatorInRange) {
|
|
3676
|
-
results.push({ issue, userChanges });
|
|
3677
|
-
} else {
|
|
3678
|
-
logger.debug(`Excluding issue ${issue.key} - no user contributions in date range`);
|
|
3679
|
-
}
|
|
3680
|
-
}
|
|
3681
|
-
return results;
|
|
3682
|
-
}
|
|
3683
3928
|
/**
|
|
3684
3929
|
* Build JQL query from options
|
|
3685
3930
|
*/
|
|
@@ -3737,7 +3982,7 @@ var JiraService = class {
|
|
|
3737
3982
|
}
|
|
3738
3983
|
const isAssigned = issue.fields.assignee?.emailAddress === userEmail;
|
|
3739
3984
|
const isResolved = issue.fields.resolutiondate !== null && issue.fields.resolutiondate !== void 0;
|
|
3740
|
-
const userEdits = issue.changelog?.histories?.filter((history) => history.author
|
|
3985
|
+
const userEdits = issue.changelog?.histories?.filter((history) => history.author.emailAddress === userEmail) || [];
|
|
3741
3986
|
const hasEdits = userEdits.length > 0;
|
|
3742
3987
|
if (isAssigned && isResolved) {
|
|
3743
3988
|
return {
|
|
@@ -3772,77 +4017,6 @@ var JiraService = class {
|
|
|
3772
4017
|
};
|
|
3773
4018
|
return Math.ceil(baseComplexity * multipliers[contributionType]);
|
|
3774
4019
|
}
|
|
3775
|
-
/**
|
|
3776
|
-
* Summarize the user's specific changes from changelog entries into human-readable lines.
|
|
3777
|
-
* This enriches the brag message so the AI refinement can generate a more specific brag.
|
|
3778
|
-
*/
|
|
3779
|
-
summarizeUserChanges(userChanges) {
|
|
3780
|
-
const MAX_LINES = 10;
|
|
3781
|
-
const allItems = [];
|
|
3782
|
-
for (const entry of userChanges) {
|
|
3783
|
-
for (const item of entry.items) {
|
|
3784
|
-
allItems.push(item);
|
|
3785
|
-
}
|
|
3786
|
-
}
|
|
3787
|
-
if (allItems.length === 0) return "";
|
|
3788
|
-
const latestByField = /* @__PURE__ */ new Map();
|
|
3789
|
-
for (const item of allItems) {
|
|
3790
|
-
latestByField.set(item.field, { fromString: item.fromString, toString: item.toString });
|
|
3791
|
-
}
|
|
3792
|
-
const lines = [];
|
|
3793
|
-
for (const [field, change] of latestByField) {
|
|
3794
|
-
if (lines.length >= MAX_LINES) break;
|
|
3795
|
-
const from = change.fromString || "";
|
|
3796
|
-
const to = change.toString || "";
|
|
3797
|
-
switch (field.toLowerCase()) {
|
|
3798
|
-
case "status":
|
|
3799
|
-
lines.push(from ? `Moved status from '${from}' to '${to}'` : `Set status to '${to}'`);
|
|
3800
|
-
break;
|
|
3801
|
-
case "resolution":
|
|
3802
|
-
lines.push(to ? `Resolved as '${to}'` : "Reopened issue");
|
|
3803
|
-
break;
|
|
3804
|
-
case "assignee":
|
|
3805
|
-
lines.push(to ? `Assigned to ${to}` : "Unassigned");
|
|
3806
|
-
break;
|
|
3807
|
-
case "priority":
|
|
3808
|
-
lines.push(
|
|
3809
|
-
from ? `Changed priority from '${from}' to '${to}'` : `Set priority to '${to}'`
|
|
3810
|
-
);
|
|
3811
|
-
break;
|
|
3812
|
-
case "summary":
|
|
3813
|
-
lines.push("Updated issue title");
|
|
3814
|
-
break;
|
|
3815
|
-
case "description":
|
|
3816
|
-
lines.push("Updated description");
|
|
3817
|
-
break;
|
|
3818
|
-
case "comment":
|
|
3819
|
-
lines.push("Added comment");
|
|
3820
|
-
break;
|
|
3821
|
-
case "labels":
|
|
3822
|
-
lines.push(to ? `Updated labels: ${to}` : "Removed labels");
|
|
3823
|
-
break;
|
|
3824
|
-
case "fix version":
|
|
3825
|
-
case "fixversions":
|
|
3826
|
-
lines.push(to ? `Set fix version: ${to}` : "Removed fix version");
|
|
3827
|
-
break;
|
|
3828
|
-
case "sprint":
|
|
3829
|
-
lines.push(to ? `Moved to sprint: ${to}` : "Removed from sprint");
|
|
3830
|
-
break;
|
|
3831
|
-
case "story points":
|
|
3832
|
-
case "story point estimate":
|
|
3833
|
-
lines.push(`Set story points to ${to}`);
|
|
3834
|
-
break;
|
|
3835
|
-
default:
|
|
3836
|
-
if (to) {
|
|
3837
|
-
lines.push(`Updated ${field}`);
|
|
3838
|
-
}
|
|
3839
|
-
break;
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
if (lines.length === 0) return "";
|
|
3843
|
-
return `User changes:
|
|
3844
|
-
${lines.map((l) => `- ${l}`).join("\n")}`;
|
|
3845
|
-
}
|
|
3846
4020
|
/**
|
|
3847
4021
|
* Fetch issues with optional filtering
|
|
3848
4022
|
*/
|
|
@@ -3880,7 +4054,7 @@ ${lines.map((l) => `- ${l}`).join("\n")}`;
|
|
|
3880
4054
|
);
|
|
3881
4055
|
break;
|
|
3882
4056
|
}
|
|
3883
|
-
const endpoint = `/rest/api/3/search/jql?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=${fields.join(",")}
|
|
4057
|
+
const endpoint = `/rest/api/3/search/jql?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=${fields.join(",")}`;
|
|
3884
4058
|
try {
|
|
3885
4059
|
const response = await this.request(endpoint);
|
|
3886
4060
|
if (response.issues.length === 0) {
|
|
@@ -3906,7 +4080,14 @@ ${lines.map((l) => `- ${l}`).join("\n")}`;
|
|
|
3906
4080
|
break;
|
|
3907
4081
|
}
|
|
3908
4082
|
if (options.limit && allIssues.length >= options.limit) {
|
|
3909
|
-
|
|
4083
|
+
const email2 = await this.getCurrentUser();
|
|
4084
|
+
const limitedIssues = allIssues.slice(0, options.limit);
|
|
4085
|
+
const commits2 = [];
|
|
4086
|
+
for (const issue of limitedIssues) {
|
|
4087
|
+
const commit = await this.transformIssueToCommit(issue, void 0, email2 || void 0);
|
|
4088
|
+
commits2.push(commit);
|
|
4089
|
+
}
|
|
4090
|
+
return commits2;
|
|
3910
4091
|
}
|
|
3911
4092
|
startAt += maxResults;
|
|
3912
4093
|
} catch (error) {
|
|
@@ -3930,23 +4111,9 @@ ${lines.map((l) => `- ${l}`).join("\n")}`;
|
|
|
3930
4111
|
throw error;
|
|
3931
4112
|
}
|
|
3932
4113
|
}
|
|
3933
|
-
const issuesToProcess = options.limit ? allIssues.slice(0, options.limit) : allIssues;
|
|
3934
4114
|
const email = await this.getCurrentUser();
|
|
3935
|
-
const sinceDate = options.days ? new Date(Date.now() - options.days * 24 * 60 * 60 * 1e3) : void 0;
|
|
3936
|
-
if (sinceDate && email) {
|
|
3937
|
-
const filtered = this.filterIssuesByUserContribution(issuesToProcess, email, sinceDate);
|
|
3938
|
-
logger.debug(
|
|
3939
|
-
`Date-scoped filtering: ${issuesToProcess.length} issues -> ${filtered.length} with user contributions in range`
|
|
3940
|
-
);
|
|
3941
|
-
const commits2 = [];
|
|
3942
|
-
for (const { issue, userChanges } of filtered) {
|
|
3943
|
-
const commit = await this.transformIssueToCommit(issue, void 0, email, userChanges);
|
|
3944
|
-
commits2.push(commit);
|
|
3945
|
-
}
|
|
3946
|
-
return commits2;
|
|
3947
|
-
}
|
|
3948
4115
|
const commits = [];
|
|
3949
|
-
for (const issue of
|
|
4116
|
+
for (const issue of allIssues) {
|
|
3950
4117
|
const commit = await this.transformIssueToCommit(issue, void 0, email || void 0);
|
|
3951
4118
|
commits.push(commit);
|
|
3952
4119
|
}
|
|
@@ -3968,7 +4135,7 @@ ${lines.map((l) => `- ${l}`).join("\n")}`;
|
|
|
3968
4135
|
/**
|
|
3969
4136
|
* Transform Jira issue to GitCommit format with contribution-specific data
|
|
3970
4137
|
*/
|
|
3971
|
-
async transformIssueToCommit(issue, instanceUrl, userEmail
|
|
4138
|
+
async transformIssueToCommit(issue, instanceUrl, userEmail) {
|
|
3972
4139
|
let contribution = null;
|
|
3973
4140
|
if (userEmail) {
|
|
3974
4141
|
contribution = await this.determineJiraContributionType(issue, userEmail);
|
|
@@ -4005,21 +4172,11 @@ ${contribution.details}`;
|
|
|
4005
4172
|
${truncatedDesc}`;
|
|
4006
4173
|
}
|
|
4007
4174
|
}
|
|
4008
|
-
if (userChanges && userChanges.length > 0) {
|
|
4009
|
-
const changeSummary = this.summarizeUserChanges(userChanges);
|
|
4010
|
-
if (changeSummary) {
|
|
4011
|
-
message += `
|
|
4012
|
-
|
|
4013
|
-
${changeSummary}`;
|
|
4014
|
-
}
|
|
4015
|
-
}
|
|
4016
4175
|
let date;
|
|
4017
4176
|
if (contribution?.type === "created" || contribution?.type === "reported") {
|
|
4018
4177
|
date = issue.fields.created;
|
|
4019
4178
|
} else if (contribution?.type === "assigned-resolved" && issue.fields.resolutiondate) {
|
|
4020
4179
|
date = issue.fields.resolutiondate;
|
|
4021
|
-
} else if (userChanges && userChanges.length > 0) {
|
|
4022
|
-
date = userChanges[userChanges.length - 1].created;
|
|
4023
4180
|
} else {
|
|
4024
4181
|
date = issue.fields.updated;
|
|
4025
4182
|
}
|
|
@@ -4106,40 +4263,75 @@ init_esm_shims();
|
|
|
4106
4263
|
init_errors();
|
|
4107
4264
|
init_logger();
|
|
4108
4265
|
init_storage_service();
|
|
4266
|
+
init_constants();
|
|
4109
4267
|
var ConfluenceService = class {
|
|
4110
4268
|
/**
|
|
4111
|
-
* Get stored Confluence credentials
|
|
4269
|
+
* Get stored Confluence credentials, auto-refreshing OAuth tokens if expired
|
|
4112
4270
|
*/
|
|
4113
4271
|
async getCredentials() {
|
|
4114
4272
|
const creds = await storageService.getServiceCredentials("confluence");
|
|
4115
|
-
if (!creds || !creds.
|
|
4273
|
+
if (!creds || !creds.accessToken) {
|
|
4116
4274
|
throw new ConfluenceError("Not authenticated with Confluence", {
|
|
4117
4275
|
hint: "Run: bragduck auth atlassian"
|
|
4118
4276
|
});
|
|
4119
4277
|
}
|
|
4120
|
-
if (creds.
|
|
4278
|
+
if (creds.authMethod !== "oauth" && (!creds.username || !creds.instanceUrl)) {
|
|
4279
|
+
throw new ConfluenceError("Not authenticated with Confluence", {
|
|
4280
|
+
hint: "Run: bragduck auth atlassian"
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4283
|
+
if (creds.authMethod === "oauth" && !creds.cloudId) {
|
|
4284
|
+
throw new ConfluenceError("Missing Atlassian Cloud ID", {
|
|
4285
|
+
hint: "Run: bragduck auth atlassian"
|
|
4286
|
+
});
|
|
4287
|
+
}
|
|
4288
|
+
if (creds.authMethod === "oauth" && creds.expiresAt && creds.expiresAt < Date.now()) {
|
|
4289
|
+
if (creds.refreshToken) {
|
|
4290
|
+
logger.debug("Confluence OAuth token expired, refreshing...");
|
|
4291
|
+
await atlassianAuthService.refreshToken();
|
|
4292
|
+
const refreshed = await storageService.getServiceCredentials("confluence");
|
|
4293
|
+
if (!refreshed?.accessToken) {
|
|
4294
|
+
throw new ConfluenceError("Token refresh failed", {
|
|
4295
|
+
hint: "Run: bragduck auth atlassian"
|
|
4296
|
+
});
|
|
4297
|
+
}
|
|
4298
|
+
return refreshed;
|
|
4299
|
+
}
|
|
4300
|
+
throw new ConfluenceError("OAuth token has expired and no refresh token available", {
|
|
4301
|
+
hint: "Run: bragduck auth atlassian"
|
|
4302
|
+
});
|
|
4303
|
+
}
|
|
4304
|
+
if (creds.authMethod !== "oauth" && creds.expiresAt && creds.expiresAt < Date.now()) {
|
|
4121
4305
|
throw new ConfluenceError("API token has expired", {
|
|
4122
4306
|
hint: "Run: bragduck auth atlassian"
|
|
4123
4307
|
});
|
|
4124
4308
|
}
|
|
4125
|
-
return
|
|
4126
|
-
email: creds.username,
|
|
4127
|
-
apiToken: creds.accessToken,
|
|
4128
|
-
instanceUrl: creds.instanceUrl
|
|
4129
|
-
};
|
|
4309
|
+
return creds;
|
|
4130
4310
|
}
|
|
4131
4311
|
/**
|
|
4132
|
-
* Make authenticated request to Confluence API
|
|
4312
|
+
* Make authenticated request to Confluence API.
|
|
4313
|
+
* Uses OAuth Bearer + API gateway for Cloud, Basic Auth for Server/DC.
|
|
4314
|
+
* Retries once on 401 for OAuth (auto-refresh).
|
|
4133
4315
|
*/
|
|
4134
4316
|
async request(endpoint, method = "GET", body) {
|
|
4135
|
-
const
|
|
4136
|
-
const
|
|
4137
|
-
|
|
4317
|
+
const creds = await this.getCredentials();
|
|
4318
|
+
const isOAuth = creds.authMethod === "oauth";
|
|
4319
|
+
let baseUrl;
|
|
4320
|
+
let authHeader;
|
|
4321
|
+
if (isOAuth) {
|
|
4322
|
+
baseUrl = `${ATLASSIAN_OAUTH_CONFIG.API_GATEWAY_URL}/ex/confluence/${creds.cloudId}`;
|
|
4323
|
+
authHeader = `Bearer ${creds.accessToken}`;
|
|
4324
|
+
} else {
|
|
4325
|
+
const instanceUrl = creds.instanceUrl;
|
|
4326
|
+
baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
|
|
4327
|
+
const auth = Buffer.from(`${creds.username}:${creds.accessToken}`).toString("base64");
|
|
4328
|
+
authHeader = `Basic ${auth}`;
|
|
4329
|
+
}
|
|
4138
4330
|
logger.debug(`Confluence API: ${method} ${endpoint}`);
|
|
4139
4331
|
const options = {
|
|
4140
4332
|
method,
|
|
4141
4333
|
headers: {
|
|
4142
|
-
Authorization:
|
|
4334
|
+
Authorization: authHeader,
|
|
4143
4335
|
"Content-Type": "application/json",
|
|
4144
4336
|
Accept: "application/json"
|
|
4145
4337
|
}
|
|
@@ -4147,7 +4339,17 @@ var ConfluenceService = class {
|
|
|
4147
4339
|
if (body) {
|
|
4148
4340
|
options.body = JSON.stringify(body);
|
|
4149
4341
|
}
|
|
4150
|
-
|
|
4342
|
+
let response = await fetch(`${baseUrl}${endpoint}`, options);
|
|
4343
|
+
if (response.status === 401 && isOAuth && creds.refreshToken) {
|
|
4344
|
+
logger.debug("Confluence OAuth 401, attempting token refresh and retry");
|
|
4345
|
+
await atlassianAuthService.refreshToken();
|
|
4346
|
+
const refreshedCreds = await storageService.getServiceCredentials("confluence");
|
|
4347
|
+
if (refreshedCreds?.accessToken) {
|
|
4348
|
+
options.headers.Authorization = `Bearer ${refreshedCreds.accessToken}`;
|
|
4349
|
+
const newBaseUrl = `${ATLASSIAN_OAUTH_CONFIG.API_GATEWAY_URL}/ex/confluence/${refreshedCreds.cloudId}`;
|
|
4350
|
+
response = await fetch(`${newBaseUrl}${endpoint}`, options);
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4151
4353
|
if (!response.ok) {
|
|
4152
4354
|
const statusText = response.statusText;
|
|
4153
4355
|
const status = response.status;
|
|
@@ -4209,60 +4411,6 @@ var ConfluenceService = class {
|
|
|
4209
4411
|
return null;
|
|
4210
4412
|
}
|
|
4211
4413
|
}
|
|
4212
|
-
/**
|
|
4213
|
-
* Check if a user object matches the given identifier (accountId, email, or username)
|
|
4214
|
-
*/
|
|
4215
|
-
isMatchingUser(candidate, userIdentifier) {
|
|
4216
|
-
if (!candidate) return false;
|
|
4217
|
-
return candidate.email === userIdentifier || candidate.emailAddress === userIdentifier || candidate.accountId === userIdentifier || candidate.username === userIdentifier || candidate.name === userIdentifier;
|
|
4218
|
-
}
|
|
4219
|
-
/**
|
|
4220
|
-
* Fetch full version history for a page
|
|
4221
|
-
*/
|
|
4222
|
-
async getPageVersionHistory(pageId) {
|
|
4223
|
-
const allVersions = [];
|
|
4224
|
-
let start = 0;
|
|
4225
|
-
const limit = 50;
|
|
4226
|
-
while (true) {
|
|
4227
|
-
const response = await this.request(
|
|
4228
|
-
`/wiki/rest/api/content/${pageId}/version?start=${start}&limit=${limit}`
|
|
4229
|
-
);
|
|
4230
|
-
allVersions.push(...response.results);
|
|
4231
|
-
if (response.size < limit) break;
|
|
4232
|
-
start += limit;
|
|
4233
|
-
}
|
|
4234
|
-
return allVersions;
|
|
4235
|
-
}
|
|
4236
|
-
/**
|
|
4237
|
-
* Filter pages to only those where the user made contributions within the date range.
|
|
4238
|
-
* Fetches version history per page and checks if the user has versions in range.
|
|
4239
|
-
*/
|
|
4240
|
-
async filterPagesByUserContribution(pages, userIdentifier, sinceDate) {
|
|
4241
|
-
const results = [];
|
|
4242
|
-
for (let i = 0; i < pages.length; i++) {
|
|
4243
|
-
const page = pages[i];
|
|
4244
|
-
if (i > 0) {
|
|
4245
|
-
await new Promise((resolve) => globalThis.setTimeout(resolve, 100));
|
|
4246
|
-
}
|
|
4247
|
-
try {
|
|
4248
|
-
const versions = await this.getPageVersionHistory(page.id);
|
|
4249
|
-
const userVersions = versions.filter(
|
|
4250
|
-
(v) => this.isMatchingUser(v.by, userIdentifier) && new Date(v.when) >= sinceDate
|
|
4251
|
-
);
|
|
4252
|
-
const userCommentsInRange = page.children?.comment?.results?.filter(
|
|
4253
|
-
(comment) => this.isMatchingUser(comment.version?.by || {}, userIdentifier) && new Date(comment.version?.when || 0) >= sinceDate
|
|
4254
|
-
) || [];
|
|
4255
|
-
if (userVersions.length > 0 || userCommentsInRange.length > 0) {
|
|
4256
|
-
results.push({ page, userVersions });
|
|
4257
|
-
} else {
|
|
4258
|
-
logger.debug(`Excluding page "${page.title}" - no user contributions in date range`);
|
|
4259
|
-
}
|
|
4260
|
-
} catch (error) {
|
|
4261
|
-
logger.debug(`Skipping version history for page ${page.id}: ${error}`);
|
|
4262
|
-
}
|
|
4263
|
-
}
|
|
4264
|
-
return results;
|
|
4265
|
-
}
|
|
4266
4414
|
/**
|
|
4267
4415
|
* Build CQL query from options
|
|
4268
4416
|
* Returns empty string if no filters need CQL (will use simple endpoint instead)
|
|
@@ -4298,31 +4446,28 @@ var ConfluenceService = class {
|
|
|
4298
4446
|
* Determine the type of contribution the current user made to a page
|
|
4299
4447
|
* Returns: 'created' | 'edited' | 'commented'
|
|
4300
4448
|
*/
|
|
4301
|
-
async determineContributionType(page, userEmail
|
|
4302
|
-
|
|
4303
|
-
if (createdByUser) {
|
|
4449
|
+
async determineContributionType(page, userEmail) {
|
|
4450
|
+
if (page.history?.createdBy?.email === userEmail) {
|
|
4304
4451
|
return {
|
|
4305
4452
|
type: "created",
|
|
4306
4453
|
details: `Created page with ${page.version?.number || 1} version${(page.version?.number || 1) > 1 ? "s" : ""}`
|
|
4307
4454
|
};
|
|
4308
4455
|
}
|
|
4309
|
-
const hasEdits =
|
|
4456
|
+
const hasEdits = page.version?.by?.email === userEmail && (page.version?.number || 0) > 1;
|
|
4310
4457
|
const userComments = page.children?.comment?.results?.filter(
|
|
4311
|
-
(comment) => comment.version?.by
|
|
4458
|
+
(comment) => comment.version?.by?.email === userEmail
|
|
4312
4459
|
) || [];
|
|
4313
4460
|
const hasComments = userComments.length > 0;
|
|
4314
4461
|
if (hasEdits && hasComments) {
|
|
4315
|
-
const editCount = userVersions?.length || 1;
|
|
4316
4462
|
return {
|
|
4317
4463
|
type: "edited",
|
|
4318
|
-
details: `Edited page (${
|
|
4464
|
+
details: `Edited page (v${page.version?.number || 1}) and added ${userComments.length} comment${userComments.length > 1 ? "s" : ""}`
|
|
4319
4465
|
};
|
|
4320
4466
|
}
|
|
4321
4467
|
if (hasEdits) {
|
|
4322
|
-
const editCount = userVersions?.length || 1;
|
|
4323
4468
|
return {
|
|
4324
4469
|
type: "edited",
|
|
4325
|
-
details: `Edited page
|
|
4470
|
+
details: `Edited page to version ${page.version?.number || 1}`
|
|
4326
4471
|
};
|
|
4327
4472
|
}
|
|
4328
4473
|
if (hasComments) {
|
|
@@ -4350,31 +4495,6 @@ var ConfluenceService = class {
|
|
|
4350
4495
|
};
|
|
4351
4496
|
return Math.ceil(baseSize * multipliers[contributionType]);
|
|
4352
4497
|
}
|
|
4353
|
-
/**
|
|
4354
|
-
* Summarize the user's version edits into human-readable lines.
|
|
4355
|
-
* This enriches the brag message so the AI refinement can generate a more specific brag.
|
|
4356
|
-
*/
|
|
4357
|
-
summarizeUserVersions(userVersions) {
|
|
4358
|
-
const MAX_ENTRIES = 5;
|
|
4359
|
-
const versionsWithMessages = userVersions.filter((v) => v.message && v.message.trim());
|
|
4360
|
-
if (versionsWithMessages.length > 0) {
|
|
4361
|
-
const lines = versionsWithMessages.slice(0, MAX_ENTRIES).map((v) => {
|
|
4362
|
-
const suffix = v.minorEdit ? " (minor edit)" : "";
|
|
4363
|
-
return `- v${v.number}: ${v.message.trim()}${suffix}`;
|
|
4364
|
-
});
|
|
4365
|
-
return `Edit notes:
|
|
4366
|
-
${lines.join("\n")}`;
|
|
4367
|
-
}
|
|
4368
|
-
if (userVersions.length > 0) {
|
|
4369
|
-
const major = userVersions.filter((v) => !v.minorEdit).length;
|
|
4370
|
-
const minor = userVersions.filter((v) => v.minorEdit).length;
|
|
4371
|
-
const parts = [];
|
|
4372
|
-
if (major > 0) parts.push(`${major} major`);
|
|
4373
|
-
if (minor > 0) parts.push(`${minor} minor`);
|
|
4374
|
-
return `Made ${userVersions.length} edit${userVersions.length > 1 ? "s" : ""} to this page (${parts.join(", ")})`;
|
|
4375
|
-
}
|
|
4376
|
-
return "";
|
|
4377
|
-
}
|
|
4378
4498
|
/**
|
|
4379
4499
|
* Fetch pages with optional filtering
|
|
4380
4500
|
*/
|
|
@@ -4445,7 +4565,14 @@ ${lines.join("\n")}`;
|
|
|
4445
4565
|
break;
|
|
4446
4566
|
}
|
|
4447
4567
|
if (options.limit && allPages.length >= options.limit) {
|
|
4448
|
-
|
|
4568
|
+
const email2 = await this.getCurrentUser();
|
|
4569
|
+
const limitedPages = allPages.slice(0, options.limit);
|
|
4570
|
+
const commits2 = [];
|
|
4571
|
+
for (const page of limitedPages) {
|
|
4572
|
+
const commit = await this.transformPageToCommit(page, void 0, email2 || void 0);
|
|
4573
|
+
commits2.push(commit);
|
|
4574
|
+
}
|
|
4575
|
+
return commits2;
|
|
4449
4576
|
}
|
|
4450
4577
|
start += limit;
|
|
4451
4578
|
} catch (error) {
|
|
@@ -4469,23 +4596,9 @@ ${lines.join("\n")}`;
|
|
|
4469
4596
|
throw error;
|
|
4470
4597
|
}
|
|
4471
4598
|
}
|
|
4472
|
-
const pagesToProcess = options.limit ? allPages.slice(0, options.limit) : allPages;
|
|
4473
4599
|
const email = await this.getCurrentUser();
|
|
4474
|
-
const sinceDate = options.days ? new Date(Date.now() - options.days * 24 * 60 * 60 * 1e3) : void 0;
|
|
4475
|
-
if (sinceDate && email) {
|
|
4476
|
-
const filtered = await this.filterPagesByUserContribution(pagesToProcess, email, sinceDate);
|
|
4477
|
-
logger.debug(
|
|
4478
|
-
`Date-scoped filtering: ${pagesToProcess.length} pages -> ${filtered.length} with user contributions in range`
|
|
4479
|
-
);
|
|
4480
|
-
const commits2 = [];
|
|
4481
|
-
for (const { page, userVersions } of filtered) {
|
|
4482
|
-
const commit = await this.transformPageToCommit(page, void 0, email, userVersions);
|
|
4483
|
-
commits2.push(commit);
|
|
4484
|
-
}
|
|
4485
|
-
return commits2;
|
|
4486
|
-
}
|
|
4487
4600
|
const commits = [];
|
|
4488
|
-
for (const page of
|
|
4601
|
+
for (const page of allPages) {
|
|
4489
4602
|
const commit = await this.transformPageToCommit(page, void 0, email || void 0);
|
|
4490
4603
|
commits.push(commit);
|
|
4491
4604
|
}
|
|
@@ -4507,10 +4620,10 @@ ${lines.join("\n")}`;
|
|
|
4507
4620
|
/**
|
|
4508
4621
|
* Transform Confluence page to GitCommit format with contribution-specific data
|
|
4509
4622
|
*/
|
|
4510
|
-
async transformPageToCommit(page, instanceUrl, userEmail
|
|
4623
|
+
async transformPageToCommit(page, instanceUrl, userEmail) {
|
|
4511
4624
|
let contribution = null;
|
|
4512
4625
|
if (userEmail) {
|
|
4513
|
-
contribution = await this.determineContributionType(page, userEmail
|
|
4626
|
+
contribution = await this.determineContributionType(page, userEmail);
|
|
4514
4627
|
}
|
|
4515
4628
|
let message;
|
|
4516
4629
|
let contributionPrefix = "";
|
|
@@ -4531,14 +4644,6 @@ ${contribution.details}
|
|
|
4531
4644
|
|
|
4532
4645
|
[Confluence Page v${page.version?.number || 1}]`;
|
|
4533
4646
|
}
|
|
4534
|
-
if (userVersions && userVersions.length > 0) {
|
|
4535
|
-
const versionSummary = this.summarizeUserVersions(userVersions);
|
|
4536
|
-
if (versionSummary) {
|
|
4537
|
-
message += `
|
|
4538
|
-
|
|
4539
|
-
${versionSummary}`;
|
|
4540
|
-
}
|
|
4541
|
-
}
|
|
4542
4647
|
let baseUrl = "https://confluence.atlassian.net";
|
|
4543
4648
|
if (instanceUrl) {
|
|
4544
4649
|
baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
|
|
@@ -4556,14 +4661,7 @@ ${versionSummary}`;
|
|
|
4556
4661
|
const impactScore = contribution ? this.calculateContributionImpact(contribution.type, baseSize) : baseSize;
|
|
4557
4662
|
const author = page.history?.createdBy?.displayName || page.version?.by?.displayName || "Unknown Author";
|
|
4558
4663
|
const authorEmail = page.history?.createdBy?.email || page.version?.by?.email || "unknown@example.com";
|
|
4559
|
-
|
|
4560
|
-
if (contribution?.type === "created") {
|
|
4561
|
-
date = page.history?.createdDate || page.version?.when;
|
|
4562
|
-
} else if (userVersions && userVersions.length > 0) {
|
|
4563
|
-
date = userVersions[userVersions.length - 1].when;
|
|
4564
|
-
} else {
|
|
4565
|
-
date = page.version?.when || (/* @__PURE__ */ new Date()).toISOString();
|
|
4566
|
-
}
|
|
4664
|
+
const date = contribution?.type === "created" ? page.history?.createdDate || page.version?.when : page.version?.when || (/* @__PURE__ */ new Date()).toISOString();
|
|
4567
4665
|
return {
|
|
4568
4666
|
sha: page.id,
|
|
4569
4667
|
message,
|
|
@@ -4790,7 +4888,7 @@ init_logger();
|
|
|
4790
4888
|
|
|
4791
4889
|
// src/ui/prompts.ts
|
|
4792
4890
|
init_esm_shims();
|
|
4793
|
-
import { checkbox, confirm, input as input2, select as
|
|
4891
|
+
import { checkbox, confirm, input as input2, select as select3, editor } from "@inquirer/prompts";
|
|
4794
4892
|
import boxen4 from "boxen";
|
|
4795
4893
|
|
|
4796
4894
|
// src/ui/formatters.ts
|
|
@@ -5005,7 +5103,7 @@ async function promptDaysToScan(defaultDays = 30) {
|
|
|
5005
5103
|
{ name: "90 days", value: "90", description: "Last 3 months" },
|
|
5006
5104
|
{ name: "Custom", value: "custom", description: "Enter custom number of days" }
|
|
5007
5105
|
];
|
|
5008
|
-
const selected = await
|
|
5106
|
+
const selected = await select3({
|
|
5009
5107
|
message: "How many days back should we scan for PRs?",
|
|
5010
5108
|
choices,
|
|
5011
5109
|
default: "30"
|
|
@@ -5039,7 +5137,7 @@ async function promptScanMode() {
|
|
|
5039
5137
|
description: "Scan git commits directly"
|
|
5040
5138
|
}
|
|
5041
5139
|
];
|
|
5042
|
-
return await
|
|
5140
|
+
return await select3({
|
|
5043
5141
|
message: "What would you like to scan?",
|
|
5044
5142
|
choices,
|
|
5045
5143
|
default: "prs"
|
|
@@ -5056,7 +5154,7 @@ async function promptSortOption() {
|
|
|
5056
5154
|
{ name: "By files (most files)", value: "files", description: "Most files changed" },
|
|
5057
5155
|
{ name: "No sorting", value: "none", description: "Keep original order" }
|
|
5058
5156
|
];
|
|
5059
|
-
return await
|
|
5157
|
+
return await select3({
|
|
5060
5158
|
message: "How would you like to sort the PRs?",
|
|
5061
5159
|
choices,
|
|
5062
5160
|
default: "date"
|
|
@@ -5070,7 +5168,7 @@ async function promptSelectOrganisation(organisations) {
|
|
|
5070
5168
|
value: org.id
|
|
5071
5169
|
}))
|
|
5072
5170
|
];
|
|
5073
|
-
const selected = await
|
|
5171
|
+
const selected = await select3({
|
|
5074
5172
|
message: "Attach brags to which company?",
|
|
5075
5173
|
choices,
|
|
5076
5174
|
default: "none"
|
|
@@ -5135,7 +5233,7 @@ ${theme.label("PR Link")} ${colors.link(prUrl)}`;
|
|
|
5135
5233
|
}
|
|
5136
5234
|
console.log(boxen4(bragDetails, boxStyles.info));
|
|
5137
5235
|
console.log("");
|
|
5138
|
-
const action = await
|
|
5236
|
+
const action = await select3({
|
|
5139
5237
|
message: `What would you like to do with this brag?`,
|
|
5140
5238
|
choices: [
|
|
5141
5239
|
{ name: "\u2713 Accept", value: "accept", description: "Add this brag as-is" },
|
|
@@ -5752,7 +5850,7 @@ async function promptSelectService() {
|
|
|
5752
5850
|
description: `Sync all ${authenticatedSyncServices.length} authenticated service${authenticatedSyncServices.length > 1 ? "s" : ""}`
|
|
5753
5851
|
});
|
|
5754
5852
|
}
|
|
5755
|
-
const selected = await
|
|
5853
|
+
const selected = await select4({
|
|
5756
5854
|
message: "Select a service to sync:",
|
|
5757
5855
|
choices: serviceChoices
|
|
5758
5856
|
});
|
|
@@ -7053,10 +7151,10 @@ function getConfigHint(error) {
|
|
|
7053
7151
|
// src/cli.ts
|
|
7054
7152
|
init_version();
|
|
7055
7153
|
init_logger();
|
|
7056
|
-
var
|
|
7057
|
-
var
|
|
7058
|
-
var packageJsonPath =
|
|
7059
|
-
var packageJson = JSON.parse(
|
|
7154
|
+
var __filename7 = fileURLToPath7(import.meta.url);
|
|
7155
|
+
var __dirname7 = dirname6(__filename7);
|
|
7156
|
+
var packageJsonPath = join8(__dirname7, "../../package.json");
|
|
7157
|
+
var packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
|
|
7060
7158
|
var program = new Command();
|
|
7061
7159
|
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)");
|
|
7062
7160
|
program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status, bitbucket, gitlab)").action(async (subcommand) => {
|