@bragduck/cli 2.30.2 → 2.36.3
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 +1245 -91
- package/dist/bin/bragduck.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/package.json +20 -21
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, ATLASSIAN_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, GOOGLE_OAUTH_CONFIG, SLACK_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";
|
|
@@ -65,6 +65,16 @@ var init_constants = __esm({
|
|
|
65
65
|
ACCESSIBLE_RESOURCES_URL: "https://api.atlassian.com/oauth/token/accessible-resources",
|
|
66
66
|
API_GATEWAY_URL: "https://api.atlassian.com"
|
|
67
67
|
};
|
|
68
|
+
GOOGLE_OAUTH_CONFIG = {
|
|
69
|
+
CLIENT_ID: "1009691892834-l4hu4qce7jg4a6k1eh2kru0uov1a9fho.apps.googleusercontent.com",
|
|
70
|
+
AUTH_URL: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
71
|
+
SCOPES: "https://www.googleapis.com/auth/drive.metadata.readonly"
|
|
72
|
+
};
|
|
73
|
+
SLACK_OAUTH_CONFIG = {
|
|
74
|
+
CLIENT_ID: "2492403850420.10637070025476",
|
|
75
|
+
AUTH_URL: "https://slack.com/oauth/v2/authorize",
|
|
76
|
+
USER_SCOPE: "search:read"
|
|
77
|
+
};
|
|
68
78
|
API_ENDPOINTS = {
|
|
69
79
|
AUTH: {
|
|
70
80
|
INITIATE: "/v1/auth/cli/initiate",
|
|
@@ -74,6 +84,13 @@ var init_constants = __esm({
|
|
|
74
84
|
TOKEN: "/v1/auth/atlassian/token",
|
|
75
85
|
REFRESH: "/v1/auth/atlassian/refresh"
|
|
76
86
|
},
|
|
87
|
+
GOOGLE: {
|
|
88
|
+
TOKEN: "/v1/auth/google/token",
|
|
89
|
+
REFRESH: "/v1/auth/google/refresh"
|
|
90
|
+
},
|
|
91
|
+
SLACK: {
|
|
92
|
+
TOKEN: "/v1/auth/slack/token"
|
|
93
|
+
},
|
|
77
94
|
BRAGS: {
|
|
78
95
|
CREATE: "/v1/brags",
|
|
79
96
|
LIST: "/v1/brags",
|
|
@@ -169,7 +186,7 @@ var init_env_loader = __esm({
|
|
|
169
186
|
});
|
|
170
187
|
|
|
171
188
|
// src/utils/errors.ts
|
|
172
|
-
var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError, AtlassianError;
|
|
189
|
+
var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError, AtlassianError, GoogleDocsError, SlackError;
|
|
173
190
|
var init_errors = __esm({
|
|
174
191
|
"src/utils/errors.ts"() {
|
|
175
192
|
"use strict";
|
|
@@ -266,6 +283,18 @@ var init_errors = __esm({
|
|
|
266
283
|
this.name = "AtlassianError";
|
|
267
284
|
}
|
|
268
285
|
};
|
|
286
|
+
GoogleDocsError = class extends BragduckError {
|
|
287
|
+
constructor(message, details) {
|
|
288
|
+
super(message, "GOOGLE_DOCS_ERROR", details);
|
|
289
|
+
this.name = "GoogleDocsError";
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
SlackError = class extends BragduckError {
|
|
293
|
+
constructor(message, details) {
|
|
294
|
+
super(message, "SLACK_ERROR", details);
|
|
295
|
+
this.name = "SlackError";
|
|
296
|
+
}
|
|
297
|
+
};
|
|
269
298
|
}
|
|
270
299
|
});
|
|
271
300
|
|
|
@@ -1042,7 +1071,7 @@ var init_browser = __esm({
|
|
|
1042
1071
|
// src/services/auth.service.ts
|
|
1043
1072
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1044
1073
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1045
|
-
import { fileURLToPath as fileURLToPath3, URLSearchParams } from "url";
|
|
1074
|
+
import { fileURLToPath as fileURLToPath3, URLSearchParams as URLSearchParams2 } from "url";
|
|
1046
1075
|
import { dirname as dirname2, join as join3 } from "path";
|
|
1047
1076
|
import { ofetch } from "ofetch";
|
|
1048
1077
|
function getUserAgent() {
|
|
@@ -1086,7 +1115,7 @@ var init_auth_service = __esm({
|
|
|
1086
1115
|
* Build the OAuth authorization URL
|
|
1087
1116
|
*/
|
|
1088
1117
|
async buildAuthUrl(state, callbackUrl) {
|
|
1089
|
-
const params = new
|
|
1118
|
+
const params = new URLSearchParams2({
|
|
1090
1119
|
client_id: OAUTH_CONFIG.CLIENT_ID,
|
|
1091
1120
|
redirect_uri: callbackUrl,
|
|
1092
1121
|
state
|
|
@@ -1261,15 +1290,15 @@ __export(version_exports, {
|
|
|
1261
1290
|
getCurrentVersion: () => getCurrentVersion,
|
|
1262
1291
|
version: () => version
|
|
1263
1292
|
});
|
|
1264
|
-
import { readFileSync as
|
|
1265
|
-
import { fileURLToPath as
|
|
1266
|
-
import { dirname as
|
|
1293
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1294
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
1295
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
1267
1296
|
import chalk4 from "chalk";
|
|
1268
1297
|
import boxen2 from "boxen";
|
|
1269
1298
|
function getCurrentVersion() {
|
|
1270
1299
|
try {
|
|
1271
|
-
const packageJsonPath2 =
|
|
1272
|
-
const packageJson2 = JSON.parse(
|
|
1300
|
+
const packageJsonPath2 = join8(__dirname7, "../../package.json");
|
|
1301
|
+
const packageJson2 = JSON.parse(readFileSync6(packageJsonPath2, "utf-8"));
|
|
1273
1302
|
return packageJson2.version;
|
|
1274
1303
|
} catch {
|
|
1275
1304
|
logger.debug("Failed to read package.json version");
|
|
@@ -1351,7 +1380,7 @@ function formatVersion(includePrefix = true) {
|
|
|
1351
1380
|
const prefix = includePrefix ? "v" : "";
|
|
1352
1381
|
return `${prefix}${version}`;
|
|
1353
1382
|
}
|
|
1354
|
-
var
|
|
1383
|
+
var __filename7, __dirname7, version;
|
|
1355
1384
|
var init_version = __esm({
|
|
1356
1385
|
"src/utils/version.ts"() {
|
|
1357
1386
|
"use strict";
|
|
@@ -1359,22 +1388,22 @@ var init_version = __esm({
|
|
|
1359
1388
|
init_api_service();
|
|
1360
1389
|
init_storage_service();
|
|
1361
1390
|
init_logger();
|
|
1362
|
-
|
|
1363
|
-
|
|
1391
|
+
__filename7 = fileURLToPath7(import.meta.url);
|
|
1392
|
+
__dirname7 = dirname6(__filename7);
|
|
1364
1393
|
version = getCurrentVersion();
|
|
1365
1394
|
}
|
|
1366
1395
|
});
|
|
1367
1396
|
|
|
1368
1397
|
// src/services/api.service.ts
|
|
1369
|
-
import { ofetch as
|
|
1370
|
-
import { readFileSync as
|
|
1371
|
-
import { fileURLToPath as
|
|
1372
|
-
import { URLSearchParams as
|
|
1373
|
-
import { dirname as
|
|
1398
|
+
import { ofetch as ofetch5 } from "ofetch";
|
|
1399
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1400
|
+
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
1401
|
+
import { URLSearchParams as URLSearchParams6 } from "url";
|
|
1402
|
+
import { dirname as dirname7, join as join9 } from "path";
|
|
1374
1403
|
function getCliVersion() {
|
|
1375
1404
|
try {
|
|
1376
|
-
const packageJsonPath2 =
|
|
1377
|
-
const packageJson2 = JSON.parse(
|
|
1405
|
+
const packageJsonPath2 = join9(__dirname8, "../../package.json");
|
|
1406
|
+
const packageJson2 = JSON.parse(readFileSync7(packageJsonPath2, "utf-8"));
|
|
1378
1407
|
return packageJson2.version;
|
|
1379
1408
|
} catch {
|
|
1380
1409
|
logger.debug("Failed to read package.json version");
|
|
@@ -1386,7 +1415,7 @@ function getPlatformInfo() {
|
|
|
1386
1415
|
const arch = process.arch;
|
|
1387
1416
|
return `${platform}-${arch}`;
|
|
1388
1417
|
}
|
|
1389
|
-
var
|
|
1418
|
+
var __filename8, __dirname8, ApiService, apiService;
|
|
1390
1419
|
var init_api_service = __esm({
|
|
1391
1420
|
"src/services/api.service.ts"() {
|
|
1392
1421
|
"use strict";
|
|
@@ -1395,14 +1424,14 @@ var init_api_service = __esm({
|
|
|
1395
1424
|
init_constants();
|
|
1396
1425
|
init_errors();
|
|
1397
1426
|
init_logger();
|
|
1398
|
-
|
|
1399
|
-
|
|
1427
|
+
__filename8 = fileURLToPath8(import.meta.url);
|
|
1428
|
+
__dirname8 = dirname7(__filename8);
|
|
1400
1429
|
ApiService = class {
|
|
1401
1430
|
baseURL;
|
|
1402
1431
|
client;
|
|
1403
1432
|
constructor() {
|
|
1404
1433
|
this.baseURL = process.env.API_BASE_URL || "https://api.bragduck.com";
|
|
1405
|
-
this.client =
|
|
1434
|
+
this.client = ofetch5.create({
|
|
1406
1435
|
baseURL: this.baseURL,
|
|
1407
1436
|
// Request interceptor
|
|
1408
1437
|
onRequest: async ({ request, options }) => {
|
|
@@ -1566,7 +1595,7 @@ var init_api_service = __esm({
|
|
|
1566
1595
|
const { limit = 50, offset = 0, tags, search } = params;
|
|
1567
1596
|
logger.debug(`Listing brags: limit=${limit}, offset=${offset}`);
|
|
1568
1597
|
try {
|
|
1569
|
-
const queryParams = new
|
|
1598
|
+
const queryParams = new URLSearchParams6({
|
|
1570
1599
|
limit: limit.toString(),
|
|
1571
1600
|
offset: offset.toString()
|
|
1572
1601
|
});
|
|
@@ -1663,7 +1692,7 @@ var init_api_service = __esm({
|
|
|
1663
1692
|
*/
|
|
1664
1693
|
setBaseURL(url) {
|
|
1665
1694
|
this.baseURL = url;
|
|
1666
|
-
this.client =
|
|
1695
|
+
this.client = ofetch5.create({
|
|
1667
1696
|
baseURL: url
|
|
1668
1697
|
});
|
|
1669
1698
|
}
|
|
@@ -1681,9 +1710,9 @@ var init_api_service = __esm({
|
|
|
1681
1710
|
// src/cli.ts
|
|
1682
1711
|
init_esm_shims();
|
|
1683
1712
|
import { Command } from "commander";
|
|
1684
|
-
import { readFileSync as
|
|
1685
|
-
import { fileURLToPath as
|
|
1686
|
-
import { dirname as
|
|
1713
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
1714
|
+
import { fileURLToPath as fileURLToPath9 } from "url";
|
|
1715
|
+
import { dirname as dirname8, join as join10 } from "path";
|
|
1687
1716
|
|
|
1688
1717
|
// src/commands/auth.ts
|
|
1689
1718
|
init_esm_shims();
|
|
@@ -1702,7 +1731,7 @@ init_errors();
|
|
|
1702
1731
|
init_logger();
|
|
1703
1732
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
1704
1733
|
import { readFileSync as readFileSync3 } from "fs";
|
|
1705
|
-
import { fileURLToPath as fileURLToPath4, URLSearchParams as
|
|
1734
|
+
import { fileURLToPath as fileURLToPath4, URLSearchParams as URLSearchParams3 } from "url";
|
|
1706
1735
|
import { dirname as dirname3, join as join4 } from "path";
|
|
1707
1736
|
import { ofetch as ofetch2 } from "ofetch";
|
|
1708
1737
|
import { select } from "@inquirer/prompts";
|
|
@@ -1737,7 +1766,7 @@ var AtlassianAuthService = class {
|
|
|
1737
1766
|
* Build the Atlassian OAuth authorization URL
|
|
1738
1767
|
*/
|
|
1739
1768
|
buildAuthUrl(state, callbackUrl) {
|
|
1740
|
-
const params = new
|
|
1769
|
+
const params = new URLSearchParams3({
|
|
1741
1770
|
audience: ATLASSIAN_OAUTH_CONFIG.AUDIENCE,
|
|
1742
1771
|
client_id: ATLASSIAN_OAUTH_CONFIG.CLIENT_ID,
|
|
1743
1772
|
scope: ATLASSIAN_OAUTH_CONFIG.SCOPES,
|
|
@@ -1938,6 +1967,348 @@ var AtlassianAuthService = class {
|
|
|
1938
1967
|
};
|
|
1939
1968
|
var atlassianAuthService = new AtlassianAuthService();
|
|
1940
1969
|
|
|
1970
|
+
// src/services/google-auth.service.ts
|
|
1971
|
+
init_esm_shims();
|
|
1972
|
+
init_constants();
|
|
1973
|
+
init_storage_service();
|
|
1974
|
+
init_oauth_server();
|
|
1975
|
+
init_browser();
|
|
1976
|
+
init_errors();
|
|
1977
|
+
init_logger();
|
|
1978
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
1979
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1980
|
+
import { fileURLToPath as fileURLToPath5, URLSearchParams as URLSearchParams4 } from "url";
|
|
1981
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
1982
|
+
import { ofetch as ofetch3 } from "ofetch";
|
|
1983
|
+
var __filename5 = fileURLToPath5(import.meta.url);
|
|
1984
|
+
var __dirname5 = dirname4(__filename5);
|
|
1985
|
+
function getUserAgent3() {
|
|
1986
|
+
try {
|
|
1987
|
+
const packageJsonPath2 = join5(__dirname5, "../../package.json");
|
|
1988
|
+
const packageJson2 = JSON.parse(readFileSync4(packageJsonPath2, "utf-8"));
|
|
1989
|
+
const version2 = packageJson2.version;
|
|
1990
|
+
const platform = process.platform;
|
|
1991
|
+
const arch = process.arch;
|
|
1992
|
+
return `BragDuck-CLI/${version2} (${platform}-${arch})`;
|
|
1993
|
+
} catch {
|
|
1994
|
+
logger.debug("Failed to read package.json version");
|
|
1995
|
+
return "BragDuck-CLI/2.0.0";
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
var GoogleAuthService = class {
|
|
1999
|
+
apiBaseUrl;
|
|
2000
|
+
refreshPromise = null;
|
|
2001
|
+
constructor() {
|
|
2002
|
+
this.apiBaseUrl = process.env.API_BASE_URL || "https://api.bragduck.com";
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Generate a random state string for CSRF protection
|
|
2006
|
+
*/
|
|
2007
|
+
generateState() {
|
|
2008
|
+
return randomBytes4(32).toString("hex");
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Build the Google OAuth authorization URL
|
|
2012
|
+
*/
|
|
2013
|
+
buildAuthUrl(state, callbackUrl) {
|
|
2014
|
+
const params = new URLSearchParams4({
|
|
2015
|
+
client_id: GOOGLE_OAUTH_CONFIG.CLIENT_ID,
|
|
2016
|
+
scope: GOOGLE_OAUTH_CONFIG.SCOPES,
|
|
2017
|
+
redirect_uri: callbackUrl,
|
|
2018
|
+
state,
|
|
2019
|
+
response_type: "code",
|
|
2020
|
+
access_type: "offline",
|
|
2021
|
+
prompt: "consent"
|
|
2022
|
+
});
|
|
2023
|
+
return `${GOOGLE_OAUTH_CONFIG.AUTH_URL}?${params.toString()}`;
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Exchange authorization code for tokens via BragDuck backend
|
|
2027
|
+
*/
|
|
2028
|
+
async exchangeCodeForToken(code, redirectUri) {
|
|
2029
|
+
try {
|
|
2030
|
+
const tokenUrl = `${this.apiBaseUrl}${API_ENDPOINTS.GOOGLE.TOKEN}`;
|
|
2031
|
+
logger.debug(`Exchanging Google authorization code via: ${tokenUrl}`);
|
|
2032
|
+
const response = await ofetch3(tokenUrl, {
|
|
2033
|
+
method: "POST",
|
|
2034
|
+
body: {
|
|
2035
|
+
code,
|
|
2036
|
+
redirect_uri: redirectUri
|
|
2037
|
+
},
|
|
2038
|
+
headers: {
|
|
2039
|
+
"Content-Type": "application/json",
|
|
2040
|
+
"User-Agent": getUserAgent3()
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
logger.debug("Google token exchange successful");
|
|
2044
|
+
return response;
|
|
2045
|
+
} catch (error) {
|
|
2046
|
+
const detail = error?.data?.message || error?.data?.error_description || error.message;
|
|
2047
|
+
logger.debug(`Google token exchange failed: ${detail}`);
|
|
2048
|
+
throw new GoogleDocsError(`Failed to exchange Google authorization code: ${detail}`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Full OAuth login flow:
|
|
2053
|
+
* 1. Open browser to Google consent screen
|
|
2054
|
+
* 2. Wait for callback with authorization code
|
|
2055
|
+
* 3. Exchange code for tokens via BragDuck backend
|
|
2056
|
+
* 4. Store credentials under 'google-docs'
|
|
2057
|
+
*/
|
|
2058
|
+
async login() {
|
|
2059
|
+
logger.debug("Starting Google OAuth login flow");
|
|
2060
|
+
const state = this.generateState();
|
|
2061
|
+
const { callbackUrl: rawCallbackUrl, resultPromise } = await startOAuthCallbackServer(state);
|
|
2062
|
+
const callbackUrl = rawCallbackUrl.replace("127.0.0.1", "localhost");
|
|
2063
|
+
storageService.setOAuthState({
|
|
2064
|
+
state,
|
|
2065
|
+
createdAt: Date.now()
|
|
2066
|
+
});
|
|
2067
|
+
logger.debug(`OAuth state: ${state}`);
|
|
2068
|
+
logger.debug(`Callback URL: ${callbackUrl}`);
|
|
2069
|
+
const authUrl = this.buildAuthUrl(state, callbackUrl);
|
|
2070
|
+
logger.debug(`Authorization URL: ${authUrl}`);
|
|
2071
|
+
try {
|
|
2072
|
+
await openBrowser(authUrl);
|
|
2073
|
+
} catch {
|
|
2074
|
+
logger.warning("Could not open browser automatically");
|
|
2075
|
+
logger.info("Please open this URL in your browser:");
|
|
2076
|
+
logger.log(authUrl);
|
|
2077
|
+
}
|
|
2078
|
+
let callbackResult;
|
|
2079
|
+
try {
|
|
2080
|
+
callbackResult = await resultPromise;
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
storageService.deleteOAuthState();
|
|
2083
|
+
throw error;
|
|
2084
|
+
}
|
|
2085
|
+
storageService.deleteOAuthState();
|
|
2086
|
+
const tokenResponse = await this.exchangeCodeForToken(callbackResult.code, callbackUrl);
|
|
2087
|
+
const expiresAt = tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : void 0;
|
|
2088
|
+
const credentials = {
|
|
2089
|
+
accessToken: tokenResponse.access_token,
|
|
2090
|
+
refreshToken: tokenResponse.refresh_token,
|
|
2091
|
+
expiresAt,
|
|
2092
|
+
authMethod: "oauth"
|
|
2093
|
+
};
|
|
2094
|
+
await storageService.setServiceCredentials("google-docs", credentials);
|
|
2095
|
+
logger.debug("Google OAuth login successful");
|
|
2096
|
+
let email = "authenticated";
|
|
2097
|
+
try {
|
|
2098
|
+
const about = await ofetch3(
|
|
2099
|
+
"https://www.googleapis.com/drive/v3/about?fields=user",
|
|
2100
|
+
{
|
|
2101
|
+
headers: {
|
|
2102
|
+
Authorization: `Bearer ${tokenResponse.access_token}`
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
);
|
|
2106
|
+
email = about.user?.emailAddress || email;
|
|
2107
|
+
} catch {
|
|
2108
|
+
logger.debug("Could not fetch Google user info after login");
|
|
2109
|
+
}
|
|
2110
|
+
return { email };
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Refresh Google OAuth tokens via BragDuck backend.
|
|
2114
|
+
* Uses a singleton promise to prevent concurrent refresh race conditions.
|
|
2115
|
+
*/
|
|
2116
|
+
async refreshToken() {
|
|
2117
|
+
if (this.refreshPromise) {
|
|
2118
|
+
return this.refreshPromise;
|
|
2119
|
+
}
|
|
2120
|
+
this.refreshPromise = this.doRefreshToken();
|
|
2121
|
+
try {
|
|
2122
|
+
await this.refreshPromise;
|
|
2123
|
+
} finally {
|
|
2124
|
+
this.refreshPromise = null;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
async doRefreshToken() {
|
|
2128
|
+
logger.debug("Refreshing Google OAuth token");
|
|
2129
|
+
const creds = await storageService.getServiceCredentials("google-docs");
|
|
2130
|
+
if (!creds?.refreshToken) {
|
|
2131
|
+
throw new GoogleDocsError("No Google refresh token available", {
|
|
2132
|
+
hint: "Run: bragduck auth google"
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
try {
|
|
2136
|
+
const response = await ofetch3(
|
|
2137
|
+
`${this.apiBaseUrl}${API_ENDPOINTS.GOOGLE.REFRESH}`,
|
|
2138
|
+
{
|
|
2139
|
+
method: "POST",
|
|
2140
|
+
body: {
|
|
2141
|
+
refresh_token: creds.refreshToken
|
|
2142
|
+
},
|
|
2143
|
+
headers: {
|
|
2144
|
+
"Content-Type": "application/json",
|
|
2145
|
+
"User-Agent": getUserAgent3()
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
);
|
|
2149
|
+
const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1e3 : void 0;
|
|
2150
|
+
const updatedCreds = {
|
|
2151
|
+
...creds,
|
|
2152
|
+
accessToken: response.access_token,
|
|
2153
|
+
refreshToken: response.refresh_token ?? creds.refreshToken,
|
|
2154
|
+
expiresAt
|
|
2155
|
+
};
|
|
2156
|
+
await storageService.setServiceCredentials("google-docs", updatedCreds);
|
|
2157
|
+
logger.debug("Google token refresh successful");
|
|
2158
|
+
} catch (error) {
|
|
2159
|
+
const detail = error?.data?.message || error?.data?.error_description || error.message;
|
|
2160
|
+
logger.debug(`Google token refresh failed: ${detail}`);
|
|
2161
|
+
throw new GoogleDocsError(
|
|
2162
|
+
`Google token refresh failed: ${detail}. Please run "bragduck auth google" to re-authenticate.`
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
var googleAuthService = new GoogleAuthService();
|
|
2168
|
+
|
|
2169
|
+
// src/services/slack-auth.service.ts
|
|
2170
|
+
init_esm_shims();
|
|
2171
|
+
init_constants();
|
|
2172
|
+
init_storage_service();
|
|
2173
|
+
init_browser();
|
|
2174
|
+
init_errors();
|
|
2175
|
+
init_logger();
|
|
2176
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
2177
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
2178
|
+
import { fileURLToPath as fileURLToPath6, URLSearchParams as URLSearchParams5 } from "url";
|
|
2179
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
2180
|
+
import { ofetch as ofetch4 } from "ofetch";
|
|
2181
|
+
var __filename6 = fileURLToPath6(import.meta.url);
|
|
2182
|
+
var __dirname6 = dirname5(__filename6);
|
|
2183
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
2184
|
+
var POLL_TIMEOUT_MS = 12e4;
|
|
2185
|
+
function getUserAgent4() {
|
|
2186
|
+
try {
|
|
2187
|
+
const packageJsonPath2 = join6(__dirname6, "../../package.json");
|
|
2188
|
+
const packageJson2 = JSON.parse(readFileSync5(packageJsonPath2, "utf-8"));
|
|
2189
|
+
const version2 = packageJson2.version;
|
|
2190
|
+
const platform = process.platform;
|
|
2191
|
+
const arch = process.arch;
|
|
2192
|
+
return `BragDuck-CLI/${version2} (${platform}-${arch})`;
|
|
2193
|
+
} catch {
|
|
2194
|
+
logger.debug("Failed to read package.json version");
|
|
2195
|
+
return "BragDuck-CLI/2.0.0";
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
var SlackAuthService = class {
|
|
2199
|
+
apiBaseUrl;
|
|
2200
|
+
constructor() {
|
|
2201
|
+
this.apiBaseUrl = process.env.API_BASE_URL || "https://api.bragduck.com";
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* The HTTPS redirect URI registered with Slack.
|
|
2205
|
+
* Slack redirects here after the user authorises.
|
|
2206
|
+
*/
|
|
2207
|
+
get redirectUri() {
|
|
2208
|
+
return `${this.apiBaseUrl}/v1/auth/slack/callback`;
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Generate a random state string for CSRF protection
|
|
2212
|
+
*/
|
|
2213
|
+
generateState() {
|
|
2214
|
+
return randomBytes5(32).toString("hex");
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Build the Slack OAuth v2 authorization URL.
|
|
2218
|
+
* Uses user_scope param (not scope) to request a user token.
|
|
2219
|
+
*/
|
|
2220
|
+
buildAuthUrl(state) {
|
|
2221
|
+
const params = new URLSearchParams5({
|
|
2222
|
+
client_id: SLACK_OAUTH_CONFIG.CLIENT_ID,
|
|
2223
|
+
user_scope: SLACK_OAUTH_CONFIG.USER_SCOPE,
|
|
2224
|
+
redirect_uri: this.redirectUri,
|
|
2225
|
+
state
|
|
2226
|
+
});
|
|
2227
|
+
return `${SLACK_OAUTH_CONFIG.AUTH_URL}?${params.toString()}`;
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Poll the BragDuck backend until the Slack callback arrives or timeout
|
|
2231
|
+
*/
|
|
2232
|
+
async pollForCode(state) {
|
|
2233
|
+
const pollUrl = `${this.apiBaseUrl}/v1/auth/slack/poll`;
|
|
2234
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
2235
|
+
while (Date.now() < deadline) {
|
|
2236
|
+
await new Promise((r) => globalThis.setTimeout(r, POLL_INTERVAL_MS));
|
|
2237
|
+
const result = await ofetch4(`${pollUrl}?state=${state}`, {
|
|
2238
|
+
headers: { "User-Agent": getUserAgent4() }
|
|
2239
|
+
}).catch(() => null);
|
|
2240
|
+
if (!result) continue;
|
|
2241
|
+
if (result.status === "complete" && result.code) {
|
|
2242
|
+
return result.code;
|
|
2243
|
+
}
|
|
2244
|
+
if (result.status === "error") {
|
|
2245
|
+
throw new SlackError(`Slack OAuth error: ${result.error}`);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
throw new SlackError("Slack authentication timed out. Please try again.");
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Exchange authorization code for a user token via BragDuck backend
|
|
2252
|
+
*/
|
|
2253
|
+
async exchangeCodeForToken(code) {
|
|
2254
|
+
try {
|
|
2255
|
+
const tokenUrl = `${this.apiBaseUrl}${API_ENDPOINTS.SLACK.TOKEN}`;
|
|
2256
|
+
logger.debug(`Exchanging Slack authorization code via: ${tokenUrl}`);
|
|
2257
|
+
const response = await ofetch4(tokenUrl, {
|
|
2258
|
+
method: "POST",
|
|
2259
|
+
body: {
|
|
2260
|
+
code,
|
|
2261
|
+
redirect_uri: this.redirectUri
|
|
2262
|
+
},
|
|
2263
|
+
headers: {
|
|
2264
|
+
"Content-Type": "application/json",
|
|
2265
|
+
"User-Agent": getUserAgent4()
|
|
2266
|
+
}
|
|
2267
|
+
});
|
|
2268
|
+
logger.debug("Slack token exchange successful");
|
|
2269
|
+
return response;
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
const detail = error?.data?.message || error?.data?.error_description || error.message;
|
|
2272
|
+
logger.debug(`Slack token exchange failed: ${detail}`);
|
|
2273
|
+
throw new SlackError(`Failed to exchange Slack authorization code: ${detail}`);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
/**
|
|
2277
|
+
* Full OAuth login flow:
|
|
2278
|
+
* 1. Open browser to Slack consent screen (redirect_uri → BragDuck backend)
|
|
2279
|
+
* 2. Poll backend until the code arrives
|
|
2280
|
+
* 3. Exchange code for user token
|
|
2281
|
+
* 4. Store credentials under 'slack' service key
|
|
2282
|
+
* Returns team name for confirmation display.
|
|
2283
|
+
*/
|
|
2284
|
+
async login() {
|
|
2285
|
+
logger.debug("Starting Slack OAuth login flow");
|
|
2286
|
+
const state = this.generateState();
|
|
2287
|
+
const authUrl = this.buildAuthUrl(state);
|
|
2288
|
+
logger.debug(`Authorization URL: ${authUrl}`);
|
|
2289
|
+
logger.debug(`Redirect URI: ${this.redirectUri}`);
|
|
2290
|
+
try {
|
|
2291
|
+
await openBrowser(authUrl);
|
|
2292
|
+
} catch {
|
|
2293
|
+
logger.warning("Could not open browser automatically");
|
|
2294
|
+
logger.info("Please open this URL in your browser:");
|
|
2295
|
+
logger.log(authUrl);
|
|
2296
|
+
}
|
|
2297
|
+
logger.info("Waiting for Slack authorization...");
|
|
2298
|
+
const code = await this.pollForCode(state);
|
|
2299
|
+
const tokenResponse = await this.exchangeCodeForToken(code);
|
|
2300
|
+
const credentials = {
|
|
2301
|
+
accessToken: tokenResponse.access_token,
|
|
2302
|
+
username: tokenResponse.user_id,
|
|
2303
|
+
authMethod: "oauth"
|
|
2304
|
+
};
|
|
2305
|
+
await storageService.setServiceCredentials("slack", credentials);
|
|
2306
|
+
logger.debug("Slack OAuth login successful");
|
|
2307
|
+
return { teamName: tokenResponse.team_name };
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
var slackAuthService = new SlackAuthService();
|
|
2311
|
+
|
|
1941
2312
|
// src/commands/auth.ts
|
|
1942
2313
|
init_storage_service();
|
|
1943
2314
|
|
|
@@ -1956,9 +2327,9 @@ import simpleGit from "simple-git";
|
|
|
1956
2327
|
init_esm_shims();
|
|
1957
2328
|
init_errors();
|
|
1958
2329
|
import { existsSync as existsSync2 } from "fs";
|
|
1959
|
-
import { join as
|
|
2330
|
+
import { join as join7 } from "path";
|
|
1960
2331
|
function validateGitRepository(path4) {
|
|
1961
|
-
const gitDir =
|
|
2332
|
+
const gitDir = join7(path4, ".git");
|
|
1962
2333
|
if (!existsSync2(gitDir)) {
|
|
1963
2334
|
throw new GitError(
|
|
1964
2335
|
"Not a git repository. Please run this command from within a git repository.",
|
|
@@ -2269,6 +2640,18 @@ var GitHubService = class {
|
|
|
2269
2640
|
});
|
|
2270
2641
|
}
|
|
2271
2642
|
if (error.message?.includes("Could not resolve to a Repository")) {
|
|
2643
|
+
try {
|
|
2644
|
+
const { stdout: remoteUrl } = await execAsync2("command git remote get-url origin", {
|
|
2645
|
+
cwd: repoPath || process.cwd()
|
|
2646
|
+
});
|
|
2647
|
+
if (remoteUrl.includes("github.com")) {
|
|
2648
|
+
throw new GitHubError("Cannot access this GitHub repository", {
|
|
2649
|
+
hint: 'For private repositories, run "gh auth refresh -s repo" to add the required scope'
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
} catch (remoteErr) {
|
|
2653
|
+
if (remoteErr instanceof GitHubError) throw remoteErr;
|
|
2654
|
+
}
|
|
2272
2655
|
throw new GitHubError("This repository is not hosted on GitHub", {
|
|
2273
2656
|
hint: "Only GitHub repositories are currently supported for PR scanning"
|
|
2274
2657
|
});
|
|
@@ -2576,9 +2959,15 @@ async function authCommand(subcommand) {
|
|
|
2576
2959
|
await authGitLab();
|
|
2577
2960
|
} else if (subcommand === "atlassian") {
|
|
2578
2961
|
await authAtlassian();
|
|
2962
|
+
} else if (subcommand === "google") {
|
|
2963
|
+
await authGoogle();
|
|
2964
|
+
} else if (subcommand === "slack") {
|
|
2965
|
+
await authSlack();
|
|
2579
2966
|
} else {
|
|
2580
2967
|
logger.error(`Unknown auth subcommand: ${subcommand}`);
|
|
2581
|
-
logger.info(
|
|
2968
|
+
logger.info(
|
|
2969
|
+
"Available subcommands: login, status, bitbucket, gitlab, atlassian, google, slack"
|
|
2970
|
+
);
|
|
2582
2971
|
process.exit(1);
|
|
2583
2972
|
}
|
|
2584
2973
|
}
|
|
@@ -2650,43 +3039,87 @@ async function authStatus() {
|
|
|
2650
3039
|
logger.log("");
|
|
2651
3040
|
logger.info("Authentication Status:");
|
|
2652
3041
|
logger.log("");
|
|
2653
|
-
const services = await storageService.getAuthenticatedServices();
|
|
2654
3042
|
const bragduckAuth = await storageService.isServiceAuthenticated("bragduck");
|
|
2655
3043
|
if (bragduckAuth) {
|
|
2656
3044
|
const userInfo = authService.getUserInfo();
|
|
2657
3045
|
logger.info(`${colors.success("\u2713")} Bragduck: ${userInfo?.name || "Authenticated"}`);
|
|
2658
3046
|
} else {
|
|
2659
3047
|
logger.info(`${colors.error("\u2717")} Bragduck: Not authenticated`);
|
|
3048
|
+
logger.info(theme.secondary(` Run: bragduck auth login`));
|
|
2660
3049
|
}
|
|
3050
|
+
logger.log("");
|
|
3051
|
+
logger.info("Git Sources:");
|
|
3052
|
+
logger.log("");
|
|
2661
3053
|
const ghStatus = await githubService.getAuthStatus();
|
|
2662
3054
|
if (ghStatus.installed) {
|
|
2663
3055
|
if (ghStatus.authenticated) {
|
|
2664
|
-
logger.info(`${colors.success("\u2713")} GitHub
|
|
3056
|
+
logger.info(`${colors.success("\u2713")} GitHub: Authenticated`);
|
|
2665
3057
|
} else {
|
|
2666
|
-
logger.info(`${colors.error("\u2717")} GitHub
|
|
3058
|
+
logger.info(`${colors.error("\u2717")} GitHub: Not authenticated`);
|
|
2667
3059
|
logger.info(theme.secondary(" Run: gh auth login"));
|
|
2668
3060
|
}
|
|
2669
3061
|
} else {
|
|
2670
|
-
logger.info(`${colors.error("\u2717")} GitHub CLI
|
|
3062
|
+
logger.info(`${colors.error("\u2717")} GitHub: gh CLI not installed`);
|
|
2671
3063
|
logger.info(theme.secondary(" Install from: https://cli.github.com"));
|
|
2672
3064
|
}
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
3065
|
+
const gitlabAuth = await storageService.isServiceAuthenticated("gitlab");
|
|
3066
|
+
if (gitlabAuth) {
|
|
3067
|
+
const creds = await storageService.getServiceCredentials("gitlab");
|
|
3068
|
+
const instanceLabel = creds?.instanceUrl ? ` (${creds.instanceUrl})` : " (gitlab.com)";
|
|
3069
|
+
logger.info(`${colors.success("\u2713")} GitLab: Authenticated${instanceLabel}`);
|
|
3070
|
+
} else {
|
|
3071
|
+
logger.info(`${colors.error("\u2717")} GitLab: Not connected`);
|
|
3072
|
+
logger.info(theme.secondary(" Run: bragduck auth gitlab"));
|
|
3073
|
+
}
|
|
3074
|
+
const bitbucketAuth = await storageService.isServiceAuthenticated("bitbucket");
|
|
3075
|
+
if (bitbucketAuth) {
|
|
3076
|
+
const creds = await storageService.getServiceCredentials("bitbucket");
|
|
3077
|
+
const userLabel = creds?.username ? ` (${creds.username})` : "";
|
|
3078
|
+
logger.info(`${colors.success("\u2713")} Bitbucket: Authenticated${userLabel}`);
|
|
3079
|
+
} else {
|
|
3080
|
+
logger.info(`${colors.error("\u2717")} Bitbucket: Not connected`);
|
|
3081
|
+
logger.info(theme.secondary(" Run: bragduck auth bitbucket"));
|
|
2684
3082
|
}
|
|
2685
3083
|
logger.log("");
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
3084
|
+
logger.info("Project Management:");
|
|
3085
|
+
logger.log("");
|
|
3086
|
+
const jiraAuth = await storageService.isServiceAuthenticated("jira");
|
|
3087
|
+
const confluenceAuth = await storageService.isServiceAuthenticated("confluence");
|
|
3088
|
+
if (jiraAuth || confluenceAuth) {
|
|
3089
|
+
const creds = await storageService.getServiceCredentials("jira");
|
|
3090
|
+
let methodLabel = "";
|
|
3091
|
+
if (creds?.authMethod === "oauth") {
|
|
3092
|
+
methodLabel = " (Cloud - OAuth)";
|
|
3093
|
+
} else if (creds?.authMethod === "basic") {
|
|
3094
|
+
methodLabel = " (Server - API Token)";
|
|
3095
|
+
}
|
|
3096
|
+
logger.info(`${colors.success("\u2713")} Atlassian${methodLabel}`);
|
|
3097
|
+
if (jiraAuth) logger.info(theme.secondary(" \xB7 Jira"));
|
|
3098
|
+
if (confluenceAuth) logger.info(theme.secondary(" \xB7 Confluence"));
|
|
3099
|
+
} else {
|
|
3100
|
+
logger.info(`${colors.error("\u2717")} Atlassian (Jira + Confluence): Not connected`);
|
|
3101
|
+
logger.info(theme.secondary(" Run: bragduck auth atlassian"));
|
|
2689
3102
|
}
|
|
3103
|
+
logger.log("");
|
|
3104
|
+
logger.info("Document Sources:");
|
|
3105
|
+
logger.log("");
|
|
3106
|
+
const googleDocsAuth = await storageService.isServiceAuthenticated("google-docs");
|
|
3107
|
+
if (googleDocsAuth) {
|
|
3108
|
+
logger.info(`${colors.success("\u2713")} Google Docs (OAuth)`);
|
|
3109
|
+
} else {
|
|
3110
|
+
logger.info(`${colors.error("\u2717")} Google Docs: Not connected`);
|
|
3111
|
+
logger.info(theme.secondary(" Run: bragduck auth google"));
|
|
3112
|
+
}
|
|
3113
|
+
const slackAuth = await storageService.isServiceAuthenticated("slack");
|
|
3114
|
+
if (slackAuth) {
|
|
3115
|
+
const creds = await storageService.getServiceCredentials("slack");
|
|
3116
|
+
const userLabel = creds?.username ? ` (user: ${creds.username})` : "";
|
|
3117
|
+
logger.info(`${colors.success("\u2713")} Slack: Connected${userLabel}`);
|
|
3118
|
+
} else {
|
|
3119
|
+
logger.info(`${colors.error("\u2717")} Slack: Not connected`);
|
|
3120
|
+
logger.info(theme.secondary(" Run: bragduck auth slack"));
|
|
3121
|
+
}
|
|
3122
|
+
logger.log("");
|
|
2690
3123
|
}
|
|
2691
3124
|
async function authBitbucket() {
|
|
2692
3125
|
logger.log("");
|
|
@@ -2863,8 +3296,72 @@ Services configured:
|
|
|
2863
3296
|
process.exit(1);
|
|
2864
3297
|
}
|
|
2865
3298
|
}
|
|
2866
|
-
|
|
2867
|
-
|
|
3299
|
+
async function authGoogle() {
|
|
3300
|
+
logger.log("");
|
|
3301
|
+
logger.info("Opening browser for Google authentication...");
|
|
3302
|
+
logger.log("");
|
|
3303
|
+
try {
|
|
3304
|
+
const result = await googleAuthService.login();
|
|
3305
|
+
logger.log("");
|
|
3306
|
+
logger.log(
|
|
3307
|
+
boxen(
|
|
3308
|
+
theme.success("\u2713 Successfully authenticated with Google") + `
|
|
3309
|
+
|
|
3310
|
+
Account: ${result.email}
|
|
3311
|
+
|
|
3312
|
+
Services configured:
|
|
3313
|
+
- Google Docs (OAuth)`,
|
|
3314
|
+
boxStyles.success
|
|
3315
|
+
)
|
|
3316
|
+
);
|
|
3317
|
+
logger.log("");
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
const err = error;
|
|
3320
|
+
logger.log("");
|
|
3321
|
+
logger.log(
|
|
3322
|
+
boxen(
|
|
3323
|
+
theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
|
|
3324
|
+
boxStyles.error
|
|
3325
|
+
)
|
|
3326
|
+
);
|
|
3327
|
+
logger.log("");
|
|
3328
|
+
process.exit(1);
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
async function authSlack() {
|
|
3332
|
+
logger.log("");
|
|
3333
|
+
logger.info("Opening browser for Slack authentication...");
|
|
3334
|
+
logger.log("");
|
|
3335
|
+
try {
|
|
3336
|
+
const result = await slackAuthService.login();
|
|
3337
|
+
logger.log("");
|
|
3338
|
+
logger.log(
|
|
3339
|
+
boxen(
|
|
3340
|
+
theme.success("\u2713 Successfully authenticated with Slack") + `
|
|
3341
|
+
|
|
3342
|
+
Workspace: ${result.teamName}
|
|
3343
|
+
|
|
3344
|
+
Services configured:
|
|
3345
|
+
- Slack messages (OAuth)`,
|
|
3346
|
+
boxStyles.success
|
|
3347
|
+
)
|
|
3348
|
+
);
|
|
3349
|
+
logger.log("");
|
|
3350
|
+
} catch (error) {
|
|
3351
|
+
const err = error;
|
|
3352
|
+
logger.log("");
|
|
3353
|
+
logger.log(
|
|
3354
|
+
boxen(
|
|
3355
|
+
theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
|
|
3356
|
+
boxStyles.error
|
|
3357
|
+
)
|
|
3358
|
+
);
|
|
3359
|
+
logger.log("");
|
|
3360
|
+
process.exit(1);
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
// src/commands/sync.ts
|
|
2868
3365
|
init_esm_shims();
|
|
2869
3366
|
|
|
2870
3367
|
// ../../node_modules/.pnpm/@inquirer+core@9.2.1/node_modules/@inquirer/core/dist/esm/lib/errors.mjs
|
|
@@ -3459,7 +3956,7 @@ init_logger();
|
|
|
3459
3956
|
init_storage_service();
|
|
3460
3957
|
import { exec as exec5 } from "child_process";
|
|
3461
3958
|
import { promisify as promisify5 } from "util";
|
|
3462
|
-
import { URLSearchParams as
|
|
3959
|
+
import { URLSearchParams as URLSearchParams7 } from "url";
|
|
3463
3960
|
var execAsync5 = promisify5(exec5);
|
|
3464
3961
|
var GitLabService = class {
|
|
3465
3962
|
DEFAULT_INSTANCE = "https://gitlab.com";
|
|
@@ -3605,7 +4102,7 @@ var GitLabService = class {
|
|
|
3605
4102
|
async getMergedMRs(options = {}) {
|
|
3606
4103
|
const { projectPath } = await this.getProjectFromGit();
|
|
3607
4104
|
const encodedPath = encodeURIComponent(projectPath);
|
|
3608
|
-
const params = new
|
|
4105
|
+
const params = new URLSearchParams7({
|
|
3609
4106
|
state: "merged",
|
|
3610
4107
|
order_by: "updated_at",
|
|
3611
4108
|
sort: "desc",
|
|
@@ -4073,6 +4570,9 @@ var JiraService = class {
|
|
|
4073
4570
|
logger.debug(
|
|
4074
4571
|
`Fetched ${newIssuesCount} new issues (total: ${allIssues.length} of ${response.total})${startAt + maxResults < response.total ? ", fetching next page..." : ""}`
|
|
4075
4572
|
);
|
|
4573
|
+
if (options.onProgress) {
|
|
4574
|
+
options.onProgress(allIssues.length, response.total);
|
|
4575
|
+
}
|
|
4076
4576
|
if (newIssuesCount === 0) {
|
|
4077
4577
|
logger.debug("All results are duplicates, stopping pagination");
|
|
4078
4578
|
break;
|
|
@@ -4240,7 +4740,8 @@ var jiraSyncAdapter = {
|
|
|
4240
4740
|
return jiraService.getIssues({
|
|
4241
4741
|
days: options.days,
|
|
4242
4742
|
limit: options.limit,
|
|
4243
|
-
author: author || void 0
|
|
4743
|
+
author: author || void 0,
|
|
4744
|
+
onProgress: options.onProgress
|
|
4244
4745
|
});
|
|
4245
4746
|
},
|
|
4246
4747
|
async isAuthenticated() {
|
|
@@ -4558,6 +5059,9 @@ var ConfluenceService = class {
|
|
|
4558
5059
|
logger.debug(
|
|
4559
5060
|
`Fetched ${newPagesCount} new pages (total: ${allPages.length}, duplicates: ${response.results.length - newPagesCount})${response.size === limit ? ", fetching next page..." : ""}`
|
|
4560
5061
|
);
|
|
5062
|
+
if (options.onProgress) {
|
|
5063
|
+
options.onProgress(allPages.length);
|
|
5064
|
+
}
|
|
4561
5065
|
if (newPagesCount === 0) {
|
|
4562
5066
|
logger.debug("All results are duplicates, stopping pagination");
|
|
4563
5067
|
break;
|
|
@@ -4707,7 +5211,8 @@ var confluenceSyncAdapter = {
|
|
|
4707
5211
|
return confluenceService.getPages({
|
|
4708
5212
|
days: options.days,
|
|
4709
5213
|
limit: options.limit,
|
|
4710
|
-
author: author || void 0
|
|
5214
|
+
author: author || void 0,
|
|
5215
|
+
onProgress: options.onProgress ? (fetched) => options.onProgress(fetched, 0) : void 0
|
|
4711
5216
|
});
|
|
4712
5217
|
},
|
|
4713
5218
|
async isAuthenticated() {
|
|
@@ -4723,6 +5228,479 @@ var confluenceSyncAdapter = {
|
|
|
4723
5228
|
}
|
|
4724
5229
|
};
|
|
4725
5230
|
|
|
5231
|
+
// src/sync/google-docs-adapter.ts
|
|
5232
|
+
init_esm_shims();
|
|
5233
|
+
|
|
5234
|
+
// src/services/google-docs.service.ts
|
|
5235
|
+
init_esm_shims();
|
|
5236
|
+
init_errors();
|
|
5237
|
+
init_logger();
|
|
5238
|
+
init_storage_service();
|
|
5239
|
+
var GoogleDocsService = class {
|
|
5240
|
+
/**
|
|
5241
|
+
* Get stored Google credentials, auto-refreshing OAuth tokens if expired
|
|
5242
|
+
*/
|
|
5243
|
+
async getCredentials() {
|
|
5244
|
+
const creds = await storageService.getServiceCredentials("google-docs");
|
|
5245
|
+
if (!creds || !creds.accessToken) {
|
|
5246
|
+
throw new GoogleDocsError("Not authenticated with Google Docs", {
|
|
5247
|
+
hint: "Run: bragduck auth google"
|
|
5248
|
+
});
|
|
5249
|
+
}
|
|
5250
|
+
if (creds.expiresAt && creds.expiresAt < Date.now()) {
|
|
5251
|
+
if (creds.refreshToken) {
|
|
5252
|
+
logger.debug("Google OAuth token expired, refreshing...");
|
|
5253
|
+
await googleAuthService.refreshToken();
|
|
5254
|
+
const refreshed = await storageService.getServiceCredentials("google-docs");
|
|
5255
|
+
if (!refreshed?.accessToken) {
|
|
5256
|
+
throw new GoogleDocsError("Token refresh failed", {
|
|
5257
|
+
hint: "Run: bragduck auth google"
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
return refreshed;
|
|
5261
|
+
}
|
|
5262
|
+
throw new GoogleDocsError("OAuth token has expired and no refresh token available", {
|
|
5263
|
+
hint: "Run: bragduck auth google"
|
|
5264
|
+
});
|
|
5265
|
+
}
|
|
5266
|
+
return creds;
|
|
5267
|
+
}
|
|
5268
|
+
/**
|
|
5269
|
+
* Make authenticated request to Google APIs.
|
|
5270
|
+
* Retries once on 401 (auto-refresh).
|
|
5271
|
+
*/
|
|
5272
|
+
async request(url) {
|
|
5273
|
+
const creds = await this.getCredentials();
|
|
5274
|
+
logger.debug(`Google API: GET ${url}`);
|
|
5275
|
+
const options = {
|
|
5276
|
+
headers: {
|
|
5277
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
5278
|
+
Accept: "application/json"
|
|
5279
|
+
}
|
|
5280
|
+
};
|
|
5281
|
+
let response = await fetch(url, options);
|
|
5282
|
+
if (response.status === 401 && creds.refreshToken) {
|
|
5283
|
+
logger.debug("Google API 401, attempting token refresh and retry");
|
|
5284
|
+
await googleAuthService.refreshToken();
|
|
5285
|
+
const refreshedCreds = await storageService.getServiceCredentials("google-docs");
|
|
5286
|
+
if (refreshedCreds?.accessToken) {
|
|
5287
|
+
response = await fetch(url, {
|
|
5288
|
+
headers: {
|
|
5289
|
+
Authorization: `Bearer ${refreshedCreds.accessToken}`,
|
|
5290
|
+
Accept: "application/json"
|
|
5291
|
+
}
|
|
5292
|
+
});
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5295
|
+
if (!response.ok) {
|
|
5296
|
+
const status = response.status;
|
|
5297
|
+
const statusText = response.statusText;
|
|
5298
|
+
let errorDetail = statusText;
|
|
5299
|
+
try {
|
|
5300
|
+
const body = await response.json();
|
|
5301
|
+
errorDetail = body?.error?.message || body?.error?.errors?.[0]?.message || statusText;
|
|
5302
|
+
logger.debug(`Google API error body: ${JSON.stringify(body)}`);
|
|
5303
|
+
} catch {
|
|
5304
|
+
}
|
|
5305
|
+
if (status === 401) {
|
|
5306
|
+
throw new GoogleDocsError(`Invalid or expired Google token: ${errorDetail}`, {
|
|
5307
|
+
hint: "Run: bragduck auth google"
|
|
5308
|
+
});
|
|
5309
|
+
} else if (status === 403) {
|
|
5310
|
+
throw new GoogleDocsError(`Forbidden - check Drive API permissions: ${errorDetail}`, {
|
|
5311
|
+
hint: "Token needs: drive.metadata.readonly"
|
|
5312
|
+
});
|
|
5313
|
+
} else if (status === 429) {
|
|
5314
|
+
throw new GoogleDocsError("Rate limit exceeded", {
|
|
5315
|
+
hint: "Wait a few minutes before trying again"
|
|
5316
|
+
});
|
|
5317
|
+
}
|
|
5318
|
+
throw new GoogleDocsError(`API request failed: ${errorDetail}`);
|
|
5319
|
+
}
|
|
5320
|
+
return response.json();
|
|
5321
|
+
}
|
|
5322
|
+
/**
|
|
5323
|
+
* Get current user's email address via Drive API (works with drive.metadata.readonly scope)
|
|
5324
|
+
*/
|
|
5325
|
+
async getCurrentUser() {
|
|
5326
|
+
try {
|
|
5327
|
+
const about = await this.request(
|
|
5328
|
+
"https://www.googleapis.com/drive/v3/about?fields=user"
|
|
5329
|
+
);
|
|
5330
|
+
return about.user?.emailAddress || null;
|
|
5331
|
+
} catch {
|
|
5332
|
+
return null;
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
/**
|
|
5336
|
+
* Validate Google credentials using Drive API (drive.metadata.readonly scope is sufficient)
|
|
5337
|
+
*/
|
|
5338
|
+
async validateCredentials() {
|
|
5339
|
+
try {
|
|
5340
|
+
await this.request(
|
|
5341
|
+
"https://www.googleapis.com/drive/v3/about?fields=user"
|
|
5342
|
+
);
|
|
5343
|
+
} catch (error) {
|
|
5344
|
+
if (error instanceof GoogleDocsError) {
|
|
5345
|
+
throw error;
|
|
5346
|
+
}
|
|
5347
|
+
throw new GoogleDocsError("Could not access Google Drive API", {
|
|
5348
|
+
hint: "Check that your credentials are valid",
|
|
5349
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
5350
|
+
});
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
/**
|
|
5354
|
+
* Map Google Workspace MIME type to a short display label
|
|
5355
|
+
*/
|
|
5356
|
+
getMimeTypeLabel(mimeType) {
|
|
5357
|
+
switch (mimeType) {
|
|
5358
|
+
case "application/vnd.google-apps.document":
|
|
5359
|
+
return "Doc";
|
|
5360
|
+
case "application/vnd.google-apps.spreadsheet":
|
|
5361
|
+
return "Sheet";
|
|
5362
|
+
case "application/vnd.google-apps.presentation":
|
|
5363
|
+
return "Slides";
|
|
5364
|
+
default:
|
|
5365
|
+
return "Doc";
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
/**
|
|
5369
|
+
* Transform a Google Drive file to GitCommit format
|
|
5370
|
+
*/
|
|
5371
|
+
transformDocToCommit(file) {
|
|
5372
|
+
const isOwner = file.owners?.[0]?.me === true;
|
|
5373
|
+
const contributionType = isOwner ? "created" : "edited";
|
|
5374
|
+
const typeLabel = this.getMimeTypeLabel(file.mimeType);
|
|
5375
|
+
return {
|
|
5376
|
+
sha: file.id,
|
|
5377
|
+
message: `Google Doc [${typeLabel}]: ${file.name}`,
|
|
5378
|
+
author: file.lastModifyingUser?.displayName || "Unknown",
|
|
5379
|
+
authorEmail: file.lastModifyingUser?.emailAddress || "unknown@example.com",
|
|
5380
|
+
date: file.modifiedByMeTime || file.modifiedTime,
|
|
5381
|
+
url: file.webViewLink,
|
|
5382
|
+
diffStats: {
|
|
5383
|
+
filesChanged: 1,
|
|
5384
|
+
insertions: 50,
|
|
5385
|
+
deletions: 0
|
|
5386
|
+
},
|
|
5387
|
+
externalId: file.id,
|
|
5388
|
+
externalType: contributionType,
|
|
5389
|
+
externalSource: "google-docs",
|
|
5390
|
+
externalUrl: file.webViewLink
|
|
5391
|
+
};
|
|
5392
|
+
}
|
|
5393
|
+
/**
|
|
5394
|
+
* Fetch Google Docs the user has contributed to
|
|
5395
|
+
*/
|
|
5396
|
+
async getDocs(options = {}) {
|
|
5397
|
+
const allFiles = [];
|
|
5398
|
+
let pageToken;
|
|
5399
|
+
const pageSize = 100;
|
|
5400
|
+
let cutoffDate = "";
|
|
5401
|
+
if (options.days) {
|
|
5402
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
5403
|
+
cutoff.setDate(cutoff.getDate() - options.days);
|
|
5404
|
+
cutoffDate = cutoff.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
5405
|
+
}
|
|
5406
|
+
const docTypes = [
|
|
5407
|
+
"mimeType='application/vnd.google-apps.document'",
|
|
5408
|
+
"mimeType='application/vnd.google-apps.spreadsheet'",
|
|
5409
|
+
"mimeType='application/vnd.google-apps.presentation'"
|
|
5410
|
+
].join(" or ");
|
|
5411
|
+
const queryParts = [`(${docTypes})`, "trashed = false"];
|
|
5412
|
+
if (cutoffDate) {
|
|
5413
|
+
queryParts.push(`modifiedTime > '${cutoffDate}'`);
|
|
5414
|
+
}
|
|
5415
|
+
const q = queryParts.join(" and ");
|
|
5416
|
+
const fields = "nextPageToken,files(id,name,mimeType,createdTime,modifiedTime,modifiedByMeTime,webViewLink,owners(me,displayName,emailAddress),lastModifyingUser(displayName,emailAddress))";
|
|
5417
|
+
while (true) {
|
|
5418
|
+
const params = new URLSearchParams({
|
|
5419
|
+
q,
|
|
5420
|
+
fields,
|
|
5421
|
+
pageSize: pageSize.toString(),
|
|
5422
|
+
orderBy: "modifiedByMeTime desc"
|
|
5423
|
+
});
|
|
5424
|
+
if (pageToken) {
|
|
5425
|
+
params.set("pageToken", pageToken);
|
|
5426
|
+
}
|
|
5427
|
+
const url = `https://www.googleapis.com/drive/v3/files?${params.toString()}`;
|
|
5428
|
+
const response = await this.request(url);
|
|
5429
|
+
if (response.files && response.files.length > 0) {
|
|
5430
|
+
const cutoffMs = cutoffDate ? new Date(cutoffDate).getTime() : 0;
|
|
5431
|
+
const userInteracted = response.files.filter((f) => {
|
|
5432
|
+
const modifiedByMeMs = f.modifiedByMeTime ? new Date(f.modifiedByMeTime).getTime() : 0;
|
|
5433
|
+
const isOwnerCreatedRecently = f.owners?.[0]?.me === true && new Date(f.createdTime).getTime() >= cutoffMs;
|
|
5434
|
+
return modifiedByMeMs >= cutoffMs || isOwnerCreatedRecently;
|
|
5435
|
+
});
|
|
5436
|
+
allFiles.push(...userInteracted);
|
|
5437
|
+
}
|
|
5438
|
+
if (options.onProgress) {
|
|
5439
|
+
options.onProgress(allFiles.length, 0);
|
|
5440
|
+
}
|
|
5441
|
+
if (!response.nextPageToken) {
|
|
5442
|
+
break;
|
|
5443
|
+
}
|
|
5444
|
+
if (options.limit && allFiles.length >= options.limit) {
|
|
5445
|
+
break;
|
|
5446
|
+
}
|
|
5447
|
+
pageToken = response.nextPageToken;
|
|
5448
|
+
}
|
|
5449
|
+
const files = options.limit ? allFiles.slice(0, options.limit) : allFiles;
|
|
5450
|
+
return files.map((file) => this.transformDocToCommit(file));
|
|
5451
|
+
}
|
|
5452
|
+
};
|
|
5453
|
+
var googleDocsService = new GoogleDocsService();
|
|
5454
|
+
|
|
5455
|
+
// src/sync/google-docs-adapter.ts
|
|
5456
|
+
var googleDocsSyncAdapter = {
|
|
5457
|
+
name: "google-docs",
|
|
5458
|
+
async validate() {
|
|
5459
|
+
await googleDocsService.validateCredentials();
|
|
5460
|
+
},
|
|
5461
|
+
async getRepositoryInfo() {
|
|
5462
|
+
const user = await googleDocsService.getCurrentUser();
|
|
5463
|
+
const userName = user || "Unknown User";
|
|
5464
|
+
return {
|
|
5465
|
+
owner: userName,
|
|
5466
|
+
name: "Google Docs",
|
|
5467
|
+
fullName: `${userName}'s Google Docs`,
|
|
5468
|
+
url: "https://drive.google.com"
|
|
5469
|
+
};
|
|
5470
|
+
},
|
|
5471
|
+
async fetchWorkItems(options) {
|
|
5472
|
+
return googleDocsService.getDocs({
|
|
5473
|
+
days: options.days,
|
|
5474
|
+
limit: options.limit,
|
|
5475
|
+
onProgress: options.onProgress ? (fetched) => options.onProgress(fetched, 0) : void 0
|
|
5476
|
+
});
|
|
5477
|
+
},
|
|
5478
|
+
async isAuthenticated() {
|
|
5479
|
+
try {
|
|
5480
|
+
await this.validate();
|
|
5481
|
+
return true;
|
|
5482
|
+
} catch {
|
|
5483
|
+
return false;
|
|
5484
|
+
}
|
|
5485
|
+
},
|
|
5486
|
+
async getCurrentUser() {
|
|
5487
|
+
return googleDocsService.getCurrentUser();
|
|
5488
|
+
}
|
|
5489
|
+
};
|
|
5490
|
+
|
|
5491
|
+
// src/sync/slack-adapter.ts
|
|
5492
|
+
init_esm_shims();
|
|
5493
|
+
|
|
5494
|
+
// src/services/slack.service.ts
|
|
5495
|
+
init_esm_shims();
|
|
5496
|
+
init_errors();
|
|
5497
|
+
init_logger();
|
|
5498
|
+
init_storage_service();
|
|
5499
|
+
import { URLSearchParams as URLSearchParams8 } from "url";
|
|
5500
|
+
var SlackService = class {
|
|
5501
|
+
/**
|
|
5502
|
+
* Get stored Slack credentials.
|
|
5503
|
+
* Slack user tokens (xoxp-) don't expire by default — no expiry check needed.
|
|
5504
|
+
*/
|
|
5505
|
+
async getCredentials() {
|
|
5506
|
+
const creds = await storageService.getServiceCredentials("slack");
|
|
5507
|
+
if (!creds || !creds.accessToken) {
|
|
5508
|
+
throw new SlackError("Not authenticated with Slack", {
|
|
5509
|
+
hint: "Run: bragduck auth slack"
|
|
5510
|
+
});
|
|
5511
|
+
}
|
|
5512
|
+
return creds;
|
|
5513
|
+
}
|
|
5514
|
+
/**
|
|
5515
|
+
* Make authenticated GET request to Slack Web API
|
|
5516
|
+
*/
|
|
5517
|
+
async request(url) {
|
|
5518
|
+
const creds = await this.getCredentials();
|
|
5519
|
+
logger.debug(`Slack API: GET ${url}`);
|
|
5520
|
+
const response = await fetch(url, {
|
|
5521
|
+
headers: {
|
|
5522
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
5523
|
+
Accept: "application/json"
|
|
5524
|
+
}
|
|
5525
|
+
});
|
|
5526
|
+
if (!response.ok) {
|
|
5527
|
+
const status = response.status;
|
|
5528
|
+
let errorDetail = response.statusText;
|
|
5529
|
+
try {
|
|
5530
|
+
const body = await response.json();
|
|
5531
|
+
errorDetail = body?.error || errorDetail;
|
|
5532
|
+
logger.debug(`Slack API error body: ${JSON.stringify(body)}`);
|
|
5533
|
+
} catch {
|
|
5534
|
+
}
|
|
5535
|
+
if (status === 401) {
|
|
5536
|
+
throw new SlackError(`Invalid or expired Slack token: ${errorDetail}`, {
|
|
5537
|
+
hint: "Run: bragduck auth slack"
|
|
5538
|
+
});
|
|
5539
|
+
} else if (status === 429) {
|
|
5540
|
+
throw new SlackError("Rate limit exceeded", {
|
|
5541
|
+
hint: "Wait a few minutes before trying again"
|
|
5542
|
+
});
|
|
5543
|
+
}
|
|
5544
|
+
throw new SlackError(`API request failed: ${errorDetail}`);
|
|
5545
|
+
}
|
|
5546
|
+
const data = await response.json();
|
|
5547
|
+
if (data.ok === false) {
|
|
5548
|
+
if (data.error === "invalid_auth" || data.error === "token_revoked") {
|
|
5549
|
+
throw new SlackError(`Invalid or revoked Slack token: ${data.error}`, {
|
|
5550
|
+
hint: "Run: bragduck auth slack"
|
|
5551
|
+
});
|
|
5552
|
+
}
|
|
5553
|
+
throw new SlackError(`Slack API error: ${data.error}`);
|
|
5554
|
+
}
|
|
5555
|
+
return data;
|
|
5556
|
+
}
|
|
5557
|
+
/**
|
|
5558
|
+
* Validate Slack credentials via auth.test
|
|
5559
|
+
*/
|
|
5560
|
+
async validateCredentials() {
|
|
5561
|
+
const creds = await this.getCredentials();
|
|
5562
|
+
const response = await fetch("https://slack.com/api/auth.test", {
|
|
5563
|
+
method: "POST",
|
|
5564
|
+
headers: {
|
|
5565
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
5566
|
+
"Content-Type": "application/json"
|
|
5567
|
+
}
|
|
5568
|
+
});
|
|
5569
|
+
const data = await response.json();
|
|
5570
|
+
if (!data.ok) {
|
|
5571
|
+
throw new SlackError(`Slack credentials invalid: ${data.error || "unknown error"}`, {
|
|
5572
|
+
hint: "Run: bragduck auth slack"
|
|
5573
|
+
});
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
/**
|
|
5577
|
+
* Get current Slack user display name via auth.test
|
|
5578
|
+
*/
|
|
5579
|
+
async getCurrentUser() {
|
|
5580
|
+
try {
|
|
5581
|
+
const creds = await this.getCredentials();
|
|
5582
|
+
const response = await fetch("https://slack.com/api/auth.test", {
|
|
5583
|
+
method: "POST",
|
|
5584
|
+
headers: {
|
|
5585
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
5586
|
+
"Content-Type": "application/json"
|
|
5587
|
+
}
|
|
5588
|
+
});
|
|
5589
|
+
const data = await response.json();
|
|
5590
|
+
return data.ok ? data.user : null;
|
|
5591
|
+
} catch {
|
|
5592
|
+
return null;
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
/**
|
|
5596
|
+
* Fetch messages sent by the authenticated user via search.messages.
|
|
5597
|
+
* Paginates across all pages until exhausted or limit reached.
|
|
5598
|
+
*/
|
|
5599
|
+
async getMessages(options = {}) {
|
|
5600
|
+
const creds = await this.getCredentials();
|
|
5601
|
+
if (!creds.username) {
|
|
5602
|
+
throw new SlackError("No Slack user ID stored", {
|
|
5603
|
+
hint: "Run: bragduck auth slack"
|
|
5604
|
+
});
|
|
5605
|
+
}
|
|
5606
|
+
let query = `from:<@${creds.username}>`;
|
|
5607
|
+
if (options.days) {
|
|
5608
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
5609
|
+
cutoff.setDate(cutoff.getDate() - options.days);
|
|
5610
|
+
const dateStr = cutoff.toISOString().slice(0, 10);
|
|
5611
|
+
query += ` after:${dateStr}`;
|
|
5612
|
+
}
|
|
5613
|
+
const allMessages = [];
|
|
5614
|
+
let page = 1;
|
|
5615
|
+
while (true) {
|
|
5616
|
+
const params = new URLSearchParams8({
|
|
5617
|
+
query,
|
|
5618
|
+
count: "100",
|
|
5619
|
+
page: page.toString()
|
|
5620
|
+
});
|
|
5621
|
+
const url = `https://slack.com/api/search.messages?${params.toString()}`;
|
|
5622
|
+
const response = await this.request(url);
|
|
5623
|
+
const matches = response.messages?.matches || [];
|
|
5624
|
+
allMessages.push(...matches);
|
|
5625
|
+
if (options.onProgress) {
|
|
5626
|
+
options.onProgress(allMessages.length, response.messages?.paging?.total || 0);
|
|
5627
|
+
}
|
|
5628
|
+
const paging = response.messages?.paging;
|
|
5629
|
+
if (!paging || page >= paging.pages) {
|
|
5630
|
+
break;
|
|
5631
|
+
}
|
|
5632
|
+
if (options.limit && allMessages.length >= options.limit) {
|
|
5633
|
+
break;
|
|
5634
|
+
}
|
|
5635
|
+
page++;
|
|
5636
|
+
}
|
|
5637
|
+
const messages = options.limit ? allMessages.slice(0, options.limit) : allMessages;
|
|
5638
|
+
return messages.map((msg) => this.transformMessageToCommit(msg));
|
|
5639
|
+
}
|
|
5640
|
+
/**
|
|
5641
|
+
* Transform a Slack message to GitCommit format
|
|
5642
|
+
*/
|
|
5643
|
+
transformMessageToCommit(msg) {
|
|
5644
|
+
const channelName = msg.channel?.name || msg.channel?.id || "unknown";
|
|
5645
|
+
const preview = msg.text.slice(0, 100);
|
|
5646
|
+
const date = new Date(parseFloat(msg.ts) * 1e3).toISOString();
|
|
5647
|
+
return {
|
|
5648
|
+
sha: msg.ts,
|
|
5649
|
+
message: `Slack [#${channelName}]: ${preview}`,
|
|
5650
|
+
author: msg.username || "Unknown",
|
|
5651
|
+
authorEmail: "unknown@slack",
|
|
5652
|
+
date,
|
|
5653
|
+
url: msg.permalink,
|
|
5654
|
+
diffStats: {
|
|
5655
|
+
filesChanged: 1,
|
|
5656
|
+
insertions: 1,
|
|
5657
|
+
deletions: 0
|
|
5658
|
+
},
|
|
5659
|
+
externalId: msg.ts,
|
|
5660
|
+
externalType: "message",
|
|
5661
|
+
externalSource: "slack",
|
|
5662
|
+
externalUrl: msg.permalink
|
|
5663
|
+
};
|
|
5664
|
+
}
|
|
5665
|
+
};
|
|
5666
|
+
var slackService = new SlackService();
|
|
5667
|
+
|
|
5668
|
+
// src/sync/slack-adapter.ts
|
|
5669
|
+
var slackSyncAdapter = {
|
|
5670
|
+
name: "slack",
|
|
5671
|
+
async validate() {
|
|
5672
|
+
await slackService.validateCredentials();
|
|
5673
|
+
},
|
|
5674
|
+
async getRepositoryInfo() {
|
|
5675
|
+
const user = await slackService.getCurrentUser();
|
|
5676
|
+
const userName = user || "Unknown User";
|
|
5677
|
+
return {
|
|
5678
|
+
owner: userName,
|
|
5679
|
+
name: "Slack",
|
|
5680
|
+
fullName: `${userName}'s Slack Messages`,
|
|
5681
|
+
url: "https://slack.com"
|
|
5682
|
+
};
|
|
5683
|
+
},
|
|
5684
|
+
async fetchWorkItems(options) {
|
|
5685
|
+
return slackService.getMessages({
|
|
5686
|
+
days: options.days,
|
|
5687
|
+
limit: options.limit,
|
|
5688
|
+
onProgress: options.onProgress ? (fetched, total) => options.onProgress(fetched, total) : void 0
|
|
5689
|
+
});
|
|
5690
|
+
},
|
|
5691
|
+
async isAuthenticated() {
|
|
5692
|
+
try {
|
|
5693
|
+
await this.validate();
|
|
5694
|
+
return true;
|
|
5695
|
+
} catch {
|
|
5696
|
+
return false;
|
|
5697
|
+
}
|
|
5698
|
+
},
|
|
5699
|
+
async getCurrentUser() {
|
|
5700
|
+
return slackService.getCurrentUser();
|
|
5701
|
+
}
|
|
5702
|
+
};
|
|
5703
|
+
|
|
4726
5704
|
// src/sync/adapter-factory.ts
|
|
4727
5705
|
var AdapterFactory = class {
|
|
4728
5706
|
/**
|
|
@@ -4741,6 +5719,10 @@ var AdapterFactory = class {
|
|
|
4741
5719
|
return jiraSyncAdapter;
|
|
4742
5720
|
case "confluence":
|
|
4743
5721
|
return confluenceSyncAdapter;
|
|
5722
|
+
case "google-docs":
|
|
5723
|
+
return googleDocsSyncAdapter;
|
|
5724
|
+
case "slack":
|
|
5725
|
+
return slackSyncAdapter;
|
|
4744
5726
|
default:
|
|
4745
5727
|
throw new Error(`Unknown source type: ${source}`);
|
|
4746
5728
|
}
|
|
@@ -4749,7 +5731,7 @@ var AdapterFactory = class {
|
|
|
4749
5731
|
* Check if adapter is available for source
|
|
4750
5732
|
*/
|
|
4751
5733
|
static isSupported(source) {
|
|
4752
|
-
return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab" || source === "jira" || source === "confluence";
|
|
5734
|
+
return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab" || source === "jira" || source === "confluence" || source === "google-docs" || source === "slack";
|
|
4753
5735
|
}
|
|
4754
5736
|
};
|
|
4755
5737
|
|
|
@@ -5216,11 +6198,18 @@ async function ensureAuthenticated() {
|
|
|
5216
6198
|
// src/ui/spinners.ts
|
|
5217
6199
|
init_esm_shims();
|
|
5218
6200
|
import ora from "ora";
|
|
5219
|
-
function
|
|
6201
|
+
function createFetchSpinner(text) {
|
|
5220
6202
|
return ora({
|
|
5221
6203
|
text,
|
|
5222
6204
|
color: "cyan",
|
|
5223
|
-
spinner: "
|
|
6205
|
+
spinner: "bouncingBar"
|
|
6206
|
+
});
|
|
6207
|
+
}
|
|
6208
|
+
function createValidateSpinner(text) {
|
|
6209
|
+
return ora({
|
|
6210
|
+
text,
|
|
6211
|
+
color: "cyan",
|
|
6212
|
+
spinner: "line"
|
|
5224
6213
|
});
|
|
5225
6214
|
}
|
|
5226
6215
|
function createStepSpinner(currentStep, totalSteps, text) {
|
|
@@ -5231,8 +6220,12 @@ function createStepSpinner(currentStep, totalSteps, text) {
|
|
|
5231
6220
|
spinner: "dots"
|
|
5232
6221
|
});
|
|
5233
6222
|
}
|
|
6223
|
+
function updateStepSpinner(spinner, currentStep, totalSteps, text) {
|
|
6224
|
+
const stepIndicator = theme.step(currentStep, totalSteps);
|
|
6225
|
+
spinner.text = `${stepIndicator} ${text}`;
|
|
6226
|
+
}
|
|
5234
6227
|
function fetchingBragsSpinner() {
|
|
5235
|
-
return
|
|
6228
|
+
return createFetchSpinner("Fetching your brags...");
|
|
5236
6229
|
}
|
|
5237
6230
|
function succeedSpinner(spinner, text) {
|
|
5238
6231
|
if (text) {
|
|
@@ -5257,6 +6250,20 @@ function failStepSpinner(spinner, currentStep, totalSteps, text) {
|
|
|
5257
6250
|
spinner.fail(`${stepIndicator} ${colors.error(text)}`);
|
|
5258
6251
|
}
|
|
5259
6252
|
|
|
6253
|
+
// src/ui/header.ts
|
|
6254
|
+
init_esm_shims();
|
|
6255
|
+
init_logger();
|
|
6256
|
+
function showCommandHeader(userInfo, tier) {
|
|
6257
|
+
if (!userInfo?.email) return;
|
|
6258
|
+
if (tier) {
|
|
6259
|
+
const tierLabel = tier.charAt(0).toUpperCase() + tier.slice(1).toLowerCase() + " plan";
|
|
6260
|
+
logger.log(colors.dim(` ${userInfo.email} \xB7 ${tierLabel}`));
|
|
6261
|
+
} else {
|
|
6262
|
+
logger.log(colors.dim(` ${userInfo.email}`));
|
|
6263
|
+
}
|
|
6264
|
+
logger.log("");
|
|
6265
|
+
}
|
|
6266
|
+
|
|
5260
6267
|
// src/utils/repo-scanner.ts
|
|
5261
6268
|
init_esm_shims();
|
|
5262
6269
|
import { promises as fs2 } from "fs";
|
|
@@ -5509,12 +6516,12 @@ async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, opt
|
|
|
5509
6516
|
return { repoName: repo.name, service: repo.service, created: 0, skipped: duplicates.length };
|
|
5510
6517
|
}
|
|
5511
6518
|
logger.log("");
|
|
5512
|
-
const
|
|
6519
|
+
const createSpinner = createStepSpinner(
|
|
5513
6520
|
4,
|
|
5514
6521
|
TOTAL_STEPS,
|
|
5515
6522
|
`Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
|
|
5516
6523
|
);
|
|
5517
|
-
|
|
6524
|
+
createSpinner.start();
|
|
5518
6525
|
const createRequest = {
|
|
5519
6526
|
brags: acceptedBrags.map((refined, index) => {
|
|
5520
6527
|
const originalCommit = selectedCommits[index];
|
|
@@ -5558,14 +6565,14 @@ async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, opt
|
|
|
5558
6565
|
]);
|
|
5559
6566
|
logger.debug(`API response: ${createResponse.created} brags created`);
|
|
5560
6567
|
succeedStepSpinner(
|
|
5561
|
-
|
|
6568
|
+
createSpinner,
|
|
5562
6569
|
4,
|
|
5563
6570
|
TOTAL_STEPS,
|
|
5564
6571
|
`Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
|
|
5565
6572
|
);
|
|
5566
6573
|
logger.log("");
|
|
5567
6574
|
} catch (error) {
|
|
5568
|
-
failStepSpinner(
|
|
6575
|
+
failStepSpinner(createSpinner, 4, TOTAL_STEPS, "Failed to create brags");
|
|
5569
6576
|
logger.log("");
|
|
5570
6577
|
throw error;
|
|
5571
6578
|
}
|
|
@@ -5697,7 +6704,7 @@ async function syncMultipleRepositories(repos, options) {
|
|
|
5697
6704
|
|
|
5698
6705
|
// src/commands/sync.ts
|
|
5699
6706
|
async function promptSelectService() {
|
|
5700
|
-
const nonGitServices = ["atlassian", "jira", "confluence"];
|
|
6707
|
+
const nonGitServices = ["atlassian", "jira", "confluence", "google-docs", "slack"];
|
|
5701
6708
|
const authenticatedServices = await storageService.getAuthenticatedServices();
|
|
5702
6709
|
const allServices = [
|
|
5703
6710
|
"github",
|
|
@@ -5705,7 +6712,9 @@ async function promptSelectService() {
|
|
|
5705
6712
|
"bitbucket",
|
|
5706
6713
|
"atlassian",
|
|
5707
6714
|
"jira",
|
|
5708
|
-
"confluence"
|
|
6715
|
+
"confluence",
|
|
6716
|
+
"google-docs",
|
|
6717
|
+
"slack"
|
|
5709
6718
|
];
|
|
5710
6719
|
const authenticatedSyncServices = authenticatedServices.filter(
|
|
5711
6720
|
(service) => service !== "bragduck" && allServices.includes(service)
|
|
@@ -5720,7 +6729,7 @@ async function promptSelectService() {
|
|
|
5720
6729
|
for (const service of nonGitServices) {
|
|
5721
6730
|
const isAuth = await storageService.isServiceAuthenticated(service);
|
|
5722
6731
|
const indicator = isAuth ? "\u2713" : "\u2717";
|
|
5723
|
-
const serviceLabel =
|
|
6732
|
+
const serviceLabel = getServiceLabel(service);
|
|
5724
6733
|
serviceChoices.push({
|
|
5725
6734
|
name: `${indicator} ${serviceLabel}`,
|
|
5726
6735
|
value: service,
|
|
@@ -5728,7 +6737,7 @@ async function promptSelectService() {
|
|
|
5728
6737
|
});
|
|
5729
6738
|
}
|
|
5730
6739
|
if (authenticatedSyncServices.length > 0) {
|
|
5731
|
-
const serviceNames = authenticatedSyncServices.map(
|
|
6740
|
+
const serviceNames = authenticatedSyncServices.map(getServiceLabel).join(", ");
|
|
5732
6741
|
serviceChoices.push({
|
|
5733
6742
|
name: `\u2713 All authenticated services (${serviceNames})`,
|
|
5734
6743
|
value: "all",
|
|
@@ -5743,7 +6752,8 @@ async function promptSelectService() {
|
|
|
5743
6752
|
}
|
|
5744
6753
|
async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, sharedOrgId) {
|
|
5745
6754
|
const adapter = AdapterFactory.getAdapter(sourceType);
|
|
5746
|
-
const
|
|
6755
|
+
const validationText = sourceType === "jira" || sourceType === "confluence" ? "Connecting to Atlassian instance" : sourceType === "google-docs" ? "Connecting to Google Drive" : "Validating repository";
|
|
6756
|
+
const repoSpinner = createStepSpinner(2, TOTAL_STEPS, validationText);
|
|
5747
6757
|
repoSpinner.start();
|
|
5748
6758
|
const VALIDATION_TIMEOUT = 3e4;
|
|
5749
6759
|
let repoInfo;
|
|
@@ -5789,7 +6799,16 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5789
6799
|
fetchSpinner.start();
|
|
5790
6800
|
const workItems = await adapter.fetchWorkItems({
|
|
5791
6801
|
days,
|
|
5792
|
-
author: await adapter.getCurrentUser() || void 0
|
|
6802
|
+
author: await adapter.getCurrentUser() || void 0,
|
|
6803
|
+
onProgress: (fetched, total) => {
|
|
6804
|
+
const progress = total > 0 ? `${fetched} of ${total}` : `${fetched} so far`;
|
|
6805
|
+
updateStepSpinner(
|
|
6806
|
+
fetchSpinner,
|
|
6807
|
+
3,
|
|
6808
|
+
TOTAL_STEPS,
|
|
6809
|
+
`Fetching work items from the last ${days} days (${progress})`
|
|
6810
|
+
);
|
|
6811
|
+
}
|
|
5793
6812
|
});
|
|
5794
6813
|
if (workItems.length === 0) {
|
|
5795
6814
|
failStepSpinner(fetchSpinner, 3, TOTAL_STEPS, `No work items found in the last ${days} days`);
|
|
@@ -5806,6 +6825,8 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5806
6825
|
logger.log("");
|
|
5807
6826
|
logger.log(formatCommitStats(workItems));
|
|
5808
6827
|
logger.log("");
|
|
6828
|
+
const dupSpinner = createFetchSpinner("Checking for duplicates...");
|
|
6829
|
+
dupSpinner.start();
|
|
5809
6830
|
const existingBrags = await apiService.listBrags({ limit: 100 });
|
|
5810
6831
|
logger.debug(`Fetched ${existingBrags.brags.length} existing brags`);
|
|
5811
6832
|
const existingUrls = new Set(existingBrags.brags.flatMap((b) => b.attachments || []));
|
|
@@ -5814,13 +6835,14 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5814
6835
|
const newWorkItems = workItems.filter((c) => !c.url || !existingUrls.has(c.url));
|
|
5815
6836
|
logger.debug(`Found ${duplicates.length} duplicates, ${newWorkItems.length} new items`);
|
|
5816
6837
|
if (duplicates.length > 0) {
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
`\u2139 ${duplicates.length} work item${duplicates.length > 1 ? "s" : ""} already exist in Bragduck (will be skipped)`
|
|
5821
|
-
)
|
|
6838
|
+
succeedSpinner(
|
|
6839
|
+
dupSpinner,
|
|
6840
|
+
`${duplicates.length} item${duplicates.length > 1 ? "s" : ""} already synced, skipping`
|
|
5822
6841
|
);
|
|
5823
6842
|
logger.log("");
|
|
6843
|
+
} else {
|
|
6844
|
+
succeedSpinner(dupSpinner, "No duplicates found");
|
|
6845
|
+
logger.log("");
|
|
5824
6846
|
}
|
|
5825
6847
|
if (newWorkItems.length === 0) {
|
|
5826
6848
|
logger.log("");
|
|
@@ -5907,23 +6929,27 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5907
6929
|
if (sharedOrgId === void 0) {
|
|
5908
6930
|
const userInfo = authService.getUserInfo();
|
|
5909
6931
|
if (userInfo?.id) {
|
|
6932
|
+
const orgSpinner = createFetchSpinner("Loading organizations...");
|
|
6933
|
+
orgSpinner.start();
|
|
5910
6934
|
try {
|
|
5911
6935
|
const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
|
|
6936
|
+
orgSpinner.stop();
|
|
5912
6937
|
if (orgsResponse.items.length > 0) {
|
|
5913
6938
|
selectedOrgId = await promptSelectOrganisation(orgsResponse.items);
|
|
5914
6939
|
logger.log("");
|
|
5915
6940
|
}
|
|
5916
6941
|
} catch {
|
|
6942
|
+
orgSpinner.stop();
|
|
5917
6943
|
logger.debug("Failed to fetch organisations, skipping org selection");
|
|
5918
6944
|
}
|
|
5919
6945
|
}
|
|
5920
6946
|
}
|
|
5921
|
-
const
|
|
6947
|
+
const createSpinner = createStepSpinner(
|
|
5922
6948
|
5,
|
|
5923
6949
|
TOTAL_STEPS,
|
|
5924
6950
|
`Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
|
|
5925
6951
|
);
|
|
5926
|
-
|
|
6952
|
+
createSpinner.start();
|
|
5927
6953
|
const createRequest = {
|
|
5928
6954
|
brags: acceptedBrags.map((refined, index) => {
|
|
5929
6955
|
const originalCommit = selectedCommits[index];
|
|
@@ -5971,14 +6997,14 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5971
6997
|
]);
|
|
5972
6998
|
logger.debug(`API response: ${createResponse.created} brags created`);
|
|
5973
6999
|
succeedStepSpinner(
|
|
5974
|
-
|
|
7000
|
+
createSpinner,
|
|
5975
7001
|
5,
|
|
5976
7002
|
TOTAL_STEPS,
|
|
5977
7003
|
`Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
|
|
5978
7004
|
);
|
|
5979
7005
|
logger.log("");
|
|
5980
7006
|
} catch (error) {
|
|
5981
|
-
failStepSpinner(
|
|
7007
|
+
failStepSpinner(createSpinner, 5, TOTAL_STEPS, "Failed to create brags");
|
|
5982
7008
|
logger.log("");
|
|
5983
7009
|
throw error;
|
|
5984
7010
|
}
|
|
@@ -5997,7 +7023,9 @@ async function syncAllAuthenticatedServices(options) {
|
|
|
5997
7023
|
"bitbucket",
|
|
5998
7024
|
"atlassian",
|
|
5999
7025
|
"jira",
|
|
6000
|
-
"confluence"
|
|
7026
|
+
"confluence",
|
|
7027
|
+
"google-docs",
|
|
7028
|
+
"slack"
|
|
6001
7029
|
];
|
|
6002
7030
|
const authenticatedServices = await storageService.getAuthenticatedServices();
|
|
6003
7031
|
let servicesToSync = authenticatedServices.filter(
|
|
@@ -6064,14 +7092,18 @@ async function syncAllAuthenticatedServices(options) {
|
|
|
6064
7092
|
if (!options.turbo) {
|
|
6065
7093
|
const userInfo = authService.getUserInfo();
|
|
6066
7094
|
if (userInfo?.id) {
|
|
7095
|
+
const orgSpinner = createFetchSpinner("Loading organizations...");
|
|
7096
|
+
orgSpinner.start();
|
|
6067
7097
|
try {
|
|
6068
7098
|
const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
|
|
7099
|
+
orgSpinner.stop();
|
|
6069
7100
|
if (orgsResponse.items.length > 0) {
|
|
6070
7101
|
const defaultOrgId = storageService.getConfig("defaultCompany");
|
|
6071
7102
|
sharedOrgId = await promptSelectOrganisationWithDefault(orgsResponse.items, defaultOrgId);
|
|
6072
7103
|
logger.log("");
|
|
6073
7104
|
}
|
|
6074
7105
|
} catch {
|
|
7106
|
+
orgSpinner.stop();
|
|
6075
7107
|
logger.debug("Failed to fetch organisations, skipping org selection");
|
|
6076
7108
|
}
|
|
6077
7109
|
}
|
|
@@ -6080,7 +7112,7 @@ async function syncAllAuthenticatedServices(options) {
|
|
|
6080
7112
|
for (let i = 0; i < servicesToSync.length; i++) {
|
|
6081
7113
|
const service = servicesToSync[i];
|
|
6082
7114
|
if (!service) continue;
|
|
6083
|
-
const serviceLabel =
|
|
7115
|
+
const serviceLabel = getServiceLabel(service);
|
|
6084
7116
|
logger.log(
|
|
6085
7117
|
colors.highlight(`\u2501\u2501\u2501 Syncing ${i + 1}/${servicesToSync.length}: ${serviceLabel} \u2501\u2501\u2501`)
|
|
6086
7118
|
);
|
|
@@ -6121,7 +7153,7 @@ async function syncAllAuthenticatedServices(options) {
|
|
|
6121
7153
|
)
|
|
6122
7154
|
);
|
|
6123
7155
|
for (const result of successful) {
|
|
6124
|
-
const serviceLabel =
|
|
7156
|
+
const serviceLabel = getServiceLabel(result.service);
|
|
6125
7157
|
if (result.created === 0 && result.skipped > 0) {
|
|
6126
7158
|
logger.info(
|
|
6127
7159
|
` \u2022 ${serviceLabel}: ${colors.dim(`All ${result.skipped} item${result.skipped !== 1 ? "s" : ""} already synced`)}`
|
|
@@ -6141,7 +7173,7 @@ async function syncAllAuthenticatedServices(options) {
|
|
|
6141
7173
|
colors.warning(`\u2717 Failed to sync ${failed.length} service${failed.length > 1 ? "s" : ""}:`)
|
|
6142
7174
|
);
|
|
6143
7175
|
for (const result of failed) {
|
|
6144
|
-
const serviceLabel =
|
|
7176
|
+
const serviceLabel = getServiceLabel(result.service);
|
|
6145
7177
|
logger.info(` \u2022 ${serviceLabel}: ${result.error}`);
|
|
6146
7178
|
}
|
|
6147
7179
|
logger.log("");
|
|
@@ -6169,10 +7201,19 @@ async function syncCommand(options = {}) {
|
|
|
6169
7201
|
process.exit(1);
|
|
6170
7202
|
}
|
|
6171
7203
|
logger.debug("Fetching subscription status...");
|
|
6172
|
-
const
|
|
7204
|
+
const subSpinner = createValidateSpinner("Checking subscription...");
|
|
7205
|
+
subSpinner.start();
|
|
7206
|
+
let subscriptionStatus;
|
|
7207
|
+
try {
|
|
7208
|
+
subscriptionStatus = await apiService.getSubscriptionStatus();
|
|
7209
|
+
} catch (error) {
|
|
7210
|
+
failSpinner(subSpinner, "Failed to check subscription");
|
|
7211
|
+
throw error;
|
|
7212
|
+
}
|
|
6173
7213
|
logger.debug("Subscription status response:", JSON.stringify(subscriptionStatus, null, 2));
|
|
6174
7214
|
if (subscriptionStatus.tier === "FREE") {
|
|
6175
7215
|
logger.debug("FREE tier detected - blocking sync command");
|
|
7216
|
+
failSpinner(subSpinner, "Free plan \u2014 upgrade to use CLI sync");
|
|
6176
7217
|
logger.log("");
|
|
6177
7218
|
logger.log(
|
|
6178
7219
|
boxen5(
|
|
@@ -6187,7 +7228,13 @@ async function syncCommand(options = {}) {
|
|
|
6187
7228
|
logger.log("");
|
|
6188
7229
|
return;
|
|
6189
7230
|
}
|
|
7231
|
+
succeedSpinner(
|
|
7232
|
+
subSpinner,
|
|
7233
|
+
`${subscriptionStatus.tier.charAt(0).toUpperCase() + subscriptionStatus.tier.slice(1).toLowerCase()} plan \xB7 Ready`
|
|
7234
|
+
);
|
|
6190
7235
|
logger.debug(`Subscription tier "${subscriptionStatus.tier}" - proceeding with sync`);
|
|
7236
|
+
const userInfo = authService.getUserInfo();
|
|
7237
|
+
showCommandHeader(userInfo, subscriptionStatus.tier);
|
|
6191
7238
|
let selectedSource;
|
|
6192
7239
|
if (options.all) {
|
|
6193
7240
|
selectedSource = "all";
|
|
@@ -6198,7 +7245,9 @@ async function syncCommand(options = {}) {
|
|
|
6198
7245
|
logger.log("");
|
|
6199
7246
|
logger.error(`Unsupported source: ${options.source}`);
|
|
6200
7247
|
logger.log("");
|
|
6201
|
-
logger.info(
|
|
7248
|
+
logger.info(
|
|
7249
|
+
"Supported sources: github, gitlab, bitbucket, atlassian, jira, confluence, google-docs"
|
|
7250
|
+
);
|
|
6202
7251
|
return;
|
|
6203
7252
|
}
|
|
6204
7253
|
selectedSource = sourceType;
|
|
@@ -6213,6 +7262,69 @@ async function syncCommand(options = {}) {
|
|
|
6213
7262
|
const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Preparing sync");
|
|
6214
7263
|
detectionSpinner.start();
|
|
6215
7264
|
if (selectedSource === "git") {
|
|
7265
|
+
let authCheckService;
|
|
7266
|
+
let authCheckPassed = false;
|
|
7267
|
+
try {
|
|
7268
|
+
const detection = await sourceDetector.detectSources({}, process.cwd());
|
|
7269
|
+
if (detection.detected.length > 0) {
|
|
7270
|
+
authCheckService = detection.detected[0]?.type;
|
|
7271
|
+
authCheckPassed = detection.detected.some((s) => s.isAuthenticated);
|
|
7272
|
+
}
|
|
7273
|
+
} catch {
|
|
7274
|
+
}
|
|
7275
|
+
if (!authCheckPassed && authCheckService) {
|
|
7276
|
+
failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Not authenticated");
|
|
7277
|
+
logger.log("");
|
|
7278
|
+
logger.log(
|
|
7279
|
+
boxen5(
|
|
7280
|
+
theme.warning("Authentication Required") + `
|
|
7281
|
+
|
|
7282
|
+
Not authenticated with ${authCheckService.charAt(0).toUpperCase() + authCheckService.slice(1)}.
|
|
7283
|
+
|
|
7284
|
+
To authenticate:
|
|
7285
|
+
|
|
7286
|
+
` + getGitAuthCommand(authCheckService),
|
|
7287
|
+
{ ...boxStyles.warning, padding: 1, margin: 1 }
|
|
7288
|
+
)
|
|
7289
|
+
);
|
|
7290
|
+
logger.log("");
|
|
7291
|
+
return;
|
|
7292
|
+
}
|
|
7293
|
+
if (!authCheckPassed && !authCheckService) {
|
|
7294
|
+
const gitServices = ["github", "gitlab", "bitbucket"];
|
|
7295
|
+
const authResults = await Promise.all(
|
|
7296
|
+
gitServices.map(async (s) => ({
|
|
7297
|
+
service: s,
|
|
7298
|
+
auth: await sourceDetector.checkAuthentication(s)
|
|
7299
|
+
}))
|
|
7300
|
+
);
|
|
7301
|
+
const anyAuth = authResults.some((r) => r.auth);
|
|
7302
|
+
if (!anyAuth) {
|
|
7303
|
+
failStepSpinner(
|
|
7304
|
+
detectionSpinner,
|
|
7305
|
+
1,
|
|
7306
|
+
TOTAL_STEPS,
|
|
7307
|
+
"Not authenticated with any git service"
|
|
7308
|
+
);
|
|
7309
|
+
logger.log("");
|
|
7310
|
+
logger.log(
|
|
7311
|
+
boxen5(
|
|
7312
|
+
theme.warning("Authentication Required") + `
|
|
7313
|
+
|
|
7314
|
+
Not authenticated with any git service.
|
|
7315
|
+
|
|
7316
|
+
To authenticate:
|
|
7317
|
+
|
|
7318
|
+
GitHub: ${theme.command("gh auth login")}
|
|
7319
|
+
GitLab: ${theme.command("bragduck auth gitlab")}
|
|
7320
|
+
Bitbucket: ${theme.command("bragduck auth atlassian")}`,
|
|
7321
|
+
{ ...boxStyles.warning, padding: 1, margin: 1 }
|
|
7322
|
+
)
|
|
7323
|
+
);
|
|
7324
|
+
logger.log("");
|
|
7325
|
+
return;
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
6216
7328
|
succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Discovering repositories");
|
|
6217
7329
|
logger.log("");
|
|
6218
7330
|
const repos = await discoverRepositories();
|
|
@@ -6261,6 +7373,24 @@ async function syncCommand(options = {}) {
|
|
|
6261
7373
|
);
|
|
6262
7374
|
return;
|
|
6263
7375
|
}
|
|
7376
|
+
if (!creds?.accessToken) {
|
|
7377
|
+
failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Not authenticated with ${sourceType}`);
|
|
7378
|
+
logger.log("");
|
|
7379
|
+
logger.log(
|
|
7380
|
+
boxen5(
|
|
7381
|
+
theme.warning("Authentication Required") + `
|
|
7382
|
+
|
|
7383
|
+
Not authenticated with ${sourceType.charAt(0).toUpperCase() + sourceType.slice(1)}.
|
|
7384
|
+
|
|
7385
|
+
To authenticate:
|
|
7386
|
+
|
|
7387
|
+
${theme.command("bragduck auth atlassian")}`,
|
|
7388
|
+
{ ...boxStyles.warning, padding: 1, margin: 1 }
|
|
7389
|
+
)
|
|
7390
|
+
);
|
|
7391
|
+
logger.log("");
|
|
7392
|
+
return;
|
|
7393
|
+
}
|
|
6264
7394
|
}
|
|
6265
7395
|
succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source: ${theme.value(sourceType)}`);
|
|
6266
7396
|
logger.log("");
|
|
@@ -6299,6 +7429,23 @@ async function syncCommand(options = {}) {
|
|
|
6299
7429
|
process.exit(1);
|
|
6300
7430
|
}
|
|
6301
7431
|
}
|
|
7432
|
+
function getServiceLabel(service) {
|
|
7433
|
+
if (service === "google-docs") return "Google Docs";
|
|
7434
|
+
if (service === "slack") return "Slack";
|
|
7435
|
+
return service.charAt(0).toUpperCase() + service.slice(1);
|
|
7436
|
+
}
|
|
7437
|
+
function getGitAuthCommand(service) {
|
|
7438
|
+
if (service === "github") {
|
|
7439
|
+
return ` ${theme.command("gh auth login")}`;
|
|
7440
|
+
}
|
|
7441
|
+
if (service === "gitlab") {
|
|
7442
|
+
return ` ${theme.command("bragduck auth gitlab")}`;
|
|
7443
|
+
}
|
|
7444
|
+
if (service === "bitbucket" || service === "atlassian") {
|
|
7445
|
+
return ` ${theme.command("bragduck auth atlassian")}`;
|
|
7446
|
+
}
|
|
7447
|
+
return ` ${theme.command("bragduck auth login")}`;
|
|
7448
|
+
}
|
|
6302
7449
|
function getErrorHint(error, sourceType) {
|
|
6303
7450
|
if (error.name === "GitHubError") {
|
|
6304
7451
|
return 'Make sure you are in a GitHub repository and have authenticated with "gh auth login"';
|
|
@@ -6312,6 +7459,9 @@ function getErrorHint(error, sourceType) {
|
|
|
6312
7459
|
}
|
|
6313
7460
|
return 'Run "bragduck auth login" to login again';
|
|
6314
7461
|
}
|
|
7462
|
+
if (error.name === "GoogleDocsError") {
|
|
7463
|
+
return 'Run "bragduck auth google" to re-authenticate with Google';
|
|
7464
|
+
}
|
|
6315
7465
|
if (error.name === "NetworkError") {
|
|
6316
7466
|
return "Check your internet connection and try again";
|
|
6317
7467
|
}
|
|
@@ -6391,6 +7541,7 @@ init_api_service();
|
|
|
6391
7541
|
import boxen7 from "boxen";
|
|
6392
7542
|
import Table2 from "cli-table3";
|
|
6393
7543
|
init_logger();
|
|
7544
|
+
init_auth_service();
|
|
6394
7545
|
async function listCommand(options = {}) {
|
|
6395
7546
|
logger.log("");
|
|
6396
7547
|
try {
|
|
@@ -6423,6 +7574,7 @@ async function listCommand(options = {}) {
|
|
|
6423
7574
|
return;
|
|
6424
7575
|
}
|
|
6425
7576
|
succeedSpinner(spinner, `Found ${response.total} brag${response.total > 1 ? "s" : ""}`);
|
|
7577
|
+
showCommandHeader(authService.getUserInfo());
|
|
6426
7578
|
logger.log("");
|
|
6427
7579
|
if (options.oneline) {
|
|
6428
7580
|
logger.log(formatBragsOneline(response.brags));
|
|
@@ -6756,13 +7908,15 @@ function getConfigHint(error) {
|
|
|
6756
7908
|
// src/cli.ts
|
|
6757
7909
|
init_version();
|
|
6758
7910
|
init_logger();
|
|
6759
|
-
var
|
|
6760
|
-
var
|
|
6761
|
-
var packageJsonPath =
|
|
6762
|
-
var packageJson = JSON.parse(
|
|
7911
|
+
var __filename9 = fileURLToPath9(import.meta.url);
|
|
7912
|
+
var __dirname9 = dirname8(__filename9);
|
|
7913
|
+
var packageJsonPath = join10(__dirname9, "../../package.json");
|
|
7914
|
+
var packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf-8"));
|
|
6763
7915
|
var program = new Command();
|
|
6764
7916
|
program.name("bragduck").description("CLI tool for managing developer achievements and brags\nAliases: 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)");
|
|
6765
|
-
program.command("auth [subcommand]").description(
|
|
7917
|
+
program.command("auth [subcommand]").description(
|
|
7918
|
+
"Manage authentication (subcommands: login, status, github, gitlab, bitbucket, atlassian, google, slack)"
|
|
7919
|
+
).action(async (subcommand) => {
|
|
6766
7920
|
try {
|
|
6767
7921
|
await authCommand(subcommand);
|
|
6768
7922
|
} catch (error) {
|